Skip to content

Instantly share code, notes, and snippets.

@littledivy
Last active January 30, 2022 06:28
Show Gist options
  • Select an option

  • Save littledivy/2d5ed5939811a130edd760421cb5038f to your computer and use it in GitHub Desktop.

Select an option

Save littledivy/2d5ed5939811a130edd760421cb5038f to your computer and use it in GitHub Desktop.

Revisions

  1. littledivy renamed this gist Jan 30, 2022. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  2. littledivy renamed this gist Jan 30, 2022. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  3. littledivy revised this gist Jan 30, 2022. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion readme.md
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    Run this several times to reproduce the bug:
    ```
    $ deno run main.js
    $ deno run wrapKey_unwrapKey_flaky.js
    ```
  4. littledivy created this gist Jan 30, 2022.
    4 changes: 4 additions & 0 deletions readme.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,4 @@
    Run this several times to reproduce the bug:
    ```
    $ deno run main.js
    ```
    262 changes: 262 additions & 0 deletions wrapKey_unwrapKey_flaky.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,262 @@
    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;
    }
    }
    }
    }