uncrypto icon indicating copy to clipboard operation
uncrypto copied to clipboard

Support for Argon2

Open murisceman opened this issue 1 year ago • 6 comments

Describe the feature

Using Argon2 for password hashing in Node.js + worker environments would be awesome. Currently, the only choice in Cloudflare Workers, as far as I'm aware, is PBKDF2 via Web Crypto.

Additional information

  • [x] Would you be willing to help implement this feature?

murisceman avatar Mar 04 '24 09:03 murisceman

@murisceman @pi0 I've been using the below in cloudflare workers.

A small utility for hashing and verifying text, if this looks good, can I create a PR @pi0 ?

function hexStringToByteArray(hexString) {
  return new Uint8Array(
    hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))
  );
}

function timingSafeEqual(a, b) {
  if (a.length !== b.length) {
    return false;
  }
  let result = 0;
  for (let i = 0; i < a.length; i++) {
    result |= a[i] ^ b[i];
  }
  return result === 0;
}
export async function hashPassword(password) {
  const encoder = new TextEncoder();
  const salt = crypto.getRandomValues(new Uint8Array(16));
  const keyMaterial = await crypto.subtle.importKey(
    "raw",
    encoder.encode(password),
    { name: "PBKDF2" },
    false,
    ["deriveBits", "deriveKey"]
  );
  const key = await crypto.subtle.deriveKey(
    {
      name: "PBKDF2",
      salt: salt,
      iterations: 100000,
      hash: "SHA-256",
    },
    keyMaterial,
    { name: "AES-GCM", length: 256 },
    true,
    ["encrypt", "decrypt"]
  );

  const exportedKey = await crypto.subtle.exportKey("raw", key);
  const hash = Array.from(new Uint8Array(exportedKey))
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
  const encodedSalt = Array.from(salt)
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
  return `pbkdf2_sha256$100000$${encodedSalt}$${hash}`;
}

export async function verifyPassword(stored, passwordAttempt) {
  const parts = stored.split("$");
  const iterations = parseInt(parts[1], 10);
  const salt = hexStringToByteArray(parts[2]);
  const storedHash = parts[3];

  const encoder = new TextEncoder();
  const keyMaterial = await crypto.subtle.importKey(
    "raw",
    encoder.encode(passwordAttempt),
    "PBKDF2",
    false,
    ["deriveBits"]
  );

  const derivedBits = await crypto.subtle.deriveBits(
    {
      name: "PBKDF2",
      salt: salt,
      iterations: iterations,
      hash: "SHA-256",
    },
    keyMaterial,
    256
  );

  const attemptedHash = Array.from(new Uint8Array(derivedBits))
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");

  return timingSafeEqual(
    hexStringToByteArray(attemptedHash),
    hexStringToByteArray(storedHash)
  );
}

fayazara avatar Mar 04 '24 10:03 fayazara

Thanks for sharing snippet but is it Argon2?

pi0 avatar Mar 04 '24 10:03 pi0

Thanks for sharing snippet but is it Argon2?

No, this is using PBKDF2. I thought @murisceman was just looking for hashing some text.

fayazara avatar Mar 04 '24 10:03 fayazara

This issue is specifically for argon2. Generic encryption utils are in the roadmap already. Thanks anyway.

pi0 avatar Mar 04 '24 11:03 pi0

It seems that someone made bcrypt-edge.

I tested it and it works well on Cloudflare Workers: https://github.com/atinux/nuxt-bcrypt-edge

Demo: https://bcrypt.nuxt.dev

atinux avatar Sep 02 '24 22:09 atinux

This is awesome! I'd love to see how much time it takes on a Worker with more than 10 rounds, specifically 12 and 13. I believe 14 might be too costly. (https://github.com/atinux/nuxt-bcrypt-edge/pull/1)

murisceman avatar Sep 02 '24 23:09 murisceman