From 3454d93fef76f84c7351990089f7b155b88580f8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 3 Jul 2018 11:41:55 -0400 Subject: [PATCH] generate keypair on registration --- .../nodeCryptoFunction.service.spec.ts | 19 +++++++++++++ .../webCryptoFunction.service.spec.ts | 16 +++++++++++ src/abstractions/crypto.service.ts | 5 ++-- src/abstractions/cryptoFunction.service.ts | 1 + src/angular/components/register.component.ts | 27 +++++++++++-------- src/models/request/registerRequest.ts | 17 ++++++++++-- src/services/crypto.service.ts | 22 ++++++++++----- src/services/lock.service.ts | 2 +- src/services/nodeCryptoFunction.service.ts | 26 ++++++++++++++++++ src/services/webCryptoFunction.service.ts | 14 ++++++++++ 10 files changed, 127 insertions(+), 22 deletions(-) diff --git a/spec/node/services/nodeCryptoFunction.service.spec.ts b/spec/node/services/nodeCryptoFunction.service.spec.ts index 3bd8835127..828b8decec 100644 --- a/spec/node/services/nodeCryptoFunction.service.spec.ts +++ b/spec/node/services/nodeCryptoFunction.service.spec.ts @@ -170,6 +170,15 @@ describe('NodeCrypto Function Service', () => { }); }); + describe('rsaGenerateKeyPair', () => { + testRsaGenerateKeyPair(1024); + testRsaGenerateKeyPair(2048); + + // Generating 4096 bit keys is really slow with Forge lib. + // Maybe move to something else if we ever want to generate keys of this size. + // testRsaGenerateKeyPair(4096); + }); + describe('randomBytes', () => { it('should make a value of the correct length', async () => { const nodeCryptoFunctionService = new NodeCryptoFunctionService(); @@ -302,6 +311,16 @@ function testCompare(fast = false) { }); } +function testRsaGenerateKeyPair(length: 1024 | 2048 | 4096) { + it('should successfully generate a ' + length + ' bit key pair', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const keyPair = await cryptoFunctionService.rsaGenerateKeyPair(length); + expect(keyPair[0] == null || keyPair[1] == null).toBe(false); + const publicKey = await cryptoFunctionService.rsaExtractPublicKey(keyPair[1]); + expect(Utils.fromBufferToB64(keyPair[0])).toBe(Utils.fromBufferToB64(publicKey)); + }, 10000); +} + function makeStaticByteArray(length: number) { const arr = new Uint8Array(length); for (let i = 0; i < length; i++) { diff --git a/spec/web/services/webCryptoFunction.service.spec.ts b/spec/web/services/webCryptoFunction.service.spec.ts index 92c80e090d..50605b10fb 100644 --- a/spec/web/services/webCryptoFunction.service.spec.ts +++ b/spec/web/services/webCryptoFunction.service.spec.ts @@ -256,6 +256,12 @@ describe('WebCrypto Function Service', () => { }); }); + describe('rsaGenerateKeyPair', () => { + testRsaGenerateKeyPair(1024); + testRsaGenerateKeyPair(2048); + testRsaGenerateKeyPair(4096); + }); + describe('randomBytes', () => { it('should make a value of the correct length', async () => { const cryptoFunctionService = getWebCryptoFunctionService(); @@ -357,6 +363,16 @@ function testHmacFast(algorithm: 'sha1' | 'sha256' | 'sha512', mac: string) { }); } +function testRsaGenerateKeyPair(length: 1024 | 2048 | 4096) { + it('should successfully generate a ' + length + ' bit key pair', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const keyPair = await cryptoFunctionService.rsaGenerateKeyPair(length); + expect(keyPair[0] == null || keyPair[1] == null).toBe(false); + const publicKey = await cryptoFunctionService.rsaExtractPublicKey(keyPair[1]); + expect(Utils.fromBufferToB64(keyPair[0])).toBe(Utils.fromBufferToB64(publicKey)); + }); +} + function getWebCryptoFunctionService() { const platformUtilsMock = TypeMoq.Mock.ofType(PlatformUtilsServiceMock); platformUtilsMock.setup((x) => x.isEdge()).returns(() => navigator.userAgent.indexOf(' Edge/') !== -1); diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index c4807087c1..9c153efca9 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -20,14 +20,15 @@ export abstract class CryptoService { clearKey: () => Promise; clearKeyHash: () => Promise; clearEncKey: (memoryOnly?: boolean) => Promise; - clearPrivateKey: (memoryOnly?: boolean) => Promise; + clearKeyPair: (memoryOnly?: boolean) => Promise; clearOrgKeys: (memoryOnly?: boolean) => Promise; clearKeys: () => Promise; toggleKey: () => Promise; makeKey: (password: string, salt: string) => Promise; makeShareKey: () => Promise<[CipherString, SymmetricCryptoKey]>; + makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, CipherString]>; hashPassword: (password: string, key: SymmetricCryptoKey) => Promise; - makeEncKey: (key: SymmetricCryptoKey) => Promise; + makeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]>; encrypt: (plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey) => Promise; encryptToBytes: (plainValue: ArrayBuffer, key?: SymmetricCryptoKey) => Promise; decryptToUtf8: (cipherString: CipherString, key?: SymmetricCryptoKey) => Promise; diff --git a/src/abstractions/cryptoFunction.service.ts b/src/abstractions/cryptoFunction.service.ts index d45221ea2d..72b64fac57 100644 --- a/src/abstractions/cryptoFunction.service.ts +++ b/src/abstractions/cryptoFunction.service.ts @@ -18,5 +18,6 @@ export abstract class CryptoFunctionService { rsaEncrypt: (data: ArrayBuffer, publicKey: ArrayBuffer, algorithm: 'sha1' | 'sha256') => Promise; rsaDecrypt: (data: ArrayBuffer, privateKey: ArrayBuffer, algorithm: 'sha1' | 'sha256') => Promise; rsaExtractPublicKey: (privateKey: ArrayBuffer) => Promise; + rsaGenerateKeyPair: (length: 1024 | 2048 | 4096) => Promise<[ArrayBuffer, ArrayBuffer]>; randomBytes: (length: number) => Promise; } diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index 8b89897324..336073e50e 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -3,7 +3,10 @@ import { Router } from '@angular/router'; import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; -import { RegisterRequest } from '../../models/request/registerRequest'; +import { + RegisterKeysRequest, + RegisterRequest, +} from '../../models/request/registerRequest'; import { ApiService } from '../../abstractions/api.service'; import { AuthService } from '../../abstractions/auth.service'; @@ -11,6 +14,7 @@ import { CryptoService } from '../../abstractions/crypto.service'; import { I18nService } from '../../abstractions/i18n.service'; export class RegisterComponent { + name: string = ''; email: string = ''; masterPassword: string = ''; confirmMasterPassword: string = ''; @@ -52,8 +56,18 @@ export class RegisterComponent { return; } + this.name = this.name === '' ? null : this.name; + this.email = this.email.toLowerCase(); + const key = await this.cryptoService.makeKey(this.masterPassword, this.email); + const encKey = await this.cryptoService.makeEncKey(key); + const hashedPassword = await this.cryptoService.hashPassword(this.masterPassword, key); + const keys = await this.cryptoService.makeKeyPair(encKey[0]); + const request = new RegisterRequest(this.email, this.name, hashedPassword, + this.hint, encKey[1].encryptedString); + request.keys = new RegisterKeysRequest(keys[0], keys[1].encryptedString); + try { - this.formPromise = this.register(); + this.formPromise = this.apiService.postRegister(request); await this.formPromise; this.analytics.eventTrack.next({ action: 'Registered' }); this.toasterService.popAsync('success', null, this.i18nService.t('newAccountCreated')); @@ -66,13 +80,4 @@ export class RegisterComponent { this.showPassword = !this.showPassword; document.getElementById(confirmField ? 'masterPasswordRetype' : 'masterPassword').focus(); } - - private async register() { - this.email = this.email.toLowerCase(); - const key = await this.cryptoService.makeKey(this.masterPassword, this.email); - const encKey = await this.cryptoService.makeEncKey(key); - const hashedPassword = await this.cryptoService.hashPassword(this.masterPassword, key); - const request = new RegisterRequest(this.email, hashedPassword, this.hint, encKey.encryptedString); - await this.apiService.postRegister(request); - } } diff --git a/src/models/request/registerRequest.ts b/src/models/request/registerRequest.ts index fc815d5bb7..9f375af0c1 100644 --- a/src/models/request/registerRequest.ts +++ b/src/models/request/registerRequest.ts @@ -4,12 +4,25 @@ export class RegisterRequest { masterPasswordHash: string; masterPasswordHint: string; key: string; + keys: RegisterKeysRequest; + token: string; + organizationUserId: string; - constructor(email: string, masterPasswordHash: string, masterPasswordHint: string, key: string) { - this.name = null; + constructor(email: string, name: string, masterPasswordHash: string, masterPasswordHint: string, key: string) { + this.name = name; this.email = email; this.masterPasswordHash = masterPasswordHash; this.masterPasswordHint = masterPasswordHint ? masterPasswordHint : null; this.key = key; } } + +export class RegisterKeysRequest { + publicKey: string; + encryptedPrivateKey: string; + + constructor(publicKey: string, encryptedPrivateKey: string) { + this.publicKey = publicKey; + this.encryptedPrivateKey = encryptedPrivateKey; + } +} diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 356d15ccae..43aa0e0fd2 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -229,8 +229,9 @@ export class CryptoService implements CryptoServiceAbstraction { return this.storageService.remove(Keys.encKey); } - clearPrivateKey(memoryOnly?: boolean): Promise { + clearKeyPair(memoryOnly?: boolean): Promise { this.privateKey = null; + this.publicKey = null; if (memoryOnly) { return Promise.resolve(); } @@ -251,7 +252,7 @@ export class CryptoService implements CryptoServiceAbstraction { this.clearKeyHash(), this.clearOrgKeys(), this.clearEncKey(), - this.clearPrivateKey(), + this.clearKeyPair(), ]); } @@ -281,6 +282,13 @@ export class CryptoService implements CryptoServiceAbstraction { return [encShareKey, new SymmetricCryptoKey(shareKey)]; } + 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]; + } + async hashPassword(password: string, key: SymmetricCryptoKey): Promise { if (key == null) { key = await this.getKey(); @@ -293,20 +301,22 @@ export class CryptoService implements CryptoServiceAbstraction { return Utils.fromBufferToB64(hash); } - async makeEncKey(key: SymmetricCryptoKey): Promise { + async makeEncKey(key: SymmetricCryptoKey): Promise<[SymmetricCryptoKey, CipherString]> { const encKey = await this.cryptoFunctionService.randomBytes(64); + let encKeyEnc: CipherString = null; // TODO: Uncomment when we're ready to enable key stretching - return this.encrypt(encKey, key); + encKeyEnc = await this.encrypt(encKey, key); /* if (key.key.byteLength === 32) { const newKey = await this.stretchKey(key); - return this.encrypt(encKey, newKey); + encKeyEnc = await this.encrypt(encKey, newKey); } else if (key.key.byteLength === 64) { - return this.encrypt(encKey, key); + encKeyEnc = await this.encrypt(encKey, key); } else { throw new Error('Invalid key size.'); } */ + return [new SymmetricCryptoKey(encKey), encKeyEnc]; } async encrypt(plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey): Promise { diff --git a/src/services/lock.service.ts b/src/services/lock.service.ts index bc744ce012..4d00022741 100644 --- a/src/services/lock.service.ts +++ b/src/services/lock.service.ts @@ -67,7 +67,7 @@ export class LockService implements LockServiceAbstraction { await Promise.all([ this.cryptoService.clearKey(), this.cryptoService.clearOrgKeys(true), - this.cryptoService.clearPrivateKey(true), + this.cryptoService.clearKeyPair(true), this.cryptoService.clearEncKey(true), ]); diff --git a/src/services/nodeCryptoFunction.service.ts b/src/services/nodeCryptoFunction.service.ts index 7af5de0ffb..eedacf5374 100644 --- a/src/services/nodeCryptoFunction.service.ts +++ b/src/services/nodeCryptoFunction.service.ts @@ -144,6 +144,32 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { return Promise.resolve(publicKeyArray.buffer); } + async rsaGenerateKeyPair(length: 1024 | 2048 | 4096): Promise<[ArrayBuffer, ArrayBuffer]> { + return new Promise<[ArrayBuffer, ArrayBuffer]>((resolve, reject) => { + forge.pki.rsa.generateKeyPair({ + bits: length, + workers: -1, + e: 0x10001, // 65537 + }, (error, keyPair) => { + if (error != null) { + reject(error); + return; + } + + const publicKeyAsn1 = (forge.pki as any).publicKeyToAsn1(keyPair.publicKey); + const publicKeyByteString = forge.asn1.toDer(publicKeyAsn1).getBytes(); + const publicKey = Utils.fromByteStringToArray(publicKeyByteString); + + const privateKeyAsn1 = (forge.pki as any).privateKeyToAsn1(keyPair.privateKey); + const privateKeyPkcs8 = (forge.pki as any).wrapRsaPrivateKey(privateKeyAsn1); + const privateKeyByteString = forge.asn1.toDer(privateKeyPkcs8).getBytes(); + const privateKey = Utils.fromByteStringToArray(privateKeyByteString); + + resolve([publicKey.buffer, privateKey.buffer]); + }); + }); + } + randomBytes(length: number): Promise { return new Promise((resolve, reject) => { crypto.randomBytes(length, (error, bytes) => { diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index f5617236b9..0c6c23d4e9 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -223,6 +223,20 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return await this.subtle.exportKey('spki', impPublicKey); } + async rsaGenerateKeyPair(length: 1024 | 2048 | 4096): Promise<[ArrayBuffer, ArrayBuffer]> { + const rsaParams = { + name: 'RSA-OAEP', + modulusLength: length, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 65537 + // Have to specify some algorithm + hash: { name: this.toWebCryptoAlgorithm('sha1') }, + }; + const keyPair = await this.subtle.generateKey(rsaParams, true, ['encrypt', 'decrypt']); + const publicKey = await this.subtle.exportKey('spki', keyPair.publicKey); + const privateKey = await this.subtle.exportKey('pkcs8', keyPair.privateKey); + return [publicKey, privateKey]; + } + randomBytes(length: number): Promise { const arr = new Uint8Array(length); this.crypto.getRandomValues(arr);