Skip to content

Instantly share code, notes, and snippets.

@littledivy
Last active January 30, 2022 06:28
Show Gist options
  • Save littledivy/2d5ed5939811a130edd760421cb5038f to your computer and use it in GitHub Desktop.
Save littledivy/2d5ed5939811a130edd760421cb5038f to your computer and use it in GitHub Desktop.
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment