diff --git a/README.md b/README.md
index fb577b2..7bdb44b 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,7 @@ npm install @peculiar/webcrypto
| AES-KW | X | | X | | | X | |
| ECDSA1 | X | | X | X | | | |
| ECDH1 | X | | X | | | | X |
+| HKDF | | | X | | | | X |
| PBKDF2 | | | X | | | | X |
| DES-CBC2| X | | X | | X | X | |
| DES-EDE3-CBC2| X | | X | | X | X | |
diff --git a/package-lock.json b/package-lock.json
index df6979e..29a3bcc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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"
diff --git a/package.json b/package.json
index cf943ee..8fcc210 100644
--- a/package.json
+++ b/package.json
@@ -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": [
diff --git a/src/mechs/hkdf/hkdf.ts b/src/mechs/hkdf/hkdf.ts
new file mode 100644
index 0000000..7da2545
--- /dev/null
+++ b/src/mechs/hkdf/hkdf.ts
@@ -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 {
+ 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 {
+ 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 {
+ 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 {
+
+ 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,
+ },
+ ],
+ },
+ },
+ ]);
+});