diff --git a/cli/tests/unit/webcrypto_test.ts b/cli/tests/unit/webcrypto_test.ts index 0e4b17b9ae..a37e32eba1 100644 --- a/cli/tests/unit/webcrypto_test.ts +++ b/cli/tests/unit/webcrypto_test.ts @@ -243,22 +243,43 @@ unitTest(async function testSignRSASSAKey() { assert(signature); }); +// deno-fmt-ignore +const rawKey = new Uint8Array([ + 1, 2, 3, 4, 5, 6, 7, 8, + 9, 10, 11, 12, 13, 14, 15, 16 +]); + +const jwk: JsonWebKey = { + kty: "oct", + // unpadded base64 for rawKey. + k: "AQIDBAUGBwgJCgsMDQ4PEA", + alg: "HS256", +}; + unitTest(async function subtleCryptoHmacImportExport() { - // deno-fmt-ignore - const rawKey = new Uint8Array([ - 1, 2, 3, 4, 5, 6, 7, 8, - 9, 10, 11, 12, 13, 14, 15, 16 - ]); - const key = await crypto.subtle.importKey( + const key1 = await crypto.subtle.importKey( "raw", rawKey, { name: "HMAC", hash: "SHA-256" }, true, ["sign"], ); - const actual = await crypto.subtle.sign( + const key2 = await crypto.subtle.importKey( + "jwk", + jwk, + { name: "HMAC", hash: "SHA-256" }, + true, + ["sign"], + ); + const actual1 = await crypto.subtle.sign( { name: "HMAC" }, - key, + key1, + new Uint8Array([1, 2, 3, 4]), + ); + + const actual2 = await crypto.subtle.sign( + { name: "HMAC" }, + key2, new Uint8Array([1, 2, 3, 4]), ); // deno-fmt-ignore @@ -269,10 +290,14 @@ unitTest(async function subtleCryptoHmacImportExport() { 23, 122, 222, 1, 146, 46, 182, 87, ]); assertEquals( - new Uint8Array(actual), + new Uint8Array(actual1), expected, ); - - const exportedKey = await crypto.subtle.exportKey("raw", key); + assertEquals( + new Uint8Array(actual2), + expected, + ); + // TODO(@littledivy): Add a test for exporting JWK key when supported. + const exportedKey = await crypto.subtle.exportKey("raw", key1); assertEquals(new Uint8Array(exportedKey), rawKey); }); diff --git a/ext/crypto/00_crypto.js b/ext/crypto/00_crypto.js index 5c80ac0ca6..375170ad9c 100644 --- a/ext/crypto/00_crypto.js +++ b/ext/crypto/00_crypto.js @@ -12,13 +12,18 @@ const core = window.Deno.core; const webidl = window.__bootstrap.webidl; const { DOMException } = window.__bootstrap.domException; + const { atob } = window.__bootstrap.base64; const { ArrayPrototypeFind, - ArrayBufferIsView, + ArrayPrototypeEvery, ArrayPrototypeIncludes, + ArrayBuffer, + ArrayBufferIsView, BigInt64Array, StringPrototypeToUpperCase, + StringPrototypeReplace, + StringPrototypeCharCodeAt, Symbol, SymbolFor, SymbolToStringTag, @@ -100,6 +105,23 @@ }, }; + // Decodes the unpadded base64 to the octet sequence containing key value `k` defined in RFC7518 Section 6.4 + function decodeSymmetricKey(key) { + // Decode from base64url without `=` padding. + const base64 = StringPrototypeReplace( + StringPrototypeReplace(key, /\-/g, "+"), + /\_/g, + "/", + ); + const decodedKey = atob(base64); + const keyLength = decodedKey.length; + const keyBytes = new Uint8Array(keyLength); + for (let i = 0; i < keyLength; i++) { + keyBytes[i] = StringPrototypeCharCodeAt(decodedKey, i); + } + return keyBytes; + } + // See https://www.w3.org/TR/WebCryptoAPI/#dfn-normalize-an-algorithm // 18.4.4 function normalizeAlgorithm(algorithm, op) { @@ -631,7 +653,7 @@ prefix, context: "Argument 1", }); - keyData = webidl.converters.BufferSource(keyData, { + keyData = webidl.converters["BufferSource or JsonWebKey"](keyData, { prefix, context: "Argument 2", }); @@ -649,22 +671,32 @@ }); // 2. - if (ArrayBufferIsView(keyData)) { - keyData = new Uint8Array( - keyData.buffer, - keyData.byteOffset, - keyData.byteLength, - ); + if (format !== "jwk") { + if (ArrayBufferIsView(keyData) || keyData instanceof ArrayBuffer) { + if (ArrayBufferIsView(keyData)) { + keyData = new Uint8Array( + keyData.buffer, + keyData.byteOffset, + keyData.byteLength, + ); + } else { + keyData = new Uint8Array(keyData); + } + keyData = TypedArrayPrototypeSlice(keyData); + } else { + throw new TypeError("keyData is a JsonWebKey"); + } } else { - keyData = new Uint8Array(keyData); + if (ArrayBufferIsView(keyData) || keyData instanceof ArrayBuffer) { + throw new TypeError("keyData is not a JsonWebKey"); + } } - keyData = TypedArrayPrototypeSlice(keyData); const normalizedAlgorithm = normalizeAlgorithm(algorithm, "importKey"); switch (normalizedAlgorithm.name) { - // https://w3c.github.io/webcrypto/#hmac-operations case "HMAC": { + // 2. if ( ArrayPrototypeFind( keyUsages, @@ -674,59 +706,177 @@ throw new DOMException("Invalid key usages", "SyntaxError"); } + // 3. + let hash; + let data; + + // 4. https://w3c.github.io/webcrypto/#hmac-operations switch (format) { case "raw": { - const hash = normalizedAlgorithm.hash; - // 5. - let length = keyData.byteLength * 8; - // 6. - if (length === 0) { - throw new DOMException("Key length is zero", "DataError"); + data = keyData; + hash = normalizedAlgorithm.hash; + break; + } + case "jwk": { + // TODO(@littledivy): Why does the spec validate JWK twice? + const jwk = keyData; + // 2. + if (jwk.kty !== "oct") { + throw new DOMException( + "`kty` member of JsonWebKey must be `oct`", + "DataError", + ); } - if (normalizeAlgorithm.length) { - // 7. + + // Section 6.4.1 of RFC7518 + if (!jwk.k) { + throw new DOMException( + "`k` member of JsonWebKey must be present", + "DataError", + ); + } + + // 4. + data = decodeSymmetricKey(jwk.k); + // 5. + hash = normalizedAlgorithm.hash; + // 6. + switch (hash.name) { + case "SHA-1": { + if (jwk.alg !== undefined && jwk.alg !== "HS1") { + throw new DOMException( + "`alg` member of JsonWebKey must be `HS1`", + "DataError", + ); + } + break; + } + case "SHA-256": { + if (jwk.alg !== undefined && jwk.alg !== "HS256") { + throw new DOMException( + "`alg` member of JsonWebKey must be `HS256`", + "DataError", + ); + } + break; + } + case "SHA-384": { + if (jwk.alg !== undefined && jwk.alg !== "HS384") { + throw new DOMException( + "`alg` member of JsonWebKey must be `HS384`", + "DataError", + ); + } + break; + } + case "SHA-512": { + if (jwk.alg !== undefined && jwk.alg !== "HS512") { + throw new DOMException( + "`alg` member of JsonWebKey must be `HS512`", + "DataError", + ); + } + break; + } + default: + throw new TypeError("unreachable"); + } + + // 7. + if (keyUsages.length > 0 && jwk.use && jwk.use !== "sign") { + throw new DOMException( + "`use` member of JsonWebKey must be `sign`", + "DataError", + ); + } + + // 8. + // Section 4.3 of RFC7517 + if (jwk.key_ops) { if ( - normalizedAlgorithm.length > length || - normalizedAlgorithm.length <= (length - 8) + ArrayPrototypeFind( + jwk.key_ops, + (u) => !ArrayPrototypeIncludes(recognisedUsages, u), + ) !== undefined ) { throw new DOMException( - "Key length is invalid", + "`key_ops` member of JsonWebKey is invalid", + "DataError", + ); + } + + if ( + !ArrayPrototypeEvery( + jwk.key_ops, + (u) => ArrayPrototypeIncludes(keyUsages, u), + ) + ) { + throw new DOMException( + "`key_ops` member of JsonWebKey is invalid", "DataError", ); } - length = normalizeAlgorithm.length; } - if (keyUsages.length == 0) { - throw new DOMException("Key usage is empty", "SyntaxError"); + // 9. + if (jwk.ext === false && extractable == true) { + throw new DOMException( + "`ext` member of JsonWebKey is invalid", + "DataError", + ); } - const handle = {}; - WeakMapPrototypeSet(KEY_STORE, handle, { - type: "raw", - data: keyData, - }); - - const algorithm = { - name: "HMAC", - length, - hash, - }; - - const key = constructKey( - "secret", - extractable, - usageIntersection(keyUsages, recognisedUsages), - algorithm, - handle, - ); - - return key; + break; } - // TODO(@littledivy): jwk default: throw new DOMException("Not implemented", "NotSupportedError"); } + + // 5. + let length = data.byteLength * 8; + // 6. + if (length === 0) { + throw new DOMException("Key length is zero", "DataError"); + } + // 7. + if (normalizedAlgorithm.length !== undefined) { + if ( + normalizedAlgorithm.length > length || + normalizedAlgorithm.length <= (length - 8) + ) { + throw new DOMException( + "Key length is invalid", + "DataError", + ); + } + length = normalizedAlgorithm.length; + } + + if (keyUsages.length == 0) { + throw new DOMException("Key usage is empty", "SyntaxError"); + } + + const handle = {}; + WeakMapPrototypeSet(KEY_STORE, handle, { + type: "raw", + data, + }); + + const algorithm = { + name: "HMAC", + length, + hash, + }; + + const key = constructKey( + "secret", + extractable, + usageIntersection(keyUsages, recognisedUsages), + algorithm, + handle, + ); + + return key; } // TODO(@littledivy): RSASSA-PKCS1-v1_5 // TODO(@littledivy): RSA-PSS diff --git a/ext/crypto/01_webidl.js b/ext/crypto/01_webidl.js index 43bc5e822e..e781e43347 100644 --- a/ext/crypto/01_webidl.js +++ b/ext/crypto/01_webidl.js @@ -9,6 +9,7 @@ ((window) => { const webidl = window.__bootstrap.webidl; const { CryptoKey } = window.__bootstrap.crypto; + const { ArrayBufferIsView, ArrayBuffer } = window.__bootstrap.primordials; webidl.converters.AlgorithmIdentifier = (V, opts) => { // Union for (object or DOMString) @@ -18,6 +19,14 @@ return webidl.converters.DOMString(V, opts); }; + webidl.converters["BufferSource or JsonWebKey"] = (V, opts) => { + // Union for (BufferSource or JsonWebKey) + if (ArrayBufferIsView(V) || V instanceof ArrayBuffer) { + return webidl.converters.BufferSource(V, opts); + } + return webidl.converters.JsonWebKey(V, opts); + }; + webidl.converters.KeyType = webidl.createEnumConverter("KeyType", [ "public", "private", @@ -178,6 +187,115 @@ webidl.converters.HmacImportParams = webidl .createDictionaryConverter("HmacImportParams", dictHmacImportParams); + const dictRsaOtherPrimesInfo = [ + { + key: "r", + converter: webidl.converters["DOMString"], + }, + { + key: "d", + converter: webidl.converters["DOMString"], + }, + { + key: "t", + converter: webidl.converters["DOMString"], + }, + ]; + + webidl.converters.RsaOtherPrimesInfo = webidl.createDictionaryConverter( + "RsaOtherPrimesInfo", + dictRsaOtherPrimesInfo, + ); + webidl.converters["sequence"] = webidl + .createSequenceConverter( + webidl.converters.RsaOtherPrimesInfo, + ); + + const dictJsonWebKey = [ + // Sections 4.2 and 4.3 of RFC7517. + // https://datatracker.ietf.org/doc/html/rfc7517#section-4 + { + key: "kty", + converter: webidl.converters["DOMString"], + }, + { + key: "use", + converter: webidl.converters["DOMString"], + }, + { + key: "key_ops", + converter: webidl.converters["sequence"], + }, + { + key: "alg", + converter: webidl.converters["DOMString"], + }, + // JSON Web Key Parameters Registration + { + key: "ext", + converter: webidl.converters["boolean"], + }, + // Section 6 of RFC7518 JSON Web Algorithms + // https://datatracker.ietf.org/doc/html/rfc7518#section-6 + { + key: "crv", + converter: webidl.converters["DOMString"], + }, + { + key: "x", + converter: webidl.converters["DOMString"], + }, + { + key: "y", + converter: webidl.converters["DOMString"], + }, + { + key: "d", + converter: webidl.converters["DOMString"], + }, + { + key: "n", + converter: webidl.converters["DOMString"], + }, + { + key: "e", + converter: webidl.converters["DOMString"], + }, + { + key: "p", + converter: webidl.converters["DOMString"], + }, + { + key: "q", + converter: webidl.converters["DOMString"], + }, + { + key: "dp", + converter: webidl.converters["DOMString"], + }, + { + key: "dq", + converter: webidl.converters["DOMString"], + }, + { + key: "qi", + converter: webidl.converters["DOMString"], + }, + { + key: "oth", + converter: webidl.converters["sequence"], + }, + { + key: "k", + converter: webidl.converters["DOMString"], + }, + ]; + + webidl.converters.JsonWebKey = webidl.createDictionaryConverter( + "JsonWebKey", + dictJsonWebKey, + ); + const dictPbkdf2Params = [ ...dictAlgorithm, { diff --git a/ext/crypto/lib.deno_crypto.d.ts b/ext/crypto/lib.deno_crypto.d.ts index 6b19016117..2e8d2f8b2b 100644 --- a/ext/crypto/lib.deno_crypto.d.ts +++ b/ext/crypto/lib.deno_crypto.d.ts @@ -28,6 +28,34 @@ type KeyUsage = type NamedCurve = string; +interface RsaOtherPrimesInfo { + d?: string; + r?: string; + t?: string; +} + +interface JsonWebKey { + alg?: string; + crv?: string; + d?: string; + dp?: string; + dq?: string; + e?: string; + ext?: boolean; + k?: string; + // deno-lint-ignore camelcase + key_ops?: string[]; + kty?: string; + n?: string; + oth?: RsaOtherPrimesInfo[]; + p?: string; + q?: string; + qi?: string; + use?: string; + x?: string; + y?: string; +} + interface HmacKeyGenParams extends Algorithm { hash: HashAlgorithmIdentifier; length?: number; @@ -122,6 +150,13 @@ interface SubtleCrypto { extractable: boolean, keyUsages: KeyUsage[], ): Promise; + importKey( + format: "jwk", + keyData: JsonWebKey, + algorithm: AlgorithmIdentifier | HmacImportParams, + extractable: boolean, + keyUsages: KeyUsage[], + ): Promise; importKey( format: "raw", keyData: BufferSource, diff --git a/tools/wpt/expectation.json b/tools/wpt/expectation.json index e71d06c10b..39786ff8e7 100644 --- a/tools/wpt/expectation.json +++ b/tools/wpt/expectation.json @@ -14041,42 +14041,6 @@ "Good parameters: 256 bits (jwk, {alg: A256KW, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {name: AES-KW}, true, [unwrapKey])", "Good parameters: 256 bits (raw, {0: 1, 1: 2, 10: 11, 11: 12, 12: 13, 13: 14, 14: 15, 15: 16, 16: 17, 17: 18, 18: 19, 19: 20, 2: 3, 20: 21, 21: 22, 22: 23, 23: 24, 24: 25, 25: 26, 26: 27, 27: 28, 28: 29, 29: 30, 3: 4, 30: 31, 31: 32, 4: 5, 5: 6, 6: 7, 7: 8, 8: 9, 9: 10}, {name: AES-KW}, false, [unwrapKey])", "Good parameters: 256 bits (jwk, {alg: A256KW, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {name: AES-KW}, false, [unwrapKey])", - "Good parameters: 128 bits (jwk, {alg: HS1, k: AQIDBAUGBwgJCgsMDQ4PEA, kty: oct}, {hash: SHA-1, name: HMAC}, false, [sign])", - "Good parameters: 128 bits (jwk, {alg: HS1, k: AQIDBAUGBwgJCgsMDQ4PEA, kty: oct}, {hash: SHA-1, name: HMAC}, false, [verify, sign])", - "Good parameters: 128 bits (jwk, {alg: HS1, k: AQIDBAUGBwgJCgsMDQ4PEA, kty: oct}, {hash: SHA-1, name: HMAC}, false, [verify])", - "Good parameters: 192 bits (jwk, {alg: HS1, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcY, kty: oct}, {hash: SHA-1, name: HMAC}, false, [sign])", - "Good parameters: 192 bits (jwk, {alg: HS1, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcY, kty: oct}, {hash: SHA-1, name: HMAC}, false, [verify, sign])", - "Good parameters: 192 bits (jwk, {alg: HS1, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcY, kty: oct}, {hash: SHA-1, name: HMAC}, false, [verify])", - "Good parameters: 256 bits (jwk, {alg: HS1, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {hash: SHA-1, name: HMAC}, false, [sign])", - "Good parameters: 256 bits (jwk, {alg: HS1, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {hash: SHA-1, name: HMAC}, false, [verify, sign])", - "Good parameters: 256 bits (jwk, {alg: HS1, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {hash: SHA-1, name: HMAC}, false, [verify])", - "Good parameters: 128 bits (jwk, {alg: HS256, k: AQIDBAUGBwgJCgsMDQ4PEA, kty: oct}, {hash: SHA-256, name: HMAC}, false, [sign])", - "Good parameters: 128 bits (jwk, {alg: HS256, k: AQIDBAUGBwgJCgsMDQ4PEA, kty: oct}, {hash: SHA-256, name: HMAC}, false, [verify, sign])", - "Good parameters: 128 bits (jwk, {alg: HS256, k: AQIDBAUGBwgJCgsMDQ4PEA, kty: oct}, {hash: SHA-256, name: HMAC}, false, [verify])", - "Good parameters: 192 bits (jwk, {alg: HS256, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcY, kty: oct}, {hash: SHA-256, name: HMAC}, false, [sign])", - "Good parameters: 192 bits (jwk, {alg: HS256, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcY, kty: oct}, {hash: SHA-256, name: HMAC}, false, [verify, sign])", - "Good parameters: 192 bits (jwk, {alg: HS256, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcY, kty: oct}, {hash: SHA-256, name: HMAC}, false, [verify])", - "Good parameters: 256 bits (jwk, {alg: HS256, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {hash: SHA-256, name: HMAC}, false, [sign])", - "Good parameters: 256 bits (jwk, {alg: HS256, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {hash: SHA-256, name: HMAC}, false, [verify, sign])", - "Good parameters: 256 bits (jwk, {alg: HS256, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {hash: SHA-256, name: HMAC}, false, [verify])", - "Good parameters: 128 bits (jwk, {alg: HS384, k: AQIDBAUGBwgJCgsMDQ4PEA, kty: oct}, {hash: SHA-384, name: HMAC}, false, [sign])", - "Good parameters: 128 bits (jwk, {alg: HS384, k: AQIDBAUGBwgJCgsMDQ4PEA, kty: oct}, {hash: SHA-384, name: HMAC}, false, [verify, sign])", - "Good parameters: 128 bits (jwk, {alg: HS384, k: AQIDBAUGBwgJCgsMDQ4PEA, kty: oct}, {hash: SHA-384, name: HMAC}, false, [verify])", - "Good parameters: 192 bits (jwk, {alg: HS384, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcY, kty: oct}, {hash: SHA-384, name: HMAC}, false, [sign])", - "Good parameters: 192 bits (jwk, {alg: HS384, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcY, kty: oct}, {hash: SHA-384, name: HMAC}, false, [verify, sign])", - "Good parameters: 192 bits (jwk, {alg: HS384, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcY, kty: oct}, {hash: SHA-384, name: HMAC}, false, [verify])", - "Good parameters: 256 bits (jwk, {alg: HS384, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {hash: SHA-384, name: HMAC}, false, [sign])", - "Good parameters: 256 bits (jwk, {alg: HS384, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {hash: SHA-384, name: HMAC}, false, [verify, sign])", - "Good parameters: 256 bits (jwk, {alg: HS384, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {hash: SHA-384, name: HMAC}, false, [verify])", - "Good parameters: 128 bits (jwk, {alg: HS512, k: AQIDBAUGBwgJCgsMDQ4PEA, kty: oct}, {hash: SHA-512, name: HMAC}, false, [sign])", - "Good parameters: 128 bits (jwk, {alg: HS512, k: AQIDBAUGBwgJCgsMDQ4PEA, kty: oct}, {hash: SHA-512, name: HMAC}, false, [verify, sign])", - "Good parameters: 128 bits (jwk, {alg: HS512, k: AQIDBAUGBwgJCgsMDQ4PEA, kty: oct}, {hash: SHA-512, name: HMAC}, false, [verify])", - "Good parameters: 192 bits (jwk, {alg: HS512, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcY, kty: oct}, {hash: SHA-512, name: HMAC}, false, [sign])", - "Good parameters: 192 bits (jwk, {alg: HS512, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcY, kty: oct}, {hash: SHA-512, name: HMAC}, false, [verify, sign])", - "Good parameters: 192 bits (jwk, {alg: HS512, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcY, kty: oct}, {hash: SHA-512, name: HMAC}, false, [verify])", - "Good parameters: 256 bits (jwk, {alg: HS512, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {hash: SHA-512, name: HMAC}, false, [sign])", - "Good parameters: 256 bits (jwk, {alg: HS512, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {hash: SHA-512, name: HMAC}, false, [verify, sign])", - "Good parameters: 256 bits (jwk, {alg: HS512, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {hash: SHA-512, name: HMAC}, false, [verify])", "Good parameters: 128 bits (raw, {0: 1, 1: 2, 10: 11, 11: 12, 12: 13, 13: 14, 14: 15, 15: 16, 2: 3, 3: 4, 4: 5, 5: 6, 6: 7, 7: 8, 8: 9, 9: 10}, {name: HKDF}, false, [deriveBits])", "Good parameters: 128 bits (raw, {0: 1, 1: 2, 10: 11, 11: 12, 12: 13, 13: 14, 14: 15, 15: 16, 2: 3, 3: 4, 4: 5, 5: 6, 6: 7, 7: 8, 8: 9, 9: 10}, {name: HKDF}, false, [deriveKey, deriveBits])", "Good parameters: 128 bits (raw, {0: 1, 1: 2, 10: 11, 11: 12, 12: 13, 13: 14, 14: 15, 15: 16, 2: 3, 3: 4, 4: 5, 5: 6, 6: 7, 7: 8, 8: 9, 9: 10}, {name: HKDF}, false, [deriveKey])",