2018-02-19 19:07:19 +01:00
|
|
|
import { EncryptionType } from '../enums/encryptionType';
|
2018-01-08 20:11:28 +01:00
|
|
|
|
2018-02-19 19:07:19 +01:00
|
|
|
import { CipherString } from '../models/domain/cipherString';
|
|
|
|
import { EncryptedObject } from '../models/domain/encryptedObject';
|
|
|
|
import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey';
|
|
|
|
import { ProfileOrganizationResponse } from '../models/response/profileOrganizationResponse';
|
2018-01-08 20:11:28 +01:00
|
|
|
|
2018-02-19 19:07:19 +01:00
|
|
|
import { CryptoService as CryptoServiceAbstraction } from '../abstractions/crypto.service';
|
2018-04-22 05:14:04 +02:00
|
|
|
import { CryptoFunctionService } from '../abstractions/cryptoFunction.service';
|
|
|
|
import { StorageService } from '../abstractions/storage.service';
|
2018-01-08 20:11:28 +01:00
|
|
|
|
|
|
|
import { ConstantsService } from './constants.service';
|
2018-04-22 05:14:04 +02:00
|
|
|
|
|
|
|
import { Utils } from '../misc/utils';
|
2018-01-08 20:11:28 +01:00
|
|
|
|
|
|
|
const Keys = {
|
|
|
|
key: 'key',
|
|
|
|
encOrgKeys: 'encOrgKeys',
|
|
|
|
encPrivateKey: 'encPrivateKey',
|
|
|
|
encKey: 'encKey',
|
|
|
|
keyHash: 'keyHash',
|
|
|
|
};
|
|
|
|
|
2018-01-25 20:26:09 +01:00
|
|
|
export class CryptoService implements CryptoServiceAbstraction {
|
2018-01-08 20:11:28 +01:00
|
|
|
private key: SymmetricCryptoKey;
|
|
|
|
private encKey: SymmetricCryptoKey;
|
|
|
|
private legacyEtmKey: SymmetricCryptoKey;
|
|
|
|
private keyHash: string;
|
2018-07-02 23:09:45 +02:00
|
|
|
private publicKey: ArrayBuffer;
|
2018-01-08 20:11:28 +01:00
|
|
|
private privateKey: ArrayBuffer;
|
|
|
|
private orgKeys: Map<string, SymmetricCryptoKey>;
|
|
|
|
|
2018-04-22 05:14:04 +02:00
|
|
|
constructor(private storageService: StorageService, private secureStorageService: StorageService,
|
|
|
|
private cryptoFunctionService: CryptoFunctionService) { }
|
2018-01-08 20:11:28 +01:00
|
|
|
|
|
|
|
async setKey(key: SymmetricCryptoKey): Promise<any> {
|
|
|
|
this.key = key;
|
|
|
|
|
|
|
|
const option = await this.storageService.get<number>(ConstantsService.lockOptionKey);
|
|
|
|
if (option != null) {
|
|
|
|
// if we have a lock option set, we do not store the key
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.secureStorageService.save(Keys.key, key.keyB64);
|
|
|
|
}
|
|
|
|
|
|
|
|
setKeyHash(keyHash: string): Promise<{}> {
|
|
|
|
this.keyHash = keyHash;
|
|
|
|
return this.storageService.save(Keys.keyHash, keyHash);
|
|
|
|
}
|
|
|
|
|
|
|
|
async setEncKey(encKey: string): Promise<{}> {
|
|
|
|
if (encKey == null) {
|
|
|
|
return;
|
|
|
|
}
|
2018-04-22 05:14:04 +02:00
|
|
|
|
2018-01-08 20:11:28 +01:00
|
|
|
await this.storageService.save(Keys.encKey, encKey);
|
|
|
|
this.encKey = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
async setEncPrivateKey(encPrivateKey: string): Promise<{}> {
|
|
|
|
if (encPrivateKey == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
await this.storageService.save(Keys.encPrivateKey, encPrivateKey);
|
|
|
|
this.privateKey = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
setOrgKeys(orgs: ProfileOrganizationResponse[]): Promise<{}> {
|
|
|
|
const orgKeys: any = {};
|
|
|
|
orgs.forEach((org) => {
|
|
|
|
orgKeys[org.id] = org.key;
|
|
|
|
});
|
|
|
|
|
|
|
|
return this.storageService.save(Keys.encOrgKeys, orgKeys);
|
|
|
|
}
|
|
|
|
|
|
|
|
async getKey(): Promise<SymmetricCryptoKey> {
|
|
|
|
if (this.key != null) {
|
|
|
|
return this.key;
|
|
|
|
}
|
|
|
|
|
|
|
|
const option = await this.storageService.get<number>(ConstantsService.lockOptionKey);
|
|
|
|
if (option != null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const key = await this.secureStorageService.get<string>(Keys.key);
|
2018-04-22 05:14:04 +02:00
|
|
|
if (key != null) {
|
|
|
|
this.key = new SymmetricCryptoKey(Utils.fromB64ToArray(key).buffer);
|
2018-01-08 20:11:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return key == null ? null : this.key;
|
|
|
|
}
|
|
|
|
|
|
|
|
getKeyHash(): Promise<string> {
|
|
|
|
if (this.keyHash != null) {
|
|
|
|
return Promise.resolve(this.keyHash);
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.storageService.get<string>(Keys.keyHash);
|
|
|
|
}
|
|
|
|
|
|
|
|
async getEncKey(): Promise<SymmetricCryptoKey> {
|
|
|
|
if (this.encKey != null) {
|
|
|
|
return this.encKey;
|
|
|
|
}
|
|
|
|
|
|
|
|
const encKey = await this.storageService.get<string>(Keys.encKey);
|
|
|
|
if (encKey == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const key = await this.getKey();
|
|
|
|
if (key == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-06-13 22:48:00 +02:00
|
|
|
let decEncKey: ArrayBuffer;
|
|
|
|
const encKeyCipher = new CipherString(encKey);
|
|
|
|
if (encKeyCipher.encryptionType === EncryptionType.AesCbc256_B64) {
|
|
|
|
decEncKey = await this.decrypt(encKeyCipher, key);
|
|
|
|
} else if (encKeyCipher.encryptionType === EncryptionType.AesCbc256_HmacSha256_B64) {
|
|
|
|
const newKey = await this.stretchKey(key);
|
|
|
|
decEncKey = await this.decrypt(encKeyCipher, newKey);
|
|
|
|
} else {
|
|
|
|
throw new Error('Unsupported encKey type.');
|
|
|
|
}
|
|
|
|
|
2018-01-08 20:11:28 +01:00
|
|
|
if (decEncKey == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
this.encKey = new SymmetricCryptoKey(decEncKey);
|
|
|
|
return this.encKey;
|
|
|
|
}
|
|
|
|
|
2018-07-02 23:09:45 +02:00
|
|
|
async getPublicKey(): Promise<ArrayBuffer> {
|
|
|
|
if (this.publicKey != null) {
|
|
|
|
return this.publicKey;
|
|
|
|
}
|
|
|
|
|
|
|
|
const privateKey = await this.getPrivateKey();
|
|
|
|
if (privateKey == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-07-03 05:57:22 +02:00
|
|
|
this.publicKey = await this.cryptoFunctionService.rsaExtractPublicKey(privateKey);
|
2018-07-02 23:09:45 +02:00
|
|
|
return this.publicKey;
|
|
|
|
}
|
|
|
|
|
2018-01-08 20:11:28 +01:00
|
|
|
async getPrivateKey(): Promise<ArrayBuffer> {
|
|
|
|
if (this.privateKey != null) {
|
|
|
|
return this.privateKey;
|
|
|
|
}
|
|
|
|
|
|
|
|
const encPrivateKey = await this.storageService.get<string>(Keys.encPrivateKey);
|
|
|
|
if (encPrivateKey == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-04-22 05:14:04 +02:00
|
|
|
this.privateKey = await this.decrypt(new CipherString(encPrivateKey), null);
|
2018-01-08 20:11:28 +01:00
|
|
|
return this.privateKey;
|
|
|
|
}
|
|
|
|
|
|
|
|
async getOrgKeys(): Promise<Map<string, SymmetricCryptoKey>> {
|
|
|
|
if (this.orgKeys != null && this.orgKeys.size > 0) {
|
|
|
|
return this.orgKeys;
|
|
|
|
}
|
|
|
|
|
|
|
|
const encOrgKeys = await this.storageService.get<any>(Keys.encOrgKeys);
|
2018-04-22 05:14:04 +02:00
|
|
|
if (encOrgKeys == null) {
|
2018-01-08 20:11:28 +01:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const orgKeys: Map<string, SymmetricCryptoKey> = new Map<string, SymmetricCryptoKey>();
|
|
|
|
let setKey = false;
|
|
|
|
|
|
|
|
for (const orgId in encOrgKeys) {
|
|
|
|
if (!encOrgKeys.hasOwnProperty(orgId)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-04-22 05:14:04 +02:00
|
|
|
const decValue = await this.rsaDecrypt(encOrgKeys[orgId]);
|
|
|
|
orgKeys.set(orgId, new SymmetricCryptoKey(decValue));
|
2018-01-08 20:11:28 +01:00
|
|
|
setKey = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (setKey) {
|
|
|
|
this.orgKeys = orgKeys;
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.orgKeys;
|
|
|
|
}
|
|
|
|
|
|
|
|
async getOrgKey(orgId: string): Promise<SymmetricCryptoKey> {
|
|
|
|
if (orgId == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const orgKeys = await this.getOrgKeys();
|
|
|
|
if (orgKeys == null || !orgKeys.has(orgId)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return orgKeys.get(orgId);
|
|
|
|
}
|
|
|
|
|
2018-06-13 23:10:52 +02:00
|
|
|
async hasKey(): Promise<boolean> {
|
|
|
|
return (await this.getKey()) != null;
|
|
|
|
}
|
|
|
|
|
2018-01-08 20:11:28 +01:00
|
|
|
clearKey(): Promise<any> {
|
|
|
|
this.key = this.legacyEtmKey = null;
|
|
|
|
return this.secureStorageService.remove(Keys.key);
|
|
|
|
}
|
|
|
|
|
|
|
|
clearKeyHash(): Promise<any> {
|
|
|
|
this.keyHash = null;
|
|
|
|
return this.storageService.remove(Keys.keyHash);
|
|
|
|
}
|
|
|
|
|
|
|
|
clearEncKey(memoryOnly?: boolean): Promise<any> {
|
|
|
|
this.encKey = null;
|
|
|
|
if (memoryOnly) {
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
return this.storageService.remove(Keys.encKey);
|
|
|
|
}
|
|
|
|
|
2018-07-03 17:41:55 +02:00
|
|
|
clearKeyPair(memoryOnly?: boolean): Promise<any> {
|
2018-01-08 20:11:28 +01:00
|
|
|
this.privateKey = null;
|
2018-07-03 17:41:55 +02:00
|
|
|
this.publicKey = null;
|
2018-01-08 20:11:28 +01:00
|
|
|
if (memoryOnly) {
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
return this.storageService.remove(Keys.encPrivateKey);
|
|
|
|
}
|
|
|
|
|
|
|
|
clearOrgKeys(memoryOnly?: boolean): Promise<any> {
|
|
|
|
this.orgKeys = null;
|
|
|
|
if (memoryOnly) {
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
return this.storageService.remove(Keys.encOrgKeys);
|
|
|
|
}
|
|
|
|
|
|
|
|
clearKeys(): Promise<any> {
|
|
|
|
return Promise.all([
|
|
|
|
this.clearKey(),
|
|
|
|
this.clearKeyHash(),
|
|
|
|
this.clearOrgKeys(),
|
|
|
|
this.clearEncKey(),
|
2018-07-03 17:41:55 +02:00
|
|
|
this.clearKeyPair(),
|
2018-01-08 20:11:28 +01:00
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
async toggleKey(): Promise<any> {
|
|
|
|
const key = await this.getKey();
|
|
|
|
const option = await this.storageService.get(ConstantsService.lockOptionKey);
|
|
|
|
if (option != null || option === 0) {
|
|
|
|
// if we have a lock option set, clear the key
|
|
|
|
await this.clearKey();
|
|
|
|
this.key = key;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
await this.setKey(key);
|
|
|
|
}
|
|
|
|
|
2018-04-22 05:14:04 +02:00
|
|
|
async makeKey(password: string, salt: string): Promise<SymmetricCryptoKey> {
|
|
|
|
const key = await this.cryptoFunctionService.pbkdf2(password, salt, 'sha256', 5000);
|
|
|
|
return new SymmetricCryptoKey(key);
|
2018-01-08 20:11:28 +01:00
|
|
|
}
|
|
|
|
|
2018-07-02 23:09:45 +02:00
|
|
|
async makeShareKey(): Promise<[CipherString, SymmetricCryptoKey]> {
|
|
|
|
const shareKey = await this.cryptoFunctionService.randomBytes(64);
|
|
|
|
const publicKey = await this.getPublicKey();
|
|
|
|
const encKey = await this.getEncKey();
|
|
|
|
const encShareKey = await this.rsaEncrypt(shareKey, publicKey, encKey);
|
|
|
|
return [encShareKey, new SymmetricCryptoKey(shareKey)];
|
|
|
|
}
|
|
|
|
|
2018-07-03 17:41:55 +02:00
|
|
|
async makeKeyPair(key?: SymmetricCryptoKey): Promise<[string, CipherString]> {
|
|
|
|
const keyPair = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
|
|
|
|
const publicB64 = Utils.fromBufferToB64(keyPair[0]);
|
|
|
|
const privateEnc = await this.encrypt(keyPair[1], key);
|
|
|
|
return [publicB64, privateEnc];
|
|
|
|
}
|
|
|
|
|
2018-01-08 20:11:28 +01:00
|
|
|
async hashPassword(password: string, key: SymmetricCryptoKey): Promise<string> {
|
2018-05-16 03:11:20 +02:00
|
|
|
if (key == null) {
|
|
|
|
key = await this.getKey();
|
|
|
|
}
|
2018-04-22 05:14:04 +02:00
|
|
|
if (password == null || key == null) {
|
2018-01-08 20:11:28 +01:00
|
|
|
throw new Error('Invalid parameters.');
|
|
|
|
}
|
|
|
|
|
2018-04-22 05:14:04 +02:00
|
|
|
const hash = await this.cryptoFunctionService.pbkdf2(key.key, password, 'sha256', 1);
|
|
|
|
return Utils.fromBufferToB64(hash);
|
2018-01-08 20:11:28 +01:00
|
|
|
}
|
|
|
|
|
2018-07-03 17:41:55 +02:00
|
|
|
async makeEncKey(key: SymmetricCryptoKey): Promise<[SymmetricCryptoKey, CipherString]> {
|
2018-06-13 22:48:00 +02:00
|
|
|
const encKey = await this.cryptoFunctionService.randomBytes(64);
|
2018-07-03 17:41:55 +02:00
|
|
|
let encKeyEnc: CipherString = null;
|
2018-06-13 23:06:09 +02:00
|
|
|
// TODO: Uncomment when we're ready to enable key stretching
|
2018-07-03 17:41:55 +02:00
|
|
|
encKeyEnc = await this.encrypt(encKey, key);
|
2018-06-13 23:06:09 +02:00
|
|
|
/*
|
|
|
|
if (key.key.byteLength === 32) {
|
2018-06-13 22:48:00 +02:00
|
|
|
const newKey = await this.stretchKey(key);
|
2018-07-03 17:41:55 +02:00
|
|
|
encKeyEnc = await this.encrypt(encKey, newKey);
|
2018-06-13 23:06:09 +02:00
|
|
|
} else if (key.key.byteLength === 64) {
|
2018-07-03 17:41:55 +02:00
|
|
|
encKeyEnc = await this.encrypt(encKey, key);
|
2018-06-13 22:48:00 +02:00
|
|
|
} else {
|
|
|
|
throw new Error('Invalid key size.');
|
|
|
|
}
|
2018-06-13 23:06:09 +02:00
|
|
|
*/
|
2018-07-03 17:41:55 +02:00
|
|
|
return [new SymmetricCryptoKey(encKey), encKeyEnc];
|
2018-06-13 22:48:00 +02:00
|
|
|
}
|
|
|
|
|
2018-04-22 05:14:04 +02:00
|
|
|
async encrypt(plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey): Promise<CipherString> {
|
|
|
|
if (plainValue == null) {
|
2018-01-08 20:11:28 +01:00
|
|
|
return Promise.resolve(null);
|
|
|
|
}
|
|
|
|
|
2018-04-22 05:14:04 +02:00
|
|
|
let plainBuf: ArrayBuffer;
|
|
|
|
if (typeof (plainValue) === 'string') {
|
|
|
|
plainBuf = Utils.fromUtf8ToArray(plainValue).buffer;
|
2018-01-08 20:11:28 +01:00
|
|
|
} else {
|
2018-04-22 05:14:04 +02:00
|
|
|
plainBuf = plainValue;
|
2018-01-08 20:11:28 +01:00
|
|
|
}
|
|
|
|
|
2018-04-22 05:14:04 +02:00
|
|
|
const encObj = await this.aesEncrypt(plainBuf, key);
|
|
|
|
const iv = Utils.fromBufferToB64(encObj.iv);
|
2018-05-09 22:00:15 +02:00
|
|
|
const data = Utils.fromBufferToB64(encObj.data);
|
2018-04-22 05:14:04 +02:00
|
|
|
const mac = encObj.mac != null ? Utils.fromBufferToB64(encObj.mac) : null;
|
2018-05-09 22:00:15 +02:00
|
|
|
return new CipherString(encObj.key.encType, iv, data, mac);
|
2018-01-08 20:11:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async encryptToBytes(plainValue: ArrayBuffer, key?: SymmetricCryptoKey): Promise<ArrayBuffer> {
|
|
|
|
const encValue = await this.aesEncrypt(plainValue, key);
|
|
|
|
let macLen = 0;
|
2018-04-22 05:14:04 +02:00
|
|
|
if (encValue.mac != null) {
|
|
|
|
macLen = encValue.mac.byteLength;
|
2018-01-08 20:11:28 +01:00
|
|
|
}
|
|
|
|
|
2018-05-09 22:00:15 +02:00
|
|
|
const encBytes = new Uint8Array(1 + encValue.iv.byteLength + macLen + encValue.data.byteLength);
|
2018-01-08 20:11:28 +01:00
|
|
|
encBytes.set([encValue.key.encType]);
|
2018-04-22 05:14:04 +02:00
|
|
|
encBytes.set(new Uint8Array(encValue.iv), 1);
|
|
|
|
if (encValue.mac != null) {
|
|
|
|
encBytes.set(new Uint8Array(encValue.mac), 1 + encValue.iv.byteLength);
|
2018-01-08 20:11:28 +01:00
|
|
|
}
|
|
|
|
|
2018-05-09 22:00:15 +02:00
|
|
|
encBytes.set(new Uint8Array(encValue.data), 1 + encValue.iv.byteLength + macLen);
|
2018-01-08 20:11:28 +01:00
|
|
|
return encBytes.buffer;
|
|
|
|
}
|
|
|
|
|
2018-07-11 19:30:06 +02:00
|
|
|
async rsaEncrypt(data: ArrayBuffer, publicKey?: ArrayBuffer, key?: SymmetricCryptoKey): Promise<CipherString> {
|
|
|
|
if (publicKey == null) {
|
|
|
|
publicKey = await this.getPublicKey();
|
|
|
|
}
|
|
|
|
if (publicKey == null) {
|
|
|
|
throw new Error('Public key unavailable.');
|
|
|
|
}
|
|
|
|
|
|
|
|
let type = EncryptionType.Rsa2048_OaepSha1_B64;
|
|
|
|
const encBytes = await this.cryptoFunctionService.rsaEncrypt(data, publicKey, 'sha1');
|
|
|
|
let mac: string = null;
|
|
|
|
if (key != null && key.macKey != null) {
|
|
|
|
type = EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64;
|
|
|
|
const macBytes = await this.cryptoFunctionService.hmac(encBytes, key.macKey, 'sha256');
|
|
|
|
mac = Utils.fromBufferToB64(macBytes);
|
|
|
|
}
|
|
|
|
return new CipherString(type, Utils.fromBufferToB64(encBytes), null, mac);
|
|
|
|
}
|
|
|
|
|
2018-04-22 05:14:04 +02:00
|
|
|
async decrypt(cipherString: CipherString, key?: SymmetricCryptoKey): Promise<ArrayBuffer> {
|
2018-05-09 22:00:15 +02:00
|
|
|
const iv = Utils.fromB64ToArray(cipherString.iv).buffer;
|
|
|
|
const data = Utils.fromB64ToArray(cipherString.data).buffer;
|
2018-04-22 05:14:04 +02:00
|
|
|
const mac = cipherString.mac ? Utils.fromB64ToArray(cipherString.mac).buffer : null;
|
2018-05-09 22:00:15 +02:00
|
|
|
const decipher = await this.aesDecryptToBytes(cipherString.encryptionType, data, iv, mac, key);
|
2018-04-22 05:14:04 +02:00
|
|
|
if (decipher == null) {
|
2018-01-08 20:11:28 +01:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-04-22 05:14:04 +02:00
|
|
|
return decipher;
|
|
|
|
}
|
2018-01-18 06:08:10 +01:00
|
|
|
|
2018-04-22 05:14:04 +02:00
|
|
|
async decryptToUtf8(cipherString: CipherString, key?: SymmetricCryptoKey): Promise<string> {
|
2018-05-09 22:00:15 +02:00
|
|
|
return await this.aesDecryptToUtf8(cipherString.encryptionType, cipherString.data,
|
|
|
|
cipherString.iv, cipherString.mac, key);
|
2018-01-08 20:11:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async decryptFromBytes(encBuf: ArrayBuffer, key: SymmetricCryptoKey): Promise<ArrayBuffer> {
|
2018-04-22 05:14:04 +02:00
|
|
|
if (encBuf == null) {
|
2018-01-08 20:11:28 +01:00
|
|
|
throw new Error('no encBuf.');
|
|
|
|
}
|
|
|
|
|
|
|
|
const encBytes = new Uint8Array(encBuf);
|
|
|
|
const encType = encBytes[0];
|
|
|
|
let ctBytes: Uint8Array = null;
|
|
|
|
let ivBytes: Uint8Array = null;
|
|
|
|
let macBytes: Uint8Array = null;
|
|
|
|
|
|
|
|
switch (encType) {
|
|
|
|
case EncryptionType.AesCbc128_HmacSha256_B64:
|
|
|
|
case EncryptionType.AesCbc256_HmacSha256_B64:
|
|
|
|
if (encBytes.length <= 49) { // 1 + 16 + 32 + ctLength
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
ivBytes = encBytes.slice(1, 17);
|
|
|
|
macBytes = encBytes.slice(17, 49);
|
|
|
|
ctBytes = encBytes.slice(49);
|
|
|
|
break;
|
|
|
|
case EncryptionType.AesCbc256_B64:
|
|
|
|
if (encBytes.length <= 17) { // 1 + 16 + ctLength
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
ivBytes = encBytes.slice(1, 17);
|
|
|
|
ctBytes = encBytes.slice(17);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-05-07 15:00:49 +02:00
|
|
|
return await this.aesDecryptToBytes(encType, ctBytes.buffer, ivBytes.buffer,
|
2018-04-22 05:14:04 +02:00
|
|
|
macBytes != null ? macBytes.buffer : null, key);
|
2018-01-08 20:11:28 +01:00
|
|
|
}
|
|
|
|
|
2018-04-23 19:03:47 +02:00
|
|
|
// EFForg/OpenWireless
|
|
|
|
// ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js
|
|
|
|
async randomNumber(min: number, max: number): Promise<number> {
|
|
|
|
let rval = 0;
|
|
|
|
const range = max - min + 1;
|
|
|
|
const bitsNeeded = Math.ceil(Math.log2(range));
|
|
|
|
if (bitsNeeded > 53) {
|
|
|
|
throw new Error('We cannot generate numbers larger than 53 bits.');
|
|
|
|
}
|
|
|
|
|
|
|
|
const bytesNeeded = Math.ceil(bitsNeeded / 8);
|
|
|
|
const mask = Math.pow(2, bitsNeeded) - 1;
|
|
|
|
// 7776 -> (2^13 = 8192) -1 == 8191 or 0x00001111 11111111
|
|
|
|
|
|
|
|
// Fill a byte array with N random numbers
|
|
|
|
const byteArray = new Uint8Array(await this.cryptoFunctionService.randomBytes(bytesNeeded));
|
|
|
|
|
|
|
|
let p = (bytesNeeded - 1) * 8;
|
|
|
|
for (let i = 0; i < bytesNeeded; i++) {
|
|
|
|
rval += byteArray[i] * Math.pow(2, p);
|
|
|
|
p -= 8;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Use & to apply the mask and reduce the number of recursive lookups
|
|
|
|
// tslint:disable-next-line
|
|
|
|
rval = rval & mask;
|
|
|
|
|
|
|
|
if (rval >= range) {
|
|
|
|
// Integer out of acceptable range
|
|
|
|
return this.randomNumber(min, max);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return an integer that falls within the range
|
|
|
|
return min + rval;
|
|
|
|
}
|
|
|
|
|
2018-04-22 05:14:04 +02:00
|
|
|
// Helpers
|
|
|
|
|
2018-05-09 22:00:15 +02:00
|
|
|
private async aesEncrypt(data: ArrayBuffer, key: SymmetricCryptoKey): Promise<EncryptedObject> {
|
2018-04-22 05:14:04 +02:00
|
|
|
const obj = new EncryptedObject();
|
|
|
|
obj.key = await this.getKeyForEncryption(key);
|
|
|
|
obj.iv = await this.cryptoFunctionService.randomBytes(16);
|
2018-05-09 22:00:15 +02:00
|
|
|
obj.data = await this.cryptoFunctionService.aesEncrypt(data, obj.iv, obj.key.encKey);
|
2018-04-22 05:14:04 +02:00
|
|
|
|
|
|
|
if (obj.key.macKey != null) {
|
2018-05-09 22:00:15 +02:00
|
|
|
const macData = new Uint8Array(obj.iv.byteLength + obj.data.byteLength);
|
2018-04-22 05:14:04 +02:00
|
|
|
macData.set(new Uint8Array(obj.iv), 0);
|
2018-05-09 22:00:15 +02:00
|
|
|
macData.set(new Uint8Array(obj.data), obj.iv.byteLength);
|
2018-04-22 05:14:04 +02:00
|
|
|
obj.mac = await this.cryptoFunctionService.hmac(macData.buffer, obj.key.macKey, 'sha256');
|
|
|
|
}
|
|
|
|
|
|
|
|
return obj;
|
|
|
|
}
|
|
|
|
|
2018-05-09 22:00:15 +02:00
|
|
|
private async aesDecryptToUtf8(encType: EncryptionType, data: string, iv: string, mac: string,
|
2018-05-07 15:00:49 +02:00
|
|
|
key: SymmetricCryptoKey): Promise<string> {
|
2018-04-22 05:14:04 +02:00
|
|
|
const keyForEnc = await this.getKeyForEncryption(key);
|
|
|
|
const theKey = this.resolveLegacyKey(encType, keyForEnc);
|
|
|
|
|
|
|
|
if (theKey.macKey != null && mac == null) {
|
|
|
|
// tslint:disable-next-line
|
|
|
|
console.error('mac required.');
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-05-07 15:00:49 +02:00
|
|
|
if (theKey.encType !== encType) {
|
2018-04-22 05:14:04 +02:00
|
|
|
// tslint:disable-next-line
|
|
|
|
console.error('encType unavailable.');
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-05-09 22:00:15 +02:00
|
|
|
const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(data, iv, mac, theKey);
|
2018-05-07 15:00:49 +02:00
|
|
|
if (fastParams.macKey != null && fastParams.mac != null) {
|
|
|
|
const computedMac = await this.cryptoFunctionService.hmacFast(fastParams.macData,
|
|
|
|
fastParams.macKey, 'sha256');
|
2018-05-07 18:14:40 +02:00
|
|
|
const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac);
|
2018-05-07 15:00:49 +02:00
|
|
|
if (!macsEqual) {
|
2018-04-22 05:14:04 +02:00
|
|
|
// tslint:disable-next-line
|
|
|
|
console.error('mac failed.');
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-07 15:00:49 +02:00
|
|
|
return this.cryptoFunctionService.aesDecryptFast(fastParams);
|
2018-04-22 05:14:04 +02:00
|
|
|
}
|
|
|
|
|
2018-05-09 22:00:15 +02:00
|
|
|
private async aesDecryptToBytes(encType: EncryptionType, data: ArrayBuffer, iv: ArrayBuffer,
|
2018-04-22 05:14:04 +02:00
|
|
|
mac: ArrayBuffer, key: SymmetricCryptoKey): Promise<ArrayBuffer> {
|
2018-05-07 15:00:49 +02:00
|
|
|
const keyForEnc = await this.getKeyForEncryption(key);
|
|
|
|
const theKey = this.resolveLegacyKey(encType, keyForEnc);
|
|
|
|
|
|
|
|
if (theKey.macKey != null && mac == null) {
|
2018-04-22 05:14:04 +02:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-05-07 15:00:49 +02:00
|
|
|
if (theKey.encType !== encType) {
|
2018-04-22 05:14:04 +02:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-05-07 15:00:49 +02:00
|
|
|
if (theKey.macKey != null && mac != null) {
|
2018-05-09 22:00:15 +02:00
|
|
|
const macData = new Uint8Array(iv.byteLength + data.byteLength);
|
2018-05-07 15:00:49 +02:00
|
|
|
macData.set(new Uint8Array(iv), 0);
|
2018-05-09 22:00:15 +02:00
|
|
|
macData.set(new Uint8Array(data), iv.byteLength);
|
2018-05-07 15:00:49 +02:00
|
|
|
const computedMac = await this.cryptoFunctionService.hmac(macData.buffer, theKey.macKey, 'sha256');
|
|
|
|
if (computedMac === null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-05-07 18:14:40 +02:00
|
|
|
const macsMatch = await this.cryptoFunctionService.compare(mac, computedMac);
|
2018-05-07 15:00:49 +02:00
|
|
|
if (!macsMatch) {
|
|
|
|
// tslint:disable-next-line
|
|
|
|
console.error('mac failed.');
|
|
|
|
return null;
|
|
|
|
}
|
2018-04-22 05:14:04 +02:00
|
|
|
}
|
|
|
|
|
2018-05-09 22:00:15 +02:00
|
|
|
return await this.cryptoFunctionService.aesDecrypt(data, iv, theKey.encKey);
|
2018-04-22 05:14:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private async rsaDecrypt(encValue: string): Promise<ArrayBuffer> {
|
2018-01-08 20:11:28 +01:00
|
|
|
const headerPieces = encValue.split('.');
|
|
|
|
let encType: EncryptionType = null;
|
|
|
|
let encPieces: string[];
|
|
|
|
|
|
|
|
if (headerPieces.length === 1) {
|
|
|
|
encType = EncryptionType.Rsa2048_OaepSha256_B64;
|
|
|
|
encPieces = [headerPieces[0]];
|
|
|
|
} else if (headerPieces.length === 2) {
|
|
|
|
try {
|
|
|
|
encType = parseInt(headerPieces[0], null);
|
|
|
|
encPieces = headerPieces[1].split('|');
|
|
|
|
} catch (e) { }
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (encType) {
|
|
|
|
case EncryptionType.Rsa2048_OaepSha256_B64:
|
|
|
|
case EncryptionType.Rsa2048_OaepSha1_B64:
|
|
|
|
if (encPieces.length !== 1) {
|
|
|
|
throw new Error('Invalid cipher format.');
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64:
|
|
|
|
case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64:
|
|
|
|
if (encPieces.length !== 2) {
|
|
|
|
throw new Error('Invalid cipher format.');
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new Error('encType unavailable.');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (encPieces == null || encPieces.length <= 0) {
|
|
|
|
throw new Error('encPieces unavailable.');
|
|
|
|
}
|
|
|
|
|
2018-05-09 22:00:15 +02:00
|
|
|
const data = Utils.fromB64ToArray(encPieces[0]).buffer;
|
2018-01-08 20:11:28 +01:00
|
|
|
const key = await this.getEncKey();
|
|
|
|
if (key != null && key.macKey != null && encPieces.length > 1) {
|
2018-04-22 05:14:04 +02:00
|
|
|
const mac = Utils.fromB64ToArray(encPieces[1]).buffer;
|
2018-05-09 22:00:15 +02:00
|
|
|
const computedMac = await this.cryptoFunctionService.hmac(data, key.macKey, 'sha256');
|
2018-05-07 18:14:40 +02:00
|
|
|
const macsEqual = await this.cryptoFunctionService.compare(mac, computedMac);
|
2018-01-08 20:11:28 +01:00
|
|
|
if (!macsEqual) {
|
|
|
|
throw new Error('MAC failed.');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-22 05:14:04 +02:00
|
|
|
const privateKey = await this.getPrivateKey();
|
|
|
|
if (privateKey == null) {
|
2018-01-08 20:11:28 +01:00
|
|
|
throw new Error('No private key.');
|
|
|
|
}
|
|
|
|
|
2018-04-22 05:14:04 +02:00
|
|
|
let alg: 'sha1' | 'sha256' = 'sha1';
|
2018-01-08 20:11:28 +01:00
|
|
|
switch (encType) {
|
|
|
|
case EncryptionType.Rsa2048_OaepSha256_B64:
|
|
|
|
case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64:
|
2018-04-22 05:14:04 +02:00
|
|
|
alg = 'sha256';
|
2018-01-08 20:11:28 +01:00
|
|
|
break;
|
|
|
|
case EncryptionType.Rsa2048_OaepSha1_B64:
|
|
|
|
case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64:
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new Error('encType unavailable.');
|
|
|
|
}
|
|
|
|
|
2018-05-09 22:00:15 +02:00
|
|
|
return this.cryptoFunctionService.rsaDecrypt(data, privateKey, alg);
|
2018-01-08 20:11:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private async getKeyForEncryption(key?: SymmetricCryptoKey): Promise<SymmetricCryptoKey> {
|
2018-04-22 05:14:04 +02:00
|
|
|
if (key != null) {
|
2018-01-08 20:11:28 +01:00
|
|
|
return key;
|
|
|
|
}
|
|
|
|
|
|
|
|
const encKey = await this.getEncKey();
|
2018-04-22 05:14:04 +02:00
|
|
|
if (encKey != null) {
|
|
|
|
return encKey;
|
|
|
|
}
|
|
|
|
|
|
|
|
return await this.getKey();
|
2018-01-08 20:11:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private resolveLegacyKey(encType: EncryptionType, key: SymmetricCryptoKey): SymmetricCryptoKey {
|
|
|
|
if (encType === EncryptionType.AesCbc128_HmacSha256_B64 &&
|
|
|
|
key.encType === EncryptionType.AesCbc256_B64) {
|
|
|
|
// Old encrypt-then-mac scheme, make a new key
|
2018-04-22 05:14:04 +02:00
|
|
|
if (this.legacyEtmKey == null) {
|
|
|
|
this.legacyEtmKey = new SymmetricCryptoKey(key.key, EncryptionType.AesCbc128_HmacSha256_B64);
|
|
|
|
}
|
2018-01-08 20:11:28 +01:00
|
|
|
return this.legacyEtmKey;
|
|
|
|
}
|
|
|
|
|
|
|
|
return key;
|
|
|
|
}
|
2018-06-13 22:48:00 +02:00
|
|
|
|
2018-06-13 23:10:52 +02:00
|
|
|
private async stretchKey(key: SymmetricCryptoKey): Promise<SymmetricCryptoKey> {
|
|
|
|
const newKey = new Uint8Array(64);
|
|
|
|
newKey.set(await this.hkdfExpand(key.key, Utils.fromUtf8ToArray('enc'), 32));
|
|
|
|
newKey.set(await this.hkdfExpand(key.key, Utils.fromUtf8ToArray('mac'), 32), 32);
|
|
|
|
return new SymmetricCryptoKey(newKey.buffer);
|
|
|
|
}
|
|
|
|
|
2018-06-13 22:48:00 +02:00
|
|
|
// ref: https://tools.ietf.org/html/rfc5869
|
|
|
|
private async hkdfExpand(prk: ArrayBuffer, info: Uint8Array, size: number) {
|
|
|
|
const hashLen = 32; // sha256
|
|
|
|
const okm = new Uint8Array(size);
|
|
|
|
let previousT = new Uint8Array(0);
|
|
|
|
const n = Math.ceil(size / hashLen);
|
|
|
|
for (let i = 0; i < n; i++) {
|
|
|
|
const t = new Uint8Array(previousT.length + info.length + 1);
|
|
|
|
t.set(previousT);
|
|
|
|
t.set(info, previousT.length);
|
|
|
|
t.set([i + 1], t.length - 1);
|
2018-06-14 04:47:40 +02:00
|
|
|
previousT = new Uint8Array(await this.cryptoFunctionService.hmac(t.buffer, prk, 'sha256'));
|
2018-06-13 22:48:00 +02:00
|
|
|
okm.set(previousT, i * hashLen);
|
|
|
|
}
|
|
|
|
return okm;
|
|
|
|
}
|
2018-01-08 20:11:28 +01:00
|
|
|
}
|