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