diff --git a/spec/node/services/nodeCryptoFunction.service.spec.ts b/spec/node/services/nodeCryptoFunction.service.spec.ts index 8b58312da6..392c0a61bc 100644 --- a/spec/node/services/nodeCryptoFunction.service.spec.ts +++ b/spec/node/services/nodeCryptoFunction.service.spec.ts @@ -49,6 +49,45 @@ describe('NodeCrypto Function Service', () => { testPbkdf2('sha512', regular512Key, utf8512Key, unicode512Key); }); + describe('hkdf', () => { + const regular256Key = 'qBUmEYtwTwwGPuw/z6bs/qYXXYNUlocFlyAuuANI8Pw='; + const utf8256Key = '6DfJwW1R3txgiZKkIFTvVAb7qVlG7lKcmJGJoxR2GBU='; + const unicode256Key = 'gejGI82xthA+nKtKmIh82kjw+ttHr+ODsUoGdu5sf0A='; + + const regular512Key = 'xe5cIG6ZfwGmb1FvsOedM0XKOm21myZkjL/eDeKIqqM='; + const utf8512Key = 'XQMVBnxVEhlvjSFDQc77j5GDE9aorvbS0vKnjhRg0LY='; + const unicode512Key = '148GImrTbrjaGAe/iWEpclINM8Ehhko+9lB14+52lqc='; + + testHkdf('sha256', regular256Key, utf8256Key, unicode256Key); + testHkdf('sha512', regular512Key, utf8512Key, unicode512Key); + }); + + describe('hkdfExpand', () => { + const prk16Byte = 'criAmKtfzxanbgea5/kelQ=='; + const prk32Byte = 'F5h4KdYQnIVH4rKH0P9CZb1GrR4n16/sJrS0PsQEn0Y='; + const prk64Byte = 'ssBK0mRG17VHdtsgt8yo4v25CRNpauH+0r2fwY/E9rLyaFBAOMbIeTry+' + + 'gUJ28p8y+hFh3EI9pcrEWaNvFYonQ==' + + testHkdfExpand('sha256', prk32Byte, 32, 'BnIqJlfnHm0e/2iB/15cbHyR19ARPIcWRp4oNS22CD8='); + testHkdfExpand('sha256', prk32Byte, 64, 'BnIqJlfnHm0e/2iB/15cbHyR19ARPIcWRp4oNS22CD9BV+' + + '/queOZenPNkDhmlVyL2WZ3OSU5+7ISNF5NhNfvZA=='); + testHkdfExpand('sha512', prk64Byte, 32, 'uLWbMWodSBms5uGJ5WTRTesyW+MD7nlpCZvagvIRXlk='); + testHkdfExpand('sha512', prk64Byte, 64, 'uLWbMWodSBms5uGJ5WTRTesyW+MD7nlpCZvagvIRXlkY5Pv0sB+' + + 'MqvaopmkC6sD/j89zDwTV9Ib2fpucUydO8w=='); + + it('should fail with prk too small', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const f = cryptoFunctionService.hkdfExpand(Utils.fromB64ToArray(prk16Byte), 'info', 32, 'sha256'); + await expectAsync(f).toBeRejectedWith(new Error('prk is too small.')); + }); + + it('should fail with outputByteSize is too large', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const f = cryptoFunctionService.hkdfExpand(Utils.fromB64ToArray(prk32Byte), 'info', 8161, 'sha256'); + await expectAsync(f).toBeRejectedWith(new Error('outputByteSize is too large.')); + }); + }); + describe('hash', () => { const regular1Hash = '2a241604fb921fad12bf877282457268e1dccb70'; const utf81Hash = '85672798dc5831e96d6c48655d3d39365a9c88b6'; @@ -234,6 +273,55 @@ function testPbkdf2(algorithm: 'sha256' | 'sha512', regularKey: string, utf8Key: }); } +function testHkdf(algorithm: 'sha256' | 'sha512', regularKey: string, utf8Key: string, unicodeKey: string) { + const ikm = Utils.fromB64ToArray('criAmKtfzxanbgea5/kelQ=='); + + const regularSalt = 'salt'; + const utf8Salt = 'üser_salt'; + const unicodeSalt = '😀salt🙏'; + + const regularInfo = 'info'; + const utf8Info = 'üser_info'; + const unicodeInfo = '😀info🙏'; + + it('should create valid ' + algorithm + ' key from regular input', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const key = await cryptoFunctionService.hkdf(ikm, regularSalt, regularInfo, 32, algorithm); + expect(Utils.fromBufferToB64(key)).toBe(regularKey); + }); + + it('should create valid ' + algorithm + ' key from utf8 input', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const key = await cryptoFunctionService.hkdf(ikm, utf8Salt, utf8Info, 32, algorithm); + expect(Utils.fromBufferToB64(key)).toBe(utf8Key); + }); + + it('should create valid ' + algorithm + ' key from unicode input', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const key = await cryptoFunctionService.hkdf(ikm, unicodeSalt, unicodeInfo, 32, algorithm); + expect(Utils.fromBufferToB64(key)).toBe(unicodeKey); + }); + + it('should create valid ' + algorithm + ' key from array buffer input', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const key = await cryptoFunctionService.hkdf(ikm, Utils.fromUtf8ToArray(regularSalt).buffer, + Utils.fromUtf8ToArray(regularInfo).buffer, 32, algorithm); + expect(Utils.fromBufferToB64(key)).toBe(regularKey); + }); +} + +function testHkdfExpand(algorithm: 'sha256' | 'sha512', b64prk: string, outputByteSize: number, + b64ExpectedOkm: string) { + const info = 'info'; + + it('should create valid ' + algorithm + ' ' + outputByteSize + ' byte okm', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const okm = await cryptoFunctionService.hkdfExpand(Utils.fromB64ToArray(b64prk), info, outputByteSize, + algorithm); + expect(Utils.fromBufferToB64(okm)).toBe(b64ExpectedOkm); + }); +} + function testHash(algorithm: 'sha1' | 'sha256' | 'sha512' | 'md5', regularHash: string, utf8Hash: string, unicodeHash: string) { const regularValue = 'HashMe!!'; diff --git a/spec/web/services/webCryptoFunction.service.spec.ts b/spec/web/services/webCryptoFunction.service.spec.ts index d1ead1592e..e71cb3a493 100644 --- a/spec/web/services/webCryptoFunction.service.spec.ts +++ b/spec/web/services/webCryptoFunction.service.spec.ts @@ -53,6 +53,45 @@ describe('WebCrypto Function Service', () => { testPbkdf2('sha512', regular512Key, utf8512Key, unicode512Key); }); + describe('hkdf', () => { + const regular256Key = 'qBUmEYtwTwwGPuw/z6bs/qYXXYNUlocFlyAuuANI8Pw='; + const utf8256Key = '6DfJwW1R3txgiZKkIFTvVAb7qVlG7lKcmJGJoxR2GBU='; + const unicode256Key = 'gejGI82xthA+nKtKmIh82kjw+ttHr+ODsUoGdu5sf0A='; + + const regular512Key = 'xe5cIG6ZfwGmb1FvsOedM0XKOm21myZkjL/eDeKIqqM='; + const utf8512Key = 'XQMVBnxVEhlvjSFDQc77j5GDE9aorvbS0vKnjhRg0LY='; + const unicode512Key = '148GImrTbrjaGAe/iWEpclINM8Ehhko+9lB14+52lqc='; + + testHkdf('sha256', regular256Key, utf8256Key, unicode256Key); + testHkdf('sha512', regular512Key, utf8512Key, unicode512Key); + }); + + describe('hkdfExpand', () => { + const prk16Byte = 'criAmKtfzxanbgea5/kelQ=='; + const prk32Byte = 'F5h4KdYQnIVH4rKH0P9CZb1GrR4n16/sJrS0PsQEn0Y='; + const prk64Byte = 'ssBK0mRG17VHdtsgt8yo4v25CRNpauH+0r2fwY/E9rLyaFBAOMbIeTry+' + + 'gUJ28p8y+hFh3EI9pcrEWaNvFYonQ==' + + testHkdfExpand('sha256', prk32Byte, 32, 'BnIqJlfnHm0e/2iB/15cbHyR19ARPIcWRp4oNS22CD8='); + testHkdfExpand('sha256', prk32Byte, 64, 'BnIqJlfnHm0e/2iB/15cbHyR19ARPIcWRp4oNS22CD9BV+' + + '/queOZenPNkDhmlVyL2WZ3OSU5+7ISNF5NhNfvZA=='); + testHkdfExpand('sha512', prk64Byte, 32, 'uLWbMWodSBms5uGJ5WTRTesyW+MD7nlpCZvagvIRXlk='); + testHkdfExpand('sha512', prk64Byte, 64, 'uLWbMWodSBms5uGJ5WTRTesyW+MD7nlpCZvagvIRXlkY5Pv0sB+' + + 'MqvaopmkC6sD/j89zDwTV9Ib2fpucUydO8w=='); + + it('should fail with prk too small', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const f = cryptoFunctionService.hkdfExpand(Utils.fromB64ToArray(prk16Byte), 'info', 32, 'sha256'); + await expectAsync(f).toBeRejectedWith(new Error('prk is too small.')); + }); + + it('should fail with outputByteSize is too large', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const f = cryptoFunctionService.hkdfExpand(Utils.fromB64ToArray(prk32Byte), 'info', 8161, 'sha256'); + await expectAsync(f).toBeRejectedWith(new Error('outputByteSize is too large.')); + }); + }); + describe('hash', () => { const regular1Hash = '2a241604fb921fad12bf877282457268e1dccb70'; const utf81Hash = '85672798dc5831e96d6c48655d3d39365a9c88b6'; @@ -318,6 +357,55 @@ function testPbkdf2(algorithm: 'sha256' | 'sha512', regularKey: string, }); } +function testHkdf(algorithm: 'sha256' | 'sha512', regularKey: string, utf8Key: string, unicodeKey: string) { + const ikm = Utils.fromB64ToArray('criAmKtfzxanbgea5/kelQ=='); + + const regularSalt = 'salt'; + const utf8Salt = 'üser_salt'; + const unicodeSalt = '😀salt🙏'; + + const regularInfo = 'info'; + const utf8Info = 'üser_info'; + const unicodeInfo = '😀info🙏'; + + it('should create valid ' + algorithm + ' key from regular input', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const key = await cryptoFunctionService.hkdf(ikm, regularSalt, regularInfo, 32, algorithm); + expect(Utils.fromBufferToB64(key)).toBe(regularKey); + }); + + it('should create valid ' + algorithm + ' key from utf8 input', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const key = await cryptoFunctionService.hkdf(ikm, utf8Salt, utf8Info, 32, algorithm); + expect(Utils.fromBufferToB64(key)).toBe(utf8Key); + }); + + it('should create valid ' + algorithm + ' key from unicode input', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const key = await cryptoFunctionService.hkdf(ikm, unicodeSalt, unicodeInfo, 32, algorithm); + expect(Utils.fromBufferToB64(key)).toBe(unicodeKey); + }); + + it('should create valid ' + algorithm + ' key from array buffer input', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const key = await cryptoFunctionService.hkdf(ikm, Utils.fromUtf8ToArray(regularSalt).buffer, + Utils.fromUtf8ToArray(regularInfo).buffer, 32, algorithm); + expect(Utils.fromBufferToB64(key)).toBe(regularKey); + }); +} + +function testHkdfExpand(algorithm: 'sha256' | 'sha512', b64prk: string, outputByteSize: number, + b64ExpectedOkm: string) { + const info = 'info'; + + it('should create valid ' + algorithm + ' ' + outputByteSize + ' byte okm', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const okm = await cryptoFunctionService.hkdfExpand(Utils.fromB64ToArray(b64prk), info, outputByteSize, + algorithm); + expect(Utils.fromBufferToB64(okm)).toBe(b64ExpectedOkm); + }); +} + function testHash(algorithm: 'sha1' | 'sha256' | 'sha512' | 'md5', regularHash: string, utf8Hash: string, unicodeHash: string) { const regularValue = 'HashMe!!'; diff --git a/src/abstractions/cryptoFunction.service.ts b/src/abstractions/cryptoFunction.service.ts index 45b1014a29..ddb38d1fe7 100644 --- a/src/abstractions/cryptoFunction.service.ts +++ b/src/abstractions/cryptoFunction.service.ts @@ -4,6 +4,10 @@ import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; export abstract class CryptoFunctionService { pbkdf2: (password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512', iterations: number) => Promise; + hkdf: (ikm: ArrayBuffer, salt: string | ArrayBuffer, info: string | ArrayBuffer, + outputByteSize: number, algorithm: 'sha256' | 'sha512') => Promise + hkdfExpand: (prk: ArrayBuffer, info: string | ArrayBuffer, outputByteSize: number, + algorithm: 'sha256' | 'sha512') => Promise; hash: (value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512' | 'md5') => Promise; hmac: (value: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512') => Promise; compare: (a: ArrayBuffer, b: ArrayBuffer) => Promise; diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 2037a57d85..df44ef7789 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -182,8 +182,8 @@ export class CryptoService implements CryptoServiceAbstraction { throw new Error('No public key available.'); } const keyFingerprint = await this.cryptoFunctionService.hash(publicKey, 'sha256'); - const userFingerprint = await this.hkdfExpand(keyFingerprint, Utils.fromUtf8ToArray(userId), 32); - return this.hashPhrase(userFingerprint.buffer); + const userFingerprint = await this.cryptoFunctionService.hkdfExpand(keyFingerprint, userId, 32, 'sha256'); + return this.hashPhrase(userFingerprint); } @sequentialize(() => 'getOrgKeys') @@ -679,28 +679,13 @@ export class CryptoService implements CryptoServiceAbstraction { private async stretchKey(key: SymmetricCryptoKey): Promise { 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); + const encKey = await this.cryptoFunctionService.hkdfExpand(key.key, 'enc', 32, 'sha256'); + const macKey = await this.cryptoFunctionService.hkdfExpand(key.key, 'mac', 32, 'sha256'); + newKey.set(new Uint8Array(encKey)); + newKey.set(new Uint8Array(macKey), 32); return new SymmetricCryptoKey(newKey.buffer); } - // 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); - previousT = new Uint8Array(await this.cryptoFunctionService.hmac(t.buffer, prk, 'sha256')); - okm.set(previousT, i * hashLen); - } - return okm; - } - private async hashPhrase(hash: ArrayBuffer, minimumEntropy: number = 64) { const entropyPerWord = Math.log(EEFLongWordList.length) / Math.log(2); let numWords = Math.ceil(minimumEntropy / entropyPerWord); diff --git a/src/services/nodeCryptoFunction.service.ts b/src/services/nodeCryptoFunction.service.ts index d88be72d99..9e8d9131e6 100644 --- a/src/services/nodeCryptoFunction.service.ts +++ b/src/services/nodeCryptoFunction.service.ts @@ -26,6 +26,46 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { }); } + // ref: https://tools.ietf.org/html/rfc5869 + async hkdf(ikm: ArrayBuffer, salt: string | ArrayBuffer, info: string | ArrayBuffer, + outputByteSize: number, algorithm: 'sha256' | 'sha512'): Promise { + const saltBuf = this.toArrayBuffer(salt); + const prk = await this.hmac(ikm, saltBuf, algorithm); + return this.hkdfExpand(prk, info, outputByteSize, algorithm); + } + + // ref: https://tools.ietf.org/html/rfc5869 + async hkdfExpand(prk: ArrayBuffer, info: string | ArrayBuffer, outputByteSize: number, + algorithm: 'sha256' | 'sha512'): Promise { + const hashLen = algorithm === 'sha256' ? 32 : 64; + if (outputByteSize > 255 * hashLen) { + throw new Error('outputByteSize is too large.'); + } + const prkArr = new Uint8Array(prk); + if (prkArr.length < hashLen) { + throw new Error('prk is too small.'); + } + const infoBuf = this.toArrayBuffer(info); + const infoArr = new Uint8Array(infoBuf); + let runningOkmLength = 0; + let previousT = new Uint8Array(0); + const n = Math.ceil(outputByteSize / hashLen); + const okm = new Uint8Array(n * hashLen); + for (let i = 0; i < n; i++) { + const t = new Uint8Array(previousT.length + infoArr.length + 1); + t.set(previousT); + t.set(infoArr, previousT.length); + t.set([i + 1], t.length - 1); + previousT = new Uint8Array(await this.hmac(t.buffer, prk, algorithm)); + okm.set(previousT, runningOkmLength); + runningOkmLength += previousT.length; + if (runningOkmLength >= outputByteSize) { + break; + } + } + return okm.slice(0, outputByteSize).buffer; + } + hash(value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512' | 'md5'): Promise { const nodeValue = this.toNodeValue(value); const hash = crypto.createHash(algorithm); @@ -196,8 +236,14 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { return Buffer.from(new Uint8Array(value) as any); } - private toArrayBuffer(buf: Buffer): ArrayBuffer { - return new Uint8Array(buf).buffer; + private toArrayBuffer(value: Buffer | string | ArrayBuffer): ArrayBuffer { + let buf: ArrayBuffer; + if (typeof (value) === 'string') { + buf = Utils.fromUtf8ToArray(value).buffer; + } else { + buf = new Uint8Array(value).buffer; + } + return buf; } private toPemPrivateKey(key: ArrayBuffer): string { diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index a651d778d3..006102ebd0 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -49,6 +49,55 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return await this.subtle.deriveBits(pbkdf2Params, impKey, wcLen); } + async hkdf(ikm: ArrayBuffer, salt: string | ArrayBuffer, info: string | ArrayBuffer, + outputByteSize: number, algorithm: 'sha256' | 'sha512'): Promise { + const saltBuf = this.toBuf(salt); + const infoBuf = this.toBuf(info); + + const hkdfParams: HkdfParams = { + name: 'HKDF', + salt: saltBuf, + info: infoBuf, + hash: { name: this.toWebCryptoAlgorithm(algorithm) }, + }; + + const impKey = await this.subtle.importKey('raw', ikm, { name: 'HKDF' } as any, + false, ['deriveBits']); + return await this.subtle.deriveBits(hkdfParams as any, impKey, outputByteSize * 8); + } + + // ref: https://tools.ietf.org/html/rfc5869 + async hkdfExpand(prk: ArrayBuffer, info: string | ArrayBuffer, outputByteSize: number, + algorithm: 'sha256' | 'sha512'): Promise { + const hashLen = algorithm === 'sha256' ? 32 : 64; + if (outputByteSize > 255 * hashLen) { + throw new Error('outputByteSize is too large.'); + } + const prkArr = new Uint8Array(prk); + if (prkArr.length < hashLen) { + throw new Error('prk is too small.'); + } + const infoBuf = this.toBuf(info); + const infoArr = new Uint8Array(infoBuf); + let runningOkmLength = 0; + let previousT = new Uint8Array(0); + const n = Math.ceil(outputByteSize / hashLen); + const okm = new Uint8Array(n * hashLen); + for (let i = 0; i < n; i++) { + const t = new Uint8Array(previousT.length + infoArr.length + 1); + t.set(previousT); + t.set(infoArr, previousT.length); + t.set([i + 1], t.length - 1); + previousT = new Uint8Array(await this.hmac(t.buffer, prk, algorithm)); + okm.set(previousT, runningOkmLength); + runningOkmLength += previousT.length; + if (runningOkmLength >= outputByteSize) { + break; + } + } + return okm.slice(0, outputByteSize).buffer; + } + async hash(value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512' | 'md5'): Promise { if ((this.isIE && algorithm === 'sha1') || algorithm === 'md5') { const md = algorithm === 'md5' ? forge.md.md5.create() : forge.md.sha1.create();