Implement HKDF

This commit is contained in:
Liran Nuna 2019-03-02 12:06:33 -08:00
parent 04c1016124
commit 7d4786e09c
10 changed files with 191 additions and 11 deletions

View File

@ -43,6 +43,7 @@ npm install @peculiar/webcrypto
| AES-KW | X | | X | | | X | |
| ECDSA<sup>1</sup> | X | | X | X | | | |
| ECDH<sup>1</sup> | X | | X | | | | X |
| HKDF | | | X | | | | X |
| PBKDF2 | | | X | | | | X |
| DES-CBC<sup>2</sup>| X | | X | | X | X | |
| DES-EDE3-CBC<sup>2</sup>| X | | X | | X | X | |

20
package-lock.json generated
View File

@ -250,7 +250,14 @@
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-2.0.21.tgz",
"integrity": "sha512-yfRxOLyqiUvtn0pO7t//tdEet9ZWBWw5nrsHkdSfC5lLKOHYvhdPcULPD3KANSfGCOqHWXxndmOmXf0tZWjNPg==",
"requires": {
"pvutils": "latest"
"pvutils": "^1.0.17"
},
"dependencies": {
"pvutils": {
"version": "1.0.17",
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.0.17.tgz",
"integrity": "sha512-wLHYUQxWaXVQvKnwIDWFVKDJku9XDCvyhhxoq8dc5MFdIlRenyPI9eSfEtcvgHgD7FlvCyGAlWgOzRnZD99GZQ=="
}
}
},
"assert-plus": {
@ -2535,11 +2542,6 @@
"tslib": "^1.9.3"
}
},
"pvutils": {
"version": "1.0.16",
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.0.16.tgz",
"integrity": "sha512-+8C8cKbON+UH3GxGRdF0xHwiaVhq0+lsHcUgN/MZLCLtvcYhVP3fQPhrnkucK2v/RqRuq4pw1jjyF9PwW9f7mA=="
},
"qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
@ -3153,9 +3155,9 @@
}
},
"webcrypto-core": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.0.7.tgz",
"integrity": "sha512-6IVimwLJxNy7fa9yUduvR0qUcVCNE2t+HK9HRN3JAlqbhgRYoSQg8KtR7I0BwRST03kUG6fusYpn7r2feKNFkg==",
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.0.10.tgz",
"integrity": "sha512-nTc37SyfR05Lg/t45uIowbJtNXi5p06zV4WX8x+ZA6FSnCrRzdiAuk/rz1OjIcAKsyMRQw3hcmgFceoTxH9T3w==",
"requires": {
"pvtsutils": "^1.0.3",
"tslib": "^1.9.3"

View File

@ -56,7 +56,7 @@
"asn1js": "^2.0.21",
"pvtsutils": "^1.0.3",
"tslib": "^1.9.3",
"webcrypto-core": "^1.0.7"
"webcrypto-core": "^1.0.10"
},
"nyc": {
"extension": [

71
src/mechs/hkdf/hkdf.ts Normal file
View File

@ -0,0 +1,71 @@
import * as core from "webcrypto-core";
import { HmacCryptoKey } from "../hmac/key";
import { HkdfCryptoKey } from "./key";
import { BufferSourceConverter, CryptoKey } from "webcrypto-core";
import crypto from "crypto";
export class HkdfProvider extends core.HkdfProvider {
private normalizeHash(hash: HashAlgorithmIdentifier): Algorithm {
if (typeof hash === "string") {
hash = {name: hash};
}
this.checkHashAlgorithm(hash, this.hashAlgorithms);
return hash;
}
public async onImportKey(format: KeyFormat, keyData: ArrayBuffer, algorithm: HmacImportParams, extractable: boolean, keyUsages: KeyUsage[]): Promise<CryptoKey> {
if (format.toLowerCase() !== "raw") {
throw new core.OperationError("Operation not supported");
}
const key: HkdfCryptoKey = new HkdfCryptoKey();
key.data = Buffer.from(keyData);
key.algorithm = { name: this.name };
key.extractable = extractable;
key.usages = keyUsages;
return key;
}
public async onExportKey(format: KeyFormat, key: HmacCryptoKey): Promise<JsonWebKey | ArrayBuffer> {
switch (format.toLowerCase()) {
case "raw":
return new Uint8Array(key.data).buffer;
default:
throw new core.OperationError("format: Must be 'raw'");
}
}
public async onDeriveBits(params: HkdfParams, baseKey: HkdfCryptoKey, length: number): Promise<ArrayBuffer> {
const hash = this.normalizeHash(params.hash).name.replace("-", "");
const hashLength = crypto.createHash(hash).digest().length;
const byteLength = length / 8;
const info = BufferSourceConverter.toUint8Array(params.info);
const PRK = crypto.createHmac(hash, BufferSourceConverter.toUint8Array(params.salt))
.update(BufferSourceConverter.toUint8Array(baseKey.data))
.digest();
let blocks = [Buffer.alloc(0)];
const blockCount = Math.ceil(byteLength / hashLength) + 1; // Includes empty buffer
for (let i=1; i<blockCount; ++i) {
blocks.push(
crypto.createHmac(hash, PRK)
.update(Buffer.concat([blocks[i - 1], info, Buffer.from([i])]))
.digest()
);
}
return Buffer.concat(blocks).slice(0, byteLength);
}
public checkCryptoKey(key: CryptoKey, keyUsage?: KeyUsage) {
super.checkCryptoKey(key, keyUsage);
if (!(key instanceof HkdfCryptoKey)) {
throw new TypeError("key: Is not HKDF CryptoKey");
}
}
}

1
src/mechs/hkdf/index.ts Normal file
View File

@ -0,0 +1 @@
export * from "./hkdf";

8
src/mechs/hkdf/key.ts Normal file
View File

@ -0,0 +1,8 @@
import { CryptoKey } from "../../keys";
export class HkdfCryptoKey extends CryptoKey {
public data!: Buffer;
public algorithm!: KeyAlgorithm;
}

View File

@ -5,3 +5,4 @@ export * from "./ec";
export * from "./sha";
export * from "./pbkdf";
export * from "./hmac";
export * from "./hkdf";

View File

@ -4,7 +4,7 @@ import {
DesCbcProvider, DesEde3CbcProvider,
EcdhProvider, EcdsaProvider,
HmacProvider,
Pbkdf2Provider,
Pbkdf2Provider, HkdfProvider,
RsaOaepProvider, RsaPssProvider, RsaSsaProvider,
Sha1Provider, Sha256Provider, Sha384Provider, Sha512Provider,
} from "./mechs";
@ -50,5 +50,9 @@ export class SubtleCrypto extends core.SubtleCrypto {
//#region HMAC
this.providers.set(new HmacProvider());
//#endregion
//#region HKDF
this.providers.set(new HkdfProvider());
//#endregion
}
}

View File

@ -223,6 +223,11 @@ export function testCrypto(crypto: Crypto, params: ITestParams[]) {
action.extractable,
action.keyUsages);
// Can't continue if key is not exctractable.
if (!action.extractable) {
return;
}
const exportedData = await crypto.subtle.exportKey(
action.format,
importedKey);

87
test/hkdf.ts Normal file
View File

@ -0,0 +1,87 @@
import { Convert } from "pvtsutils";
import { Crypto } from "../src";
import { testCrypto } from "./helper";
context("HKDF", () => {
const crypto = new Crypto();
testCrypto(crypto, [
{
name: "HKDF",
actions: {
import: [
{
name: "raw",
format: "raw",
data: Buffer.from("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", "hex"),
algorithm: {
name: "HKDF",
},
extractable: false,
keyUsages: ["deriveBits", "deriveKey"],
},
],
deriveBits: [
{
key: {
format: "raw",
data: Buffer.from("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", "hex"),
algorithm: {
name: "HKDF",
},
extractable: false,
keyUsages: ["deriveBits"],
},
algorithm: {
name: "HKDF",
hash: {name: "SHA-256"},
salt: Buffer.from("000102030405060708090a0b0c", "hex"),
info: Buffer.from("f0f1f2f3f4f5f6f7f8f9", "hex"),
} as HkdfParams,
data: Buffer.from("3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865", "hex"),
length: 42 * 8,
},
{
key: {
format: "raw",
data: Buffer.from("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f", "hex"),
algorithm: {
name: "HKDF",
},
extractable: false,
keyUsages: ["deriveBits"],
},
algorithm: {
name: "HKDF",
hash: "SHA-256",
salt: Buffer.from("606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf", "hex"),
info: Buffer.from("b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", "hex"),
} as HkdfParams,
data: Buffer.from("b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87", "hex"),
length: 82 * 8,
},
{
key: {
format: "raw",
data: Buffer.from("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", "hex"),
algorithm: {
name: "HKDF",
},
extractable: false,
keyUsages: ["deriveBits"],
},
algorithm: {
name: "HKDF",
hash: "SHA-256",
salt: Buffer.from([]),
info: Buffer.from([]),
} as HkdfParams,
data: Buffer.from("8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8", "hex"),
length: 42 * 8,
},
],
},
},
]);
});