Run this several times to reproduce the bug:
$ deno run wrapKey_unwrapKey_flaky.js
| const subtle = crypto.subtle; | |
| function generateEcdhPeerKey() { | |
| return subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, [ | |
| "deriveBits", | |
| ]).then((k) => k.publicKey); | |
| } | |
| const wrappers = []; | |
| const keys = []; | |
| function generateWrappingKeys() { | |
| // There are five algorithms that can be used for wrapKey/unwrapKey. | |
| // Generate one key with typical parameters for each kind. | |
| // | |
| // Note: we don't need cryptographically strong parameters for things | |
| // like IV - just any legal value will do. | |
| var parameters = [ | |
| { | |
| name: "RSA-OAEP", | |
| generateParameters: { | |
| name: "RSA-OAEP", | |
| modulusLength: 4096, | |
| publicExponent: new Uint8Array([1, 0, 1]), | |
| hash: "SHA-256", | |
| }, | |
| wrapParameters: { name: "RSA-OAEP", label: new Uint8Array(8) }, | |
| }, | |
| { | |
| name: "AES-CTR", | |
| generateParameters: { name: "AES-CTR", length: 128 }, | |
| wrapParameters: { | |
| name: "AES-CTR", | |
| counter: new Uint8Array(16), | |
| length: 64, | |
| }, | |
| }, | |
| { | |
| name: "AES-CBC", | |
| generateParameters: { name: "AES-CBC", length: 128 }, | |
| wrapParameters: { name: "AES-CBC", iv: new Uint8Array(16) }, | |
| }, | |
| { | |
| name: "AES-GCM", | |
| generateParameters: { name: "AES-GCM", length: 128 }, | |
| wrapParameters: { | |
| name: "AES-GCM", | |
| iv: new Uint8Array(16), | |
| additionalData: new Uint8Array(16), | |
| tagLength: 64, | |
| }, | |
| }, | |
| { | |
| name: "AES-KW", | |
| generateParameters: { name: "AES-KW", length: 128 }, | |
| wrapParameters: { name: "AES-KW" }, | |
| }, | |
| ]; | |
| return Promise.all(parameters.map(function (params) { | |
| return subtle.generateKey(params.generateParameters, true, [ | |
| "wrapKey", | |
| "unwrapKey", | |
| ]) | |
| .then(function (key) { | |
| var wrapper; | |
| if (params.name === "RSA-OAEP") { // we have a key pair, not just a key | |
| wrapper = { | |
| wrappingKey: key.publicKey, | |
| unwrappingKey: key.privateKey, | |
| parameters: params, | |
| }; | |
| } else { | |
| wrapper = { | |
| wrappingKey: key, | |
| unwrappingKey: key, | |
| parameters: params, | |
| }; | |
| } | |
| wrappers.push(wrapper); | |
| return true; | |
| }); | |
| })); | |
| } | |
| function generateKeysToWrap() { | |
| var parameters = [ | |
| { | |
| algorithm: { | |
| name: "RSASSA-PKCS1-v1_5", | |
| modulusLength: 1024, | |
| publicExponent: new Uint8Array([1, 0, 1]), | |
| hash: "SHA-256", | |
| }, | |
| privateUsages: ["sign"], | |
| publicUsages: ["verify"], | |
| }, | |
| { | |
| algorithm: { | |
| name: "RSA-PSS", | |
| modulusLength: 1024, | |
| publicExponent: new Uint8Array([1, 0, 1]), | |
| hash: "SHA-256", | |
| }, | |
| privateUsages: ["sign"], | |
| publicUsages: ["verify"], | |
| }, | |
| { | |
| algorithm: { | |
| name: "RSA-OAEP", | |
| modulusLength: 1024, | |
| publicExponent: new Uint8Array([1, 0, 1]), | |
| hash: "SHA-256", | |
| }, | |
| privateUsages: ["decrypt"], | |
| publicUsages: ["encrypt"], | |
| }, | |
| { | |
| algorithm: { name: "ECDSA", namedCurve: "P-256" }, | |
| privateUsages: ["sign"], | |
| publicUsages: ["verify"], | |
| }, | |
| { | |
| algorithm: { name: "ECDH", namedCurve: "P-256" }, | |
| privateUsages: ["deriveBits"], | |
| publicUsages: [], | |
| }, | |
| { | |
| algorithm: { name: "AES-CTR", length: 128 }, | |
| usages: ["encrypt", "decrypt"], | |
| }, | |
| { | |
| algorithm: { name: "AES-CBC", length: 128 }, | |
| usages: ["encrypt", "decrypt"], | |
| }, | |
| { | |
| algorithm: { name: "AES-GCM", length: 128 }, | |
| usages: ["encrypt", "decrypt"], | |
| }, | |
| { | |
| algorithm: { name: "AES-KW", length: 128 }, | |
| usages: ["wrapKey", "unwrapKey"], | |
| }, | |
| { | |
| algorithm: { name: "HMAC", length: 128, hash: "SHA-256" }, | |
| usages: ["sign", "verify"], | |
| }, | |
| ]; | |
| return Promise.all(parameters.map(function (params) { | |
| var usages; | |
| if ("usages" in params) { | |
| usages = params.usages; | |
| } else { | |
| usages = params.publicUsages.concat(params.privateUsages); | |
| } | |
| return subtle.generateKey(params.algorithm, true, usages) | |
| .then(function (result) { | |
| if (result.constructor === CryptoKey) { | |
| keys.push({ | |
| name: params.algorithm.name, | |
| algorithm: params.algorithm, | |
| usages: params.usages, | |
| key: result, | |
| }); | |
| } else { | |
| keys.push({ | |
| name: params.algorithm.name + " public key", | |
| algorithm: params.algorithm, | |
| usages: params.publicUsages, | |
| key: result.publicKey, | |
| }); | |
| keys.push({ | |
| name: params.algorithm.name + " private key", | |
| algorithm: params.algorithm, | |
| usages: params.privateUsages, | |
| key: result.privateKey, | |
| }); | |
| } | |
| return true; | |
| }); | |
| })); | |
| } | |
| // RSA-OAEP can only wrap relatively small payloads. AES-KW can only | |
| // wrap payloads a multiple of 8 bytes long. | |
| function wrappingIsPossible(exportedKey, algorithmName) { | |
| if ("byteLength" in exportedKey && algorithmName === "AES-KW") { | |
| return exportedKey.byteLength % 8 === 0; | |
| } | |
| if ("byteLength" in exportedKey && algorithmName === "RSA-OAEP") { | |
| // RSA-OAEP can only encrypt payloads with lengths shorter | |
| // than modulusLength - 2*hashLength - 1 bytes long. For | |
| // a 4096 bit modulus and SHA-256, that comes to | |
| // 4096/8 - 2*(256/8) - 1 = 512 - 2*32 - 1 = 447 bytes. | |
| return exportedKey.byteLength <= 446; | |
| } | |
| if ("kty" in exportedKey && algorithmName === "AES-KW") { | |
| return JSON.stringify(exportedKey).length % 8 == 0; | |
| } | |
| if ("kty" in exportedKey && algorithmName === "RSA-OAEP") { | |
| return JSON.stringify(exportedKey).length <= 478; | |
| } | |
| return true; | |
| } | |
| const ecdhPeerKey = await generateEcdhPeerKey(); | |
| await generateWrappingKeys(); | |
| await generateKeysToWrap(); | |
| for (const wrapper of wrappers) { | |
| for (const key of keys) { | |
| var formats; | |
| if (key.name.includes("private")) { | |
| formats = ["pkcs8", "jwk"]; | |
| } else if (key.name.includes("public")) { | |
| formats = ["spki", "jwk"]; | |
| } else { | |
| formats = ["raw", "jwk"]; | |
| } | |
| console.log(`Wrapping ${key.name} with ${wrapper.parameters.name};`); | |
| for (const format of formats) { | |
| console.log(` ${format}`); | |
| try { | |
| const exportedKey = await subtle.exportKey(format, key.key); | |
| if (!wrappingIsPossible(exportedKey, wrapper.parameters.name)) { | |
| console.log(` Skipping ${format} format for ${key.name}`); | |
| continue; | |
| } | |
| const wrappedResult = await subtle.wrapKey( | |
| format, | |
| key.key, | |
| wrapper.wrappingKey, | |
| wrapper.parameters.wrapParameters, | |
| ); | |
| // This is flaky. | |
| await subtle.unwrapKey( | |
| format, | |
| wrappedResult, | |
| wrapper.unwrappingKey, | |
| wrapper.parameters.wrapParameters, | |
| key.algorithm, | |
| false, | |
| key.usages, | |
| ); | |
| } catch (e) { | |
| if (e.message.includes("Initialization vector")) { | |
| continue; | |
| } | |
| if (e.message.includes("expected private key")) { | |
| continue; | |
| } | |
| throw e; | |
| } | |
| } | |
| } | |
| } |
Run this several times to reproduce the bug:
$ deno run wrapKey_unwrapKey_flaky.js