Web cryptography implement HKDF for the output of ECDH
Asked Answered
C

1

5

I want implement a elliptic curve diffie hellman using HKDF as key derivation function. I am using a python backend and (vanilla) javascript in frontend. I am using python cryptography library in backend and Web Crypto api in frontend as cryptographic library. I created ECDH key pair in both side and exchanged the pbulic keys. Now I am trying to create the AES shared key with the exchanged public key and private key along with HKDF algorithm. I am able to do it in the python backend (I followed this example for the python code):

def encrypt(public_key, secret):
global loaded_public_key
loaded_public_key = public_key
shared_key = server_private_key.exchange(ec.ECDH(), public_key)
IV = bytes("ddfbccae-b4c4-11", encoding="utf-8")
derived_key = HKDF(
        algorithm=hashes.SHA256(),
        length=32,
        salt=None,
        info=None,
).derive(shared_key)
aes = Cipher(algorithms.AES(derived_key), modes.GCM(IV))
encryptor = aes.encryptor()
padder = padding.PKCS7(128).padder()
padded_data = padder.update(secret.encode()) + padder.finalize()
return encryptor.update(secret.encode()) + encryptor.finalize()

But Iam not sure how to do it using web crypto api. Here is my attempt: (but doesn't work)

async function deriveSecretKey(privateKey, publicKey) {
  let sharedKey = await window.crypto.subtle.deriveKey(
    {
      name: "ECDH",
      public: publicKey
    },
    privateKey,
    {
      name: "AES-GCM",
      length: 256
    },
    false,
    ["encrypt", "decrypt"]
  );
  return window.crypto.subtle.deriveKey(
    {
      name: "HKDF",
      hash:  {name: "SHA-256"} ,
      salt: new ArrayBuffer(0),
      info: new ArrayBuffer(0)
    },
    sharedKey,
    {
      name: "AES-GCM",
      length: 256
    },
    false,
    ["encrypt", "decrypt"]
  );
}

How can I create the shared AES key along with HKDF (same way as python) in the frontend using web crypto api?

Clotilde answered 11/6, 2021 at 14:5 Comment(1)
@Topaco I followed this python example.... I know that web crypto api has both ECDH and HKDF options...but not sure how to use them together to generate the AES key like in the python code.Clotilde
P
8

The referenced Python code uses P-384 (aka secp384r1) as elliptic curve. This is compatible with the WebCrypto API, which supports three curves P-256 (aka secp256r1), P-384 and P-521 (aka secp521r1), see EcKeyImportParams.

The following WebCrypto code generates a shared secret using ECDH and derives an AES key from the shared secret using HKDF. In detail the following happens:

  • To allow comparison of the derived key with that of the referenced Python code, predefined EC keys are applied. The private key is imported as PKCS#8, the public key as X.509/SPKI. Note that due to a Firefox bug concerning the import of EC keys, the script below cannot be run in the Firefox browser.
  • After the import the shared secret is created with ECDH using deriveBits() (and not deriveKey()).
  • The shared secret is imported with importKey() and then the AES key is derived using HKDF, again with deriveBits().

(async () => {
    await deriveKey();
})();

async function deriveKey() {

    //
    // Key import
    //
    var server_x509 =  `-----BEGIN PUBLIC KEY-----
                        MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEd7fej9GYVI7Vt6x5B6XhruHvmE/rnzIj
                        HmpxP8PKfnfWgrJbyG2cgQc3mf9uusqk1FKImA86rx2+avK8+7xIK9wxuF3x2KQq
                        nxNp7bUBit3phyhp72Nt/QLXmZHcDKXL
                        -----END PUBLIC KEY-----`;
    var client_pkcs8 = `-----BEGIN PRIVATE KEY-----
                        MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBjr4EGktNtx+3xErsC
                        MzldruzzfAEEO8Oth1/3b8sNfrqRsAgMnB/oVy024I+15wOhZANiAASbTF7LLedW
                        dik6nH8JX8WeU0R1ZRlqq0EAZ/t+UrFcSOaVJSOx5jMJ3nrqwuk2DnobDqFwXH6t
                        ZMsZHh4NFZ+bCVeHJRqy4SCZvQFB/xcksF29p1v14XHYI/XKMGyLLx4=
                        -----END PRIVATE KEY-----`;

    var client_private_key = b64_to_ab(client_pkcs8.replace('-----BEGIN PRIVATE KEY-----', '').replace('-----END PRIVATE KEY-----', ''));
    var server_public_key = b64_to_ab(server_x509.replace('-----BEGIN PUBLIC KEY-----', '').replace('-----END PUBLIC KEY-----', ''));
    var privateKey = await window.crypto.subtle.importKey( 
        'pkcs8', 
        client_private_key,
        { name: "ECDH", namedCurve: "P-384" },
        true, 
        ["deriveKey", "deriveBits"] 
    );
    var publicKey = await window.crypto.subtle.importKey(
        "spki", 
        server_public_key,
        { name: "ECDH", namedCurve: "P-384" },
        true, 
        [] 
    );
    
    //
    // Determine shared secret
    //
    var sharedSecret = await window.crypto.subtle.deriveBits(
        { name: "ECDH", namedCurve: "P-384", public: publicKey },
        privateKey, 
        384 
    );
    console.log("Shared secret:\n", ab_to_b64(sharedSecret).replace(/(.{48})/g,'$1\n'));
    
    //
    // Derive key from shared secret via HKDF
    //
    var sharedSecretKey = await window.crypto.subtle.importKey(
        "raw", 
        sharedSecret, 
        { name: "HKDF" }, 
        false, 
        ["deriveKey", "deriveBits"]
    );
    var derived_key = await crypto.subtle.deriveBits(
        { name: "HKDF", hash: "SHA-256", salt: new Uint8Array([]), info: new Uint8Array([]) }, 
        sharedSecretKey, 
        256
    );
    console.log("Derived key:\n", ab_to_b64(derived_key).replace(/(.{48})/g,'$1\n'))
}; 

function b64_to_ab(base64_string){
    return Uint8Array.from(atob(base64_string), c => c.charCodeAt(0));
}

function ab_to_b64(arrayBuffer){
    return btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
}

with the following output:

Shared secret:
 xbU6oDHMTYj3O71liM5KEJof3/0P4HlHJ28k7qtdqU/36llCizIlOWXtj8v+IngF
Derived key:
 Yh0FkhqrT9XDQqIiSrGv5YmBjCSj9jhR5fF6HusbN1Q=

To compare the generated AES key with that of the referenced Python code, the following Python code is used, which is based on the referenced code but applies predefined keys that are the counterparts of the keys used in the WebCrypto code. Since the focus here is on key derivation, the AES part is not considered:

from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
import base64

def deriveKey():

  server_pkcs8 = b'''-----BEGIN PRIVATE KEY-----
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBReGpDVmoVTzxNbJx6
aL4L9z1EdB91eonAmAw7mKDocLfCJITXZPUAmM46c6AipTmhZANiAAR3t96P0ZhU
jtW3rHkHpeGu4e+YT+ufMiMeanE/w8p+d9aCslvIbZyBBzeZ/266yqTUUoiYDzqv
Hb5q8rz7vEgr3DG4XfHYpCqfE2nttQGK3emHKGnvY239AteZkdwMpcs=
-----END PRIVATE KEY-----'''

  client_x509 = b'''-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEm0xeyy3nVnYpOpx/CV/FnlNEdWUZaqtB
AGf7flKxXEjmlSUjseYzCd566sLpNg56Gw6hcFx+rWTLGR4eDRWfmwlXhyUasuEg
mb0BQf8XJLBdvadb9eFx2CP1yjBsiy8e
-----END PUBLIC KEY-----'''

  client_public_key = serialization.load_pem_public_key(client_x509)
  server_private_key = serialization.load_pem_private_key(server_pkcs8, password=None)
  shared_secret = server_private_key.exchange(ec.ECDH(), client_public_key)
  print('Shared secret: ' + base64.b64encode(shared_secret).decode('utf8')) # Shared secret: xbU6oDHMTYj3O71liM5KEJof3/0P4HlHJ28k7qtdqU/36llCizIlOWXtj8v+IngF

  derived_key = HKDF(
    algorithm=hashes.SHA256(),
    length=32,
    salt=None,
    info=None,
  ).derive(shared_secret) 
  print('Derived key:   ' + base64.b64encode(derived_key).decode('utf8')) # Derived key:   Yh0FkhqrT9XDQqIiSrGv5YmBjCSj9jhR5fF6HusbN1Q=

deriveKey()

with the following output:

Shared secret: xbU6oDHMTYj3O71liM5KEJof3/0P4HlHJ28k7qtdqU/36llCizIlOWXtj8v+IngF
Derived key:   Yh0FkhqrT9XDQqIiSrGv5YmBjCSj9jhR5fF6HusbN1Q=

which corresponds to the values of the WebCrypto code.

Printmaker answered 11/6, 2021 at 19:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.