insomnia/packages/insomnia-app/app/account/crypt.js
Gregory Schier 0a616fba6b
Version Control (beta) (#1439)
* VCS proof of concept underway!

* Stuff

* Some things

* Replace deprecated Electron makeSingleInstance

* Rename `window` variables so not to be confused with window object

* Don't unnecessarily update request when URL does not change

* Regenerate package-lock

* Fix tests + ESLint

* Publish

 - insomnia-app@1.0.49
 - insomnia-cookies@0.0.12
 - insomnia-httpsnippet@1.16.18
 - insomnia-importers@2.0.13
 - insomnia-libcurl@0.0.23
 - insomnia-prettify@0.1.7
 - insomnia-url@0.1.6
 - insomnia-xpath@1.0.9
 - insomnia-plugin-base64@1.0.6
 - insomnia-plugin-cookie-jar@1.0.8
 - insomnia-plugin-core-themes@1.0.5
 - insomnia-plugin-default-headers@1.1.9
 - insomnia-plugin-file@1.0.7
 - insomnia-plugin-hash@1.0.7
 - insomnia-plugin-jsonpath@1.0.12
 - insomnia-plugin-now@1.0.11
 - insomnia-plugin-os@1.0.13
 - insomnia-plugin-prompt@1.1.9
 - insomnia-plugin-request@1.0.18
 - insomnia-plugin-response@1.0.16
 - insomnia-plugin-uuid@1.0.10

* Broken but w/e

* Some tweaks

* Big refactor. Create local snapshots and push done

* POC merging and a lot of improvements

* Lots of work done on initial UI/UX

* Fix old tests

* Atomic writes and size-based batches

* Update StageEntry definition once again to be better

* Factor out GraphQL query logic

* Merge algorithm, history modal, other minor things

* Fix test

* Merge, checkout, revert w/ user changes now work

* Force UI to refresh when switching branches changes active request

* Rough draft pull() and some cleanup

* E2EE stuff and some refactoring

* Add ability to share project with team and fixed tests

* VCS now created in root component and better remote project handling

* Remove unused definition

* Publish

 - insomnia-account@0.0.2
 - insomnia-app@1.1.1
 - insomnia-cookies@0.0.14
 - insomnia-httpsnippet@1.16.20
 - insomnia-importers@2.0.15
 - insomnia-libcurl@0.0.25
 - insomnia-prettify@0.1.9
 - insomnia-sync@0.0.2
 - insomnia-url@0.1.8
 - insomnia-xpath@1.0.11
 - insomnia-plugin-base64@1.0.8
 - insomnia-plugin-cookie-jar@1.0.10
 - insomnia-plugin-core-themes@1.0.7
 - insomnia-plugin-file@1.0.9
 - insomnia-plugin-hash@1.0.9
 - insomnia-plugin-jsonpath@1.0.14
 - insomnia-plugin-now@1.0.13
 - insomnia-plugin-os@1.0.15
 - insomnia-plugin-prompt@1.1.11
 - insomnia-plugin-request@1.0.20
 - insomnia-plugin-response@1.0.18
 - insomnia-plugin-uuid@1.0.12

* Move some deps around

* Fix Flow errors

* Update package.json

* Fix eslint errors

* Fix tests

* Update deps

* bootstrap insomnia-sync

* TRy fixing appveyor

* Try something else

* Bump lerna

* try powershell

*  Try again

* Fix imports

* Fixed errors

* sync types refactor

* Show remote projects in workspace dropdown

* Improved pulling of non-local workspaces

* Loading indicators and some tweaks

* Clean up sync staging modal

* Some sync improvements:

- No longer store stage
- Upgrade Electron
- Sync UI/UX improvements

* Fix snyc tests

* Upgraded deps and hot loader tweaks (it's broken for some reason)

* Fix tests

* Branches dialog, network refactoring, some tweaks

* Fixed merging when other branch is empty

* A bunch of small fixes from real testing

* Fixed pull merge logic

* Fix tests

* Some bug fixes

* A few small tweaks

* Conflict resolution and other improvements

* Fix tests

* Add revert changes

* Deal with duplicate projects per workspace

* Some tweaks and accessibility improvements

* Tooltip accessibility

* Fix API endpoint

* Fix tests

* Remove jest dep from insomnia-importers
2019-04-17 17:50:03 -07:00

380 lines
10 KiB
JavaScript

import HKDF from 'hkdf';
import srp from 'srp-js';
import forge from 'node-forge';
const DEFAULT_BYTE_LENGTH = 32;
const DEFAULT_PBKDF2_ITERATIONS = 1e5; // 100,000
/**
* Generate hex signing key used for AES encryption
*
* @param pass
* @param email
* @param salt
*/
export async function deriveKey(pass, email, salt) {
const combinedSalt = await _hkdfSalt(salt, email);
return _pbkdf2Passphrase(pass, combinedSalt);
}
/**
* Encrypt with RSA256 public key
*
* @param publicKeyJWK
* @param plaintext
* @return String
*/
export function encryptRSAWithJWK(publicKeyJWK, plaintext) {
if (publicKeyJWK.alg !== 'RSA-OAEP-256') {
throw new Error('Public key algorithm was not RSA-OAEP-256');
} else if (publicKeyJWK.kty !== 'RSA') {
throw new Error('Public key type was not RSA');
} else if (!publicKeyJWK.key_ops.find(o => o === 'encrypt')) {
throw new Error('Public key does not have "encrypt" op');
}
const encodedPlaintext = encodeURIComponent(plaintext);
const n = _b64UrlToBigInt(publicKeyJWK.n);
const e = _b64UrlToBigInt(publicKeyJWK.e);
const publicKey = forge.rsa.setPublicKey(n, e);
const encrypted = publicKey.encrypt(encodedPlaintext, 'RSA-OAEP', {
md: forge.md.sha256.create(),
});
return forge.util.bytesToHex(encrypted);
}
export function decryptRSAWithJWK(privateJWK, encryptedBlob) {
const n = _b64UrlToBigInt(privateJWK.n);
const e = _b64UrlToBigInt(privateJWK.e);
const d = _b64UrlToBigInt(privateJWK.d);
const p = _b64UrlToBigInt(privateJWK.p);
const q = _b64UrlToBigInt(privateJWK.q);
const dP = _b64UrlToBigInt(privateJWK.dp);
const dQ = _b64UrlToBigInt(privateJWK.dq);
const qInv = _b64UrlToBigInt(privateJWK.qi);
const privateKey = forge.rsa.setPrivateKey(n, e, d, p, q, dP, dQ, qInv);
const bytes = forge.util.hexToBytes(encryptedBlob);
const decrypted = privateKey.decrypt(bytes, 'RSA-OAEP', {
md: forge.md.sha256.create(),
});
return decodeURIComponent(decrypted);
}
/**
* Encrypt data using symmetric key
*
* @param jwkOrKey JWK or string representing symmetric key
* @param buff data to encrypt
* @param additionalData any additional public data to attach
* @returns {{iv, t, d, ad}}
*/
export function encryptAESBuffer(jwkOrKey, buff, additionalData = '') {
// TODO: Add assertion checks for JWK
const rawKey = typeof jwkOrKey === 'string' ? jwkOrKey : _b64UrlToHex(jwkOrKey.k);
const key = forge.util.hexToBytes(rawKey);
const iv = forge.random.getBytesSync(12);
const cipher = forge.cipher.createCipher('AES-GCM', key);
cipher.start({ additionalData, iv, tagLength: 128 });
cipher.update(forge.util.createBuffer(buff));
cipher.finish();
return {
iv: forge.util.bytesToHex(iv),
t: forge.util.bytesToHex(cipher.mode.tag),
ad: forge.util.bytesToHex(additionalData),
d: forge.util.bytesToHex(cipher.output),
};
}
/**
* Encrypt data using symmetric key
*
* @param jwkOrKey JWK or string representing symmetric key
* @param plaintext string of data to encrypt
* @param additionalData any additional public data to attach
* @returns {{iv, t, d, ad}}
*/
export function encryptAES(jwkOrKey, plaintext, additionalData = '') {
// TODO: Add assertion checks for JWK
const rawKey = typeof jwkOrKey === 'string' ? jwkOrKey : _b64UrlToHex(jwkOrKey.k);
const key = forge.util.hexToBytes(rawKey);
const iv = forge.random.getBytesSync(12);
const cipher = forge.cipher.createCipher('AES-GCM', key);
// Plaintext could contain weird unicode, so we have to encode that
const encodedPlaintext = encodeURIComponent(plaintext);
cipher.start({ additionalData, iv, tagLength: 128 });
cipher.update(forge.util.createBuffer(encodedPlaintext));
cipher.finish();
return {
iv: forge.util.bytesToHex(iv),
t: forge.util.bytesToHex(cipher.mode.tag),
ad: forge.util.bytesToHex(additionalData),
d: forge.util.bytesToHex(cipher.output),
};
}
/**
* Decrypt AES using a key
*
* @param jwkOrKey JWK or string representing symmetric key
* @param encryptedResult encryption data
* @returns String
*/
export function decryptAES(jwkOrKey, encryptedResult) {
// TODO: Add assertion checks for JWK
const rawKey = typeof jwkOrKey === 'string' ? jwkOrKey : _b64UrlToHex(jwkOrKey.k);
const key = forge.util.hexToBytes(rawKey);
// ~~~~~~~~~~~~~~~~~~~~ //
// Decrypt with AES-GCM //
// ~~~~~~~~~~~~~~~~~~~~ //
const decipher = forge.cipher.createDecipher('AES-GCM', key);
decipher.start({
iv: forge.util.hexToBytes(encryptedResult.iv),
tagLength: encryptedResult.t.length * 4,
tag: forge.util.hexToBytes(encryptedResult.t),
additionalData: forge.util.hexToBytes(encryptedResult.ad),
});
decipher.update(forge.util.createBuffer(forge.util.hexToBytes(encryptedResult.d)));
if (decipher.finish()) {
return decodeURIComponent(decipher.output.toString());
} else {
throw new Error('Failed to decrypt data');
}
}
/**
* Decrypts AES using a key to buffer
* @param jwkOrKey
* @param encryptedResult
* @returns {string}
*/
export function decryptAESToBuffer(jwkOrKey, encryptedResult) {
// TODO: Add assertion checks for JWK
const rawKey = typeof jwkOrKey === 'string' ? jwkOrKey : _b64UrlToHex(jwkOrKey.k);
const key = forge.util.hexToBytes(rawKey);
// ~~~~~~~~~~~~~~~~~~~~ //
// Decrypt with AES-GCM //
// ~~~~~~~~~~~~~~~~~~~~ //
const decipher = forge.cipher.createDecipher('AES-GCM', key);
decipher.start({
iv: forge.util.hexToBytes(encryptedResult.iv),
tagLength: encryptedResult.t.length * 4,
tag: forge.util.hexToBytes(encryptedResult.t),
additionalData: forge.util.hexToBytes(encryptedResult.ad),
});
decipher.update(forge.util.createBuffer(forge.util.hexToBytes(encryptedResult.d)));
if (decipher.finish()) {
return Buffer.from(forge.util.bytesToHex(decipher.output), 'hex');
} else {
throw new Error('Failed to decrypt data');
}
}
/**
* Generate a random key
*
* @returns {Promise}
*/
export function srpGenKey() {
return new Promise((resolve, reject) => {
srp.genKey((err, secret1Buffer) => {
if (err) {
reject(err);
} else {
resolve(secret1Buffer.toString('hex'));
}
});
});
}
/**
* Generate a random AES256 key for use with symmetric encryption
*/
export async function generateAES256Key() {
const c = window.crypto;
const subtle = c ? c.subtle || c.webkitSubtle : null;
if (subtle) {
console.log('[crypt] Using Native AES Key Generation');
const key = await subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, [
'encrypt',
'decrypt',
]);
return subtle.exportKey('jwk', key);
} else {
console.log('[crypt] Using Fallback Forge AES Key Generation');
const key = forge.util.bytesToHex(forge.random.getBytesSync(32));
return {
kty: 'oct',
alg: 'A256GCM',
ext: true,
key_ops: ['encrypt', 'decrypt'],
k: _hexToB64Url(key),
};
}
}
/**
* Generate RSA keypair JWK with 2048 bits and exponent 0x10001
*
* @returns Object
*/
export async function generateKeyPairJWK() {
// NOTE: Safari has crypto.webkitSubtle, but it does not support RSA-OAEP-SHA256
const subtle = window.crypto && window.crypto.subtle;
if (subtle) {
console.log('[crypt] Using Native RSA Generation');
const pair = await subtle.generateKey(
{
name: 'RSA-OAEP',
publicExponent: new Uint8Array([1, 0, 1]),
modulusLength: 2048,
hash: 'SHA-256',
},
true,
['encrypt', 'decrypt'],
);
return {
publicKey: await subtle.exportKey('jwk', pair.publicKey),
privateKey: await subtle.exportKey('jwk', pair.privateKey),
};
} else {
console.log('[crypt] Using Forge RSA Generation');
const pair = forge.pki.rsa.generateKeyPair({ bits: 2048, e: 0x10001 });
const privateKey = {
alg: 'RSA-OAEP-256',
kty: 'RSA',
key_ops: ['decrypt'],
ext: true,
d: _bigIntToB64Url(pair.privateKey.d),
dp: _bigIntToB64Url(pair.privateKey.dP),
dq: _bigIntToB64Url(pair.privateKey.dQ),
e: _bigIntToB64Url(pair.privateKey.e),
n: _bigIntToB64Url(pair.privateKey.n),
p: _bigIntToB64Url(pair.privateKey.p),
q: _bigIntToB64Url(pair.privateKey.q),
qi: _bigIntToB64Url(pair.privateKey.qInv),
};
const publicKey = {
alg: 'RSA-OAEP-256',
kty: 'RSA',
key_ops: ['encrypt'],
e: _bigIntToB64Url(pair.publicKey.e),
n: _bigIntToB64Url(pair.publicKey.n),
};
return { privateKey, publicKey };
}
}
// ~~~~~~~~~~~~~~~~ //
// Helper Functions //
// ~~~~~~~~~~~~~~~~ //
/**
* Combine email and raw salt into usable salt
*
* @param rawSalt
* @param rawEmail
* @returns {Promise}
*/
async function _hkdfSalt(rawSalt, rawEmail) {
return new Promise(resolve => {
const hkdf = new HKDF('sha256', rawSalt, rawEmail);
hkdf.derive('', DEFAULT_BYTE_LENGTH, buffer => resolve(buffer.toString('hex')));
});
}
/**
* Convert a JSBN BigInteger to a URL-safe version of base64 encoding. This
* should only be used for encoding JWKs
*
* @param n BigInteger
* @returns {string}
*/
function _bigIntToB64Url(n) {
return _hexToB64Url(n.toString(16));
}
function _hexToB64Url(h) {
const bytes = forge.util.hexToBytes(h);
return window
.btoa(bytes)
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
}
function _b64UrlToBigInt(s) {
return new forge.jsbn.BigInteger(_b64UrlToHex(s), 16);
}
function _b64UrlToHex(s) {
const b64 = s.replace(/-/g, '+').replace(/_/g, '/');
return forge.util.bytesToHex(window.atob(b64));
}
/**
* Derive key from password
*
* @param passphrase
* @param salt hex representation of salt
*/
async function _pbkdf2Passphrase(passphrase, salt) {
if (window.crypto && window.crypto.subtle) {
console.log('[crypt] Using native PBKDF2');
const k = await window.crypto.subtle.importKey(
'raw',
Buffer.from(passphrase, 'utf8'),
{ name: 'PBKDF2' },
false,
['deriveBits'],
);
const algo = {
name: 'PBKDF2',
salt: Buffer.from(salt, 'hex'),
iterations: DEFAULT_PBKDF2_ITERATIONS,
hash: 'SHA-256',
};
const derivedKeyRaw = await window.crypto.subtle.deriveBits(algo, k, DEFAULT_BYTE_LENGTH * 8);
return Buffer.from(derivedKeyRaw).toString('hex');
} else {
console.log('[crypt] Using Forge PBKDF2');
const derivedKeyRaw = forge.pkcs5.pbkdf2(
passphrase,
forge.util.hexToBytes(salt),
DEFAULT_PBKDF2_ITERATIONS,
DEFAULT_BYTE_LENGTH,
forge.md.sha256.create(),
);
return forge.util.bytesToHex(derivedKeyRaw);
}
}