javascriptcryptographyelliptic-curvediffie-hellmansubtlecrypto

Javascript Web Crypto - Unable to import ECDH P-256 Public Key


I'm trying to exchange public keys between Browser and Server, and generate secret to be used for encryption of data. I'm trying to utilize ECDH (Elliptic Curve Diffie-Hellman). On the Server side I'm generating ECDH with prime256v1 algorithm. On the Browser side I'm generating ECDH with P-256 named curve. (these algorithms should be the same, they are just named differently, P-256 , also known as secp256r1 and prime256v1).

I'm able to pass Browser generated public key to the server as Base64 formatted string, and to generate secret using Server private key and Browser public key. And everything works fine on the Server side (import, generate secret, encryption).

But when I try to pass Server generated public key to the Browser as Base64 formatted string and try to import it, I get DOMException: Cannot create a key using the specified key usages.

const b64ToBin = (b64) => {
    const binaryString = window.atob(b64);
    const length = binaryString.length;
    const bytes = new Uint8Array(length);
    for (let i = 0; i < length; i++) {
        bytes[i] = binaryString.charCodeAt(i);
    }
    return bytes.buffer;
};

const importB64Key = async (base64key) => {
    const bin = b64ToBin(base64key);
    console.log('bin ', bin);
    const key = await window.crypto.subtle.importKey(
        'raw',
        bin,
        {
            name: 'ECDH',
            namedCurve: 'P-256',
        },
        true,
        ['deriveKey']
    );
    return key;
};

Solution

  • The keyUsages value is wrong (as also pointed out by the error message): When importing a public key in the context of ECDH, an empty array [] is to be passed for keyUsages (deriveKey and/or deriveBits are only passed when importing a private key).

    Also, keep in mind that the public EC key must be passed as an uncompressed or compressed key when using the format raw.

    If the keyUsages value is fixed and an uncompressed or compressed key is applied, the posted code works.

    (async () => {
    
    var uncomressedKeyB64 = 'BAmL07vrRR5lfkWuH1RAFJufx0B4J+BdOqIYZCH+fJc8c+5sFch8aXEJ6qVgTnnYjKwrQ1BO3Tg28/F1h/FjMVQ='; 
    var comressedKeyB64 = 'AgmL07vrRR5lfkWuH1RAFJufx0B4J+BdOqIYZCH+fJc8';
    
    const b64ToBin = (b64) => {
        const binaryString = window.atob(b64);
        const length = binaryString.length;
        const bytes = new Uint8Array(length);
        for (let i = 0; i < length; i++) {
            bytes[i] = binaryString.charCodeAt(i);
        }
        return bytes.buffer;
    };
    
    const importB64Key = async (base64key) => {
        const bin = b64ToBin(base64key);
        console.log('bin ', bin);
        const key = await window.crypto.subtle.importKey(
            'raw',
            bin,
            {
                name: 'ECDH',
                namedCurve: 'P-256',
            },
            true,
            [] // Fix: When importing a public key, an empty array must be passed for the key usages...
        );
        return key;
    };
    
    console.log(await importB64Key(uncomressedKeyB64));
    console.log(await importB64Key(comressedKeyB64));
    
    })();