How do you set a seed, or entropy, or default string, for keys generated by Web Crypto, please, anything to make it deterministic
Asked Answered
K

2

9

It is surprisingly difficult to set the entropy, or seed, for the Web Crypto API in javascript.

Why is this the case? Is it to make things difficult for developers?

Setting seeds allows you to deterministically run on sample data. The Web Crypto API also happens to be baked in to the library i am using. This feature of the library that i am trying to implement has also been almost abandoned because of how difficult this problem is.

Kropotkin answered 1/4, 2019 at 23:34 Comment(6)
In general, controlling the seed only leads to bad crypto. For tests, consider just decrypting known data and decrypting locally encrypted data. (Or verifying known / verifying signed).Petes
Controlling the seed allows you to create transferable secure settings If the seed is generated securely, that is. In general, libraries should enable development, not hinder it. also, it's a pretty basic element of data science, with machine learning coming to browsers and suchKropotkin
This is insanely stupidly difficult. I want to use the same entropy for multiple keys. Why would that be an issue of the entropy was appropriately generated (i.e. from Web Crypto to begin with)Kropotkin
Seems like you might be able to get what you are after with deriveKey and PBKDF2Neb
Thanks but i don't think it applies to ECDSA keys. I also am too stupid to figure it out; These are really verbose function callsKropotkin
You are right that you can't use deriveKey with ECDSA. Sounds like your issue is more specific than your question suggests. You might get a better answer if you post a more specific question that includes the minimal code you are using to implement the feature of the library you are using and explain exactly where you are stuck in that code.Neb
S
0

I am not 100% sure what the actual question is, so I am providing a generic answer.

You can use a HDKF key (https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey#hkdf) as the high entropy base key for deriving other keys.

Copy pasting from https://github.com/mdn/dom-examples/blob/main/web-crypto/derive-key/hkdf.js

  async function deriveSharedSecret(privateKey, publicKey) {
    const secret = await window.crypto.subtle.deriveBits(
      { name: "ECDH", public: publicKey },
      privateKey,
      384
    );

    return window.crypto.subtle.importKey(
      "raw",
      secret,
      { name: "HKDF" },
      false,
      ["deriveKey"]
    );
  }

You will notice a private and public key pair as input for deriveSharedSecret. ECDH is one way to generate that pair. See https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveBits. If your code is a service running on a secure server environment, you can generate and export the key (see https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/exportKey) as a part of your build or setup process and load it in your service when your service starts (see https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey). This way, the initial high entropy key will be deterministic.

You should design your WebCrypto Key usage (algorithms and processes) in accordance with your security needs.

One of the benefits of HDKF is that it supports an optional non-secret salt when deriving sub keys. This can be used to generate the salt per session or per purpose or both, thus adding some level of randomness to the high entropy base key for each use case within the application. See https://datatracker.ietf.org/doc/html/rfc5869#section-3.1

The high entropy base key should be considered top-secret. It should not be saved into version control or read off an un-secured bucket. It should not be a part of any client application. It could be loaded from a secure store, such as AWS Secrets.

Syst answered 4/10, 2023 at 19:7 Comment(0)
S
0

Continuing my earlier answer, I wrote a small program that demonstrates generating Sign, Verify and Encrypt, Decrypt keys on Node (not the browser) from a master key. Run it with ts-node. Sample package.json:

{
  "name": "crypto",
  "private": true,
  "scripts": {
    "genKeys": "ts-node src/gen_keys.ts"
  },
  "devDependencies": {
    "ts-node": "^10.9.1",
    "typescript": "^5.2.2"
  }
}

!! FOR EDUCATIONAL PURPOSES ONLY. YOU ASSUME ALL RISKS. DO NOT COPY PASTE FOR USE IN PRODUCTION !!

async function main(): Promise<void> {

  const crypto: Crypto = globalThis.crypto

  const toHex = (buffer: ArrayBuffer): string => {
    const hashArray = Array.from(new Uint8Array(buffer))
    const hashHex = hashArray
      .map((b) => b.toString(16).padStart(2, "0"))
      .join("")
    return hashHex
  }

  const fromHex = (hashHex: string): ArrayBuffer => {
    if (typeof hashHex !== "string") {
      throw new TypeError("Expected input to be a string")
    }
    if ((hashHex.length % 2) !== 0) {
      throw new RangeError("Expected string to be an even number of characters")
    }
    const result = new Uint8Array(hashHex.length / 2)
    for (let i = 0, j = 0; i < result.length; i += 1, j += 2) {
      result[i] = parseInt(hashHex.substring(j, j + 2), 16)
    }
    return result.buffer
  }

  /*
   * Keep below string in a secret location, such as AWS secrets, 512 random bits
   */
  const getSecretRawKeyStr = async (): Promise<string> => {
    return "1eb85f2e8833d299e0a4fec2ba501f72c6944a660abc1ecabc20f4c0f06ff27d2d5159bec98821ef4d893755d0b711ccdc0e881a7adb6f54acbdbeb5c8dfebd9";
  }

  const makeMasterKey = async (): Promise<CryptoKey> => {

    const rawKeyBuf = fromHex(await getSecretRawKeyStr())

    return crypto.subtle.importKey(
      "raw",
      rawKeyBuf,
      { name: "HKDF" },
      false,
      ["deriveKey"],
    );
  }

  const makeEncDecKey = async (masterKey: CryptoKey, salt: ArrayBuffer, purpose: string): Promise<CryptoKey> => {
    /*
    * Any change to salt or purpose will generate a different key
    */
    return crypto.subtle.deriveKey(
      {
        name: "HKDF",
        salt: salt,
        info: new TextEncoder().encode(purpose),
        hash: "SHA-256",
      },
      masterKey,
      { name: "AES-GCM", length: 256 },
      true,
      ["encrypt", "decrypt"],
    );
  }


  const makeSignVerifyKey = async (masterKey: CryptoKey, salt: ArrayBuffer, purpose: string): Promise<CryptoKey> => {
    /*
     * Any change to salt or purpose will generate a different key
     */
    return crypto.subtle.deriveKey(
      {
        name: "HKDF",
        salt: salt,
        info: new TextEncoder().encode(purpose),
        hash: "SHA-256",
      },
      masterKey,
      {
        name: "HMAC",
        hash: { name: "SHA-512" },
      },
      true,
      ["sign", "verify"],
    );
  }

  const masterKey = await makeMasterKey()

  /*
   * Depending on your use case, keep this in secrets or generate
   * for each session
   */
  const getSaltForEncDec = () => "eaa6867c1963b8f25168f36d5c92019a"

  const encDecKeyA = await makeEncDecKey(
    masterKey,
    fromHex(getSaltForEncDec()),
    "This key is for enc and dec")

  const encDecKeyB = await makeEncDecKey(
    masterKey,
    fromHex(getSaltForEncDec()),
    "This key is for enc and dec")

  console.log(await crypto.subtle.exportKey("jwk", encDecKeyA))
  console.log(await crypto.subtle.exportKey("jwk", encDecKeyB))

  const secretText = "All work and no play makes Jack a dull boy"

  /*
   * Depending on your use case, keep this in secrets or generate
   * for each session
   */
  const getIvForEncDec = () => "c739e132c311fc8df216a61a1d9840b4"

  const encrypted: ArrayBuffer = await crypto.subtle.encrypt(
    {
      name: "AES-GCM",
      iv: fromHex(getIvForEncDec())
    },
    encDecKeyA,
    new TextEncoder().encode(secretText)
  )
  console.log(`Encrypted ${toHex(encrypted)}`)

  const decrypted = new TextDecoder().decode(await crypto.subtle.decrypt(
    {
      name: "AES-GCM",
      /*
       * Iv must be same as that used for encryption
       */
      iv: fromHex(getIvForEncDec())
    },
    encDecKeyB,
    encrypted
  ))
  console.log(`Decrypted ${decrypted}`)

  /*
   * Depending on your use case, keep this in secrets or 
   * generate for each session
   */

  const getSaltForSignVerify = () => "e2add8a6cd3543522aad6ddbe59132b7"

  const signVerifyKeyA = await makeSignVerifyKey(
    masterKey,
    fromHex(getSaltForSignVerify()),
    "This key is for sign and verify")

  const signVerifyKeyB = await makeSignVerifyKey(
    masterKey,
    fromHex(getSaltForSignVerify()),
    "This key is for sign and verify")

  console.log(await crypto.subtle.exportKey("jwk", signVerifyKeyA))
  console.log(await crypto.subtle.exportKey("jwk", signVerifyKeyB))

  const signature = await crypto.subtle.sign(
    "HMAC",
    signVerifyKeyA,
    new TextEncoder().encode(secretText)
  )
  const signatureValid = await crypto.subtle.verify(
    "HMAC",
    signVerifyKeyB,
    signature,
    new TextEncoder().encode(secretText),
  )
  console.log(`Signature ${toHex(signature)}, verified ${signatureValid}`)

  //console.log(toHex(crypto.getRandomValues(new Uint8Array(16))))
}

main().catch(console.error);

Syst answered 4/10, 2023 at 22:11 Comment(1)
This question is about the browserKropotkin

© 2022 - 2024 — McMap. All rights reserved.