unjs / uncrypto

Single API for Web Crypto API and Crypto Subtle working in Node.js, Browsers and other runtimes
MIT License
182 stars 8 forks source link

Support for Argon2 #40

Open murisceman opened 4 months ago

murisceman commented 4 months ago

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

fayazara commented 4 months ago

@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)
  );
}
pi0 commented 4 months ago

Thanks for sharing snippet but is it Argon2?

fayazara commented 4 months ago

Thanks for sharing snippet but is it Argon2?

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

pi0 commented 4 months ago

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