1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-12-24 16:49:26 +01:00

fast decrypt

This commit is contained in:
Kyle Spearrin 2018-05-07 09:00:49 -04:00
parent c6c5dd6d46
commit de4494e1b3
9 changed files with 234 additions and 87 deletions

View File

@ -1,6 +1,7 @@
import { NodeCryptoFunctionService } from '../../../src/services/nodeCryptoFunction.service';
import { Utils } from '../../../src/misc/utils';
import { SymmetricCryptoKey } from '../../../src/models/domain/symmetricCryptoKey';
const RsaPublicKey = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl0Vawl/toXzkEvB82FEtqHP' +
'4xlU2ab/v0crqIfXfIoWF/XXdHGIdrZeilnRXPPJT1B9dTsasttEZNnua/0Rek/cjNDHtzT52irfoZYS7X6HNIfOi54Q+egP' +
@ -92,19 +93,20 @@ describe('NodeCrypto Function Service', () => {
const value = 'EncryptMe!';
const data = Utils.fromUtf8ToArray(value);
const encValue = await nodeCryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer);
const decValue = await nodeCryptoFunctionService.aesDecryptSmall(encValue, iv.buffer, key.buffer);
const decValue = await nodeCryptoFunctionService.aesDecryptLarge(encValue, iv.buffer, key.buffer);
expect(Utils.fromBufferToUtf8(decValue)).toBe(value);
});
});
describe('aesDecryptSmall', () => {
describe('aesDecryptFast', () => {
it('should successfully decrypt data', async () => {
const nodeCryptoFunctionService = new NodeCryptoFunctionService();
const iv = makeStaticByteArray(16);
const key = makeStaticByteArray(32);
const data = Utils.fromB64ToArray('ByUF8vhyX4ddU9gcooznwA==');
const decValue = await nodeCryptoFunctionService.aesDecryptSmall(data.buffer, iv.buffer, key.buffer);
expect(Utils.fromBufferToUtf8(decValue)).toBe('EncryptMe!');
const iv = Utils.fromBufferToB64(makeStaticByteArray(16).buffer);
const symKey = new SymmetricCryptoKey(makeStaticByteArray(32).buffer);
const data = 'ByUF8vhyX4ddU9gcooznwA==';
const params = nodeCryptoFunctionService.aesDecryptFastParameters(data, iv, null, symKey);
const decValue = await nodeCryptoFunctionService.aesDecryptFast(params);
expect(decValue).toBe('EncryptMe!');
});
});

View File

@ -7,6 +7,7 @@ import { PlatformUtilsService } from '../../../src/abstractions/platformUtils.se
import { WebCryptoFunctionService } from '../../../src/services/webCryptoFunction.service';
import { Utils } from '../../../src/misc/utils';
import { SymmetricCryptoKey } from '../../../src/models/domain';
const RsaPublicKey = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl0Vawl/toXzkEvB82FEtqHP' +
'4xlU2ab/v0crqIfXfIoWF/XXdHGIdrZeilnRXPPJT1B9dTsasttEZNnua/0Rek/cjNDHtzT52irfoZYS7X6HNIfOi54Q+egP' +
@ -109,8 +110,12 @@ describe('WebCrypto Function Service', () => {
const value = 'EncryptMe!';
const data = Utils.fromUtf8ToArray(value);
const encValue = await webCryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer);
const decValue = await webCryptoFunctionService.aesDecryptSmall(encValue, iv.buffer, key.buffer);
expect(Utils.fromBufferToUtf8(decValue)).toBe(value);
const encData = Utils.fromBufferToB64(encValue);
const b64Iv = Utils.fromBufferToB64(iv.buffer);
const symKey = new SymmetricCryptoKey(key.buffer);
const params = webCryptoFunctionService.aesDecryptFastParameters(encData, b64Iv, null, symKey);
const decValue = await webCryptoFunctionService.aesDecryptFast(params);
expect(decValue).toBe(value);
});
it('should successfully encrypt and then decrypt large data', async () => {
@ -125,14 +130,15 @@ describe('WebCrypto Function Service', () => {
});
});
describe('aesDecryptSmall', () => {
describe('aesDecryptFast', () => {
it('should successfully decrypt data', async () => {
const webCryptoFunctionService = getWebCryptoFunctionService();
const iv = makeStaticByteArray(16);
const key = makeStaticByteArray(32);
const data = Utils.fromB64ToArray('ByUF8vhyX4ddU9gcooznwA==');
const decValue = await webCryptoFunctionService.aesDecryptSmall(data.buffer, iv.buffer, key.buffer);
expect(Utils.fromBufferToUtf8(decValue)).toBe('EncryptMe!');
const iv = Utils.fromBufferToB64(makeStaticByteArray(16).buffer);
const symKey = new SymmetricCryptoKey(makeStaticByteArray(32).buffer);
const data = 'ByUF8vhyX4ddU9gcooznwA==';
const params = webCryptoFunctionService.aesDecryptFastParameters(data, iv, null, symKey);
const decValue = await webCryptoFunctionService.aesDecryptFast(params);
expect(decValue).toBe('EncryptMe!');
});
});

View File

@ -27,7 +27,6 @@ export abstract class CryptoService {
makeEncKey: (key: SymmetricCryptoKey) => Promise<CipherString>;
encrypt: (plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey) => Promise<CipherString>;
encryptToBytes: (plainValue: ArrayBuffer, key?: SymmetricCryptoKey) => Promise<ArrayBuffer>;
decrypt: (cipherString: CipherString, key?: SymmetricCryptoKey) => Promise<ArrayBuffer>;
decryptToUtf8: (cipherString: CipherString, key?: SymmetricCryptoKey) => Promise<string>;
decryptFromBytes: (encBuf: ArrayBuffer, key: SymmetricCryptoKey) => Promise<ArrayBuffer>;
randomNumber: (min: number, max: number) => Promise<number>;

View File

@ -1,10 +1,19 @@
import { DecryptParameters } from '../models/domain/decryptParameters';
import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey';
export abstract class CryptoFunctionService {
pbkdf2: (password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512',
iterations: number) => Promise<ArrayBuffer>;
hash: (value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512') => Promise<ArrayBuffer>;
hmac: (value: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512') => Promise<ArrayBuffer>;
timeSafeEqual: (a: ArrayBuffer, b: ArrayBuffer) => Promise<boolean>;
hmacFast: (value: ArrayBuffer | string, key: ArrayBuffer | string, algorithm: 'sha1' | 'sha256' | 'sha512') =>
Promise<ArrayBuffer | string>;
timeSafeEqualFast: (a: ArrayBuffer | string, b: ArrayBuffer | string) => Promise<boolean>;
aesEncrypt: (data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer) => Promise<ArrayBuffer>;
aesDecryptSmall: (data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer) => Promise<ArrayBuffer>;
aesDecryptFastParameters: (data: string, iv: string, mac: string, key: SymmetricCryptoKey) =>
DecryptParameters<ArrayBuffer | string>;
aesDecryptFast: (parameters: DecryptParameters<ArrayBuffer | string>) => Promise<string>;
aesDecryptLarge: (data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer) => Promise<ArrayBuffer>;
rsaEncrypt: (data: ArrayBuffer, publicKey: ArrayBuffer, algorithm: 'sha1' | 'sha256') => Promise<ArrayBuffer>;
rsaDecrypt: (data: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256') => Promise<ArrayBuffer>;

View File

@ -0,0 +1,8 @@
export class DecryptParameters<T> {
encKey: T;
data: T;
iv: T;
macKey: T;
mac: T;
macData: T;
}

View File

@ -9,6 +9,8 @@ export class SymmetricCryptoKey {
encType: EncryptionType;
keyB64: string;
encKeyB64: string;
macKeyB64: string;
constructor(key: ArrayBuffer, encType?: EncryptionType) {
if (key == null) {
@ -26,7 +28,6 @@ export class SymmetricCryptoKey {
}
this.key = key;
this.keyB64 = Utils.fromBufferToB64(key);
this.encType = encType;
if (encType === EncryptionType.AesCbc256_B64 && key.byteLength === 32) {
@ -41,5 +42,15 @@ export class SymmetricCryptoKey {
} else {
throw new Error('Unsupported encType/key length.');
}
if (this.key != null) {
this.keyB64 = Utils.fromBufferToB64(this.key);
}
if (this.encKey != null) {
this.encKeyB64 = Utils.fromBufferToB64(this.encKey);
}
if (this.macKey != null) {
this.macKeyB64 = Utils.fromBufferToB64(this.macKey);
}
}
}

View File

@ -304,7 +304,7 @@ export class CryptoService implements CryptoServiceAbstraction {
const iv = Utils.fromB64ToArray(cipherString.initializationVector).buffer;
const ct = Utils.fromB64ToArray(cipherString.cipherText).buffer;
const mac = cipherString.mac ? Utils.fromB64ToArray(cipherString.mac).buffer : null;
const decipher = await this.aesDecrypt(cipherString.encryptionType, ct, iv, mac, key);
const decipher = await this.aesDecryptToBytes(cipherString.encryptionType, ct, iv, mac, key);
if (decipher == null) {
return null;
}
@ -313,8 +313,8 @@ export class CryptoService implements CryptoServiceAbstraction {
}
async decryptToUtf8(cipherString: CipherString, key?: SymmetricCryptoKey): Promise<string> {
const decipher = await this.decrypt(cipherString, key);
return Utils.fromBufferToUtf8(decipher);
return await this.aesDecryptToUtf8(cipherString.encryptionType, cipherString.cipherText,
cipherString.initializationVector, cipherString.mac, key);
}
async decryptFromBytes(encBuf: ArrayBuffer, key: SymmetricCryptoKey): Promise<ArrayBuffer> {
@ -351,7 +351,7 @@ export class CryptoService implements CryptoServiceAbstraction {
return null;
}
return await this.aesDecryptLarge(encType, ctBytes.buffer, ivBytes.buffer,
return await this.aesDecryptToBytes(encType, ctBytes.buffer, ivBytes.buffer,
macBytes != null ? macBytes.buffer : null, key);
}
@ -409,8 +409,8 @@ export class CryptoService implements CryptoServiceAbstraction {
return obj;
}
private async aesDecrypt(encType: EncryptionType, ct: ArrayBuffer, iv: ArrayBuffer, mac: ArrayBuffer,
key: SymmetricCryptoKey): Promise<ArrayBuffer> {
private async aesDecryptToUtf8(encType: EncryptionType, ct: string, iv: string, mac: string,
key: SymmetricCryptoKey): Promise<string> {
const keyForEnc = await this.getKeyForEncryption(key);
const theKey = this.resolveLegacyKey(encType, keyForEnc);
@ -420,51 +420,57 @@ export class CryptoService implements CryptoServiceAbstraction {
return null;
}
if (encType !== theKey.encType) {
if (theKey.encType !== encType) {
// tslint:disable-next-line
console.error('encType unavailable.');
return null;
}
const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(ct, iv, mac, theKey);
if (fastParams.macKey != null && fastParams.mac != null) {
const computedMac = await this.cryptoFunctionService.hmacFast(fastParams.macData,
fastParams.macKey, 'sha256');
const macsEqual = await this.cryptoFunctionService.timeSafeEqualFast(fastParams.mac, computedMac);
if (!macsEqual) {
// tslint:disable-next-line
console.error('mac failed.');
return null;
}
}
return this.cryptoFunctionService.aesDecryptFast(fastParams);
}
private async aesDecryptToBytes(encType: EncryptionType, ct: ArrayBuffer, iv: ArrayBuffer,
mac: ArrayBuffer, key: SymmetricCryptoKey): Promise<ArrayBuffer> {
const keyForEnc = await this.getKeyForEncryption(key);
const theKey = this.resolveLegacyKey(encType, keyForEnc);
if (theKey.macKey != null && mac == null) {
return null;
}
if (theKey.encType !== encType) {
return null;
}
if (theKey.macKey != null && mac != null) {
const macData = new Uint8Array(iv.byteLength + ct.byteLength);
macData.set(new Uint8Array(iv), 0);
macData.set(new Uint8Array(ct), iv.byteLength);
const computedMac = await this.cryptoFunctionService.hmac(new Uint8Array(iv).buffer,
theKey.macKey, 'sha256');
if (!this.macsEqual(computedMac, mac)) {
const computedMac = await this.cryptoFunctionService.hmac(macData.buffer, theKey.macKey, 'sha256');
if (computedMac === null) {
return null;
}
const macsMatch = await this.cryptoFunctionService.timeSafeEqual(mac, computedMac);
if (!macsMatch) {
// tslint:disable-next-line
console.error('mac failed.');
return null;
}
}
return this.cryptoFunctionService.aesDecryptSmall(ct, iv, theKey.encKey);
}
private async aesDecryptLarge(encType: EncryptionType, ct: ArrayBuffer, iv: ArrayBuffer,
mac: ArrayBuffer, key: SymmetricCryptoKey): Promise<ArrayBuffer> {
const theKey = await this.getKeyForEncryption(key);
if (theKey.macKey == null || mac == null) {
return null;
}
const macData = new Uint8Array(iv.byteLength + ct.byteLength);
macData.set(new Uint8Array(iv), 0);
macData.set(new Uint8Array(ct), iv.byteLength);
const computedMac = await this.cryptoFunctionService.hmac(new Uint8Array(iv).buffer,
theKey.macKey, 'sha256');
if (computedMac === null) {
return null;
}
const macsMatch = await this.macsEqual(mac, computedMac);
if (macsMatch === false) {
// tslint:disable-next-line
console.error('mac failed.');
return null;
}
return await this.cryptoFunctionService.aesDecryptLarge(ct, iv, theKey.encKey);
}
@ -509,7 +515,7 @@ export class CryptoService implements CryptoServiceAbstraction {
if (key != null && key.macKey != null && encPieces.length > 1) {
const mac = Utils.fromB64ToArray(encPieces[1]).buffer;
const computedMac = await this.cryptoFunctionService.hmac(ct, key.macKey, 'sha256');
const macsEqual = await this.macsEqual(mac, computedMac);
const macsEqual = await this.cryptoFunctionService.timeSafeEqual(mac, computedMac);
if (!macsEqual) {
throw new Error('MAC failed.');
}
@ -536,28 +542,6 @@ export class CryptoService implements CryptoServiceAbstraction {
return this.cryptoFunctionService.rsaDecrypt(ct, privateKey, alg);
}
// Safely compare two MACs in a way that protects against timing attacks (Double HMAC Verification).
// ref: https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/
// ref: https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy
private async macsEqual(mac1: ArrayBuffer, mac2: ArrayBuffer): Promise<boolean> {
const key = await this.cryptoFunctionService.randomBytes(32);
const newMac1 = await this.cryptoFunctionService.hmac(mac1, key, 'sha256');
const newMac2 = await this.cryptoFunctionService.hmac(mac2, key, 'sha256');
if (newMac1.byteLength !== newMac2.byteLength) {
return false;
}
const arr1 = new Uint8Array(newMac1);
const arr2 = new Uint8Array(newMac2);
for (let i = 0; i < arr2.length; i++) {
if (arr1[i] !== arr2[i]) {
return false;
}
}
return true;
}
private async getKeyForEncryption(key?: SymmetricCryptoKey): Promise<SymmetricCryptoKey> {
if (key != null) {
return key;

View File

@ -6,6 +6,9 @@ import { CryptoFunctionService } from '../abstractions/cryptoFunction.service';
import { Utils } from '../misc/utils';
import { DecryptParameters } from '../models/domain/decryptParameters';
import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey';
export class NodeCryptoFunctionService implements CryptoFunctionService {
pbkdf2(password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512',
iterations: number): Promise<ArrayBuffer> {
@ -38,6 +41,33 @@ export class NodeCryptoFunctionService implements CryptoFunctionService {
return Promise.resolve(this.toArrayBuffer(hmac.digest()));
}
async timeSafeEqual(a: ArrayBuffer, b: ArrayBuffer): Promise<boolean> {
const key = await this.randomBytes(32);
const mac1 = await this.hmac(a, key, 'sha256');
const mac2 = await this.hmac(b, key, 'sha256');
if (mac1.byteLength !== mac2.byteLength) {
return false;
}
const arr1 = new Uint8Array(mac1);
const arr2 = new Uint8Array(mac2);
for (let i = 0; i < arr2.length; i++) {
if (arr1[i] !== arr2[i]) {
return false;
}
}
return true;
}
hmacFast(value: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise<ArrayBuffer> {
return this.hmac(value, key, algorithm);
}
timeSafeEqualFast(a: ArrayBuffer, b: ArrayBuffer): Promise<boolean> {
return this.timeSafeEqual(a, b);
}
aesEncrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer> {
const nodeData = this.toNodeBuffer(data);
const nodeIv = this.toNodeBuffer(iv);
@ -47,8 +77,31 @@ export class NodeCryptoFunctionService implements CryptoFunctionService {
return Promise.resolve(this.toArrayBuffer(encBuf));
}
aesDecryptSmall(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer> {
return this.aesDecryptLarge(data, iv, key);
aesDecryptFastParameters(data: string, iv: string, mac: string, key: SymmetricCryptoKey):
DecryptParameters<ArrayBuffer> {
const p = new DecryptParameters<ArrayBuffer>();
p.encKey = key.encKey;
p.data = Utils.fromB64ToArray(data).buffer;
p.iv = Utils.fromB64ToArray(iv).buffer;
const macData = new Uint8Array(p.iv.byteLength + p.data.byteLength);
macData.set(new Uint8Array(p.iv), 0);
macData.set(new Uint8Array(p.data), p.iv.byteLength);
p.macData = macData.buffer;
if (key.macKey != null) {
p.macKey = key.macKey;
}
if (mac != null) {
p.mac = Utils.fromB64ToArray(mac).buffer;
}
return p;
}
async aesDecryptFast(parameters: DecryptParameters<ArrayBuffer>): Promise<string> {
const decBuf = await this.aesDecryptLarge(parameters.data, parameters.iv, parameters.encKey);
return Utils.fromBufferToUtf8(decBuf);
}
aesDecryptLarge(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer> {

View File

@ -5,6 +5,9 @@ import { PlatformUtilsService } from '../abstractions/platformUtils.service';
import { Utils } from '../misc/utils';
import { SymmetricCryptoKey } from '../models/domain';
import { DecryptParameters } from '../models/domain/decryptParameters';
export class WebCryptoFunctionService implements CryptoFunctionService {
private crypto: Crypto;
private subtle: SubtleCrypto;
@ -80,21 +83,93 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
return await this.subtle.sign(signingAlgorithm, impKey, value);
}
// Safely compare two values in a way that protects against timing attacks (Double HMAC Verification).
// ref: https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/
// ref: https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy
async timeSafeEqual(a: ArrayBuffer, b: ArrayBuffer): Promise<boolean> {
const macKey = await this.randomBytes(32);
const signingAlgorithm = {
name: 'HMAC',
hash: { name: 'SHA-256' },
};
const impKey = await this.subtle.importKey('raw', macKey, signingAlgorithm, false, ['sign']);
const mac1 = await this.subtle.sign(signingAlgorithm, impKey, a);
const mac2 = await this.subtle.sign(signingAlgorithm, impKey, b);
if (mac1.byteLength !== mac2.byteLength) {
return false;
}
const arr1 = new Uint8Array(mac1);
const arr2 = new Uint8Array(mac2);
for (let i = 0; i < arr2.length; i++) {
if (arr1[i] !== arr2[i]) {
return false;
}
}
return true;
}
hmacFast(value: string, key: string, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise<string> {
const hmac = (forge as any).hmac.create();
hmac.start(algorithm, key);
hmac.update(value);
const bytes = hmac.digest().getBytes();
return Promise.resolve(bytes);
}
async timeSafeEqualFast(a: string, b: string): Promise<boolean> {
const rand = await this.randomBytes(32);
const bytes = new Uint32Array(rand);
const buffer = forge.util.createBuffer();
for (let i = 0; i < bytes.length; i++) {
buffer.putInt32(bytes[i]);
}
const macKey = buffer.getBytes();
const hmac = (forge as any).hmac.create();
hmac.start('sha256', macKey);
hmac.update(a);
const mac1 = hmac.digest().getBytes();
hmac.start(null, null);
hmac.update(b);
const mac2 = hmac.digest().getBytes();
const equals = mac1 === mac2;
return equals;
}
async aesEncrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer> {
const impKey = await this.subtle.importKey('raw', key, { name: 'AES-CBC' }, false, ['encrypt']);
return await this.subtle.encrypt({ name: 'AES-CBC', iv: iv }, impKey, data);
}
async aesDecryptSmall(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer> {
const dataBytes = this.toByteString(data);
const ivBytes = this.toByteString(iv);
const keyBytes = this.toByteString(key);
const dataBuffer = (forge as any).util.createBuffer(dataBytes);
const decipher = (forge as any).cipher.createDecipher('AES-CBC', keyBytes);
decipher.start({ iv: ivBytes });
aesDecryptFastParameters(data: string, iv: string, mac: string, key: SymmetricCryptoKey):
DecryptParameters<string> {
const p = new DecryptParameters<string>();
p.encKey = forge.util.decode64(key.encKeyB64);
p.data = forge.util.decode64(data);
p.iv = forge.util.decode64(iv);
p.macData = p.iv + p.data;
if (key.macKeyB64 != null) {
p.macKey = forge.util.decode64(key.macKeyB64);
}
if (mac != null) {
p.mac = forge.util.decode64(mac);
}
return p;
}
aesDecryptFast(parameters: DecryptParameters<string>): Promise<string> {
const dataBuffer = (forge as any).util.createBuffer(parameters.data);
const decipher = (forge as any).cipher.createDecipher('AES-CBC', parameters.encKey);
decipher.start({ iv: parameters.iv });
decipher.update(dataBuffer);
decipher.finish();
return Utils.fromByteStringToArray(decipher.output.getBytes()).buffer;
const val = decipher.output.toString('utf8');
return Promise.resolve(val);
}
async aesDecryptLarge(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer> {