1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 09:13:33 +00:00

hkdf crypto functions (#190)

* hkdf crypto functions

* comment to spec
This commit is contained in:
Kyle Spearrin
2020-10-29 15:52:12 -04:00
committed by GitHub
parent 76c09641ba
commit 8cb5a9f505
6 changed files with 283 additions and 23 deletions

View File

@@ -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<ArrayBuffer>;
hkdf: (ikm: ArrayBuffer, salt: string | ArrayBuffer, info: string | ArrayBuffer,
outputByteSize: number, algorithm: 'sha256' | 'sha512') => Promise<ArrayBuffer>
hkdfExpand: (prk: ArrayBuffer, info: string | ArrayBuffer, outputByteSize: number,
algorithm: 'sha256' | 'sha512') => Promise<ArrayBuffer>;
hash: (value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512' | 'md5') => Promise<ArrayBuffer>;
hmac: (value: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512') => Promise<ArrayBuffer>;
compare: (a: ArrayBuffer, b: ArrayBuffer) => Promise<boolean>;

View File

@@ -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<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);
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);

View File

@@ -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<ArrayBuffer> {
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<ArrayBuffer> {
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<ArrayBuffer> {
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 {

View File

@@ -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<ArrayBuffer> {
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<ArrayBuffer> {
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<ArrayBuffer> {
if ((this.isIE && algorithm === 'sha1') || algorithm === 'md5') {
const md = algorithm === 'md5' ? forge.md.md5.create() : forge.md.sha1.create();