Svelte -> Lucia -> argon2

Lucia is a reference for session-based web app auth. It contains example code snippets and projects for implementing session-based auth within SvelteKit and other JS projects. You can add code which follows the Lucia guide to your project with npx sv create when creating a new project or npx sv add lucia for an existing project. src: svelte recommends using Lucia

npx sv add lucia uses import { hash, verify } from '@node-rs/argon2'; and example code from Lucia also uses the same library

problem: @node-rs/argon2 does not work on Cloudflare pages

Cloudflare Page: Could not resolve “@node-rs/argon2-wasm32-wasi”

solution A: use different hashing algorithm (eg. PBKDF2 ; but is this secure enough?)

(src: https://www.answeroverflow.com/m/1304500967726907512)

solution B: custom “Argon2 Rust Worker”

cons:

  • Cloudflare Paid Workers Plan required because

The free tier plan might work but hashing will use ~ 100 ms of CPU time, exceeding the 10 ms CPU time limit for free plans. (src: https://mli.puffinsystems.com/blog/lucia-auth-cloudflare-argon2)

at the moment the cost of using Cloudflare Paid Workers Plan is at least $5/month (flat subscription cost) + $0/month because it won’t be used enough to go over the free stuff. But the flat $5/month is too much for a pet project…

comparison of password hashing algorithms

  1. argon2 - very secure
  2. bcrypt - secure
  3. scrypt - ?
  4. PBKDF2 - ? (src: https://guptadeepak.com/comparative-analysis-of-password-hashing-algorithms-argon2-bcrypt-scrypt-and-pbkdf2/)

Cloudflare Web Crypto API (crypto.subtle)

SHA-256 (bad for password?)

FYI: I vaguely remember that SHA-256 is not to be used for password hashing?

SHA256 […] are message digests, they were never meant to be password-hashing (src: https://stackoverflow.com/questions/11624372/best-practice-for-hashing-passwords-sha256-or-sha512)

const myDigest = await crypto.subtle.digest(
{name: 'SHA-256',},
myText // The data you want to hash as an ArrayBuffer
);

https://developers.cloudflare.com/workers/runtime-apis/web-crypto/

async function hashPassword(password: string, salt: string): Promise<string> {
    const utf8 = new TextEncoder().encode(`${salt}:${password}`);

    const hashBuffer = await crypto.subtle.digest({name: 'SHA-256'}, utf8);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    return hashArray
        .map((bytes) => bytes.toString(16).padStart(2, '0'))
        .join('');
}

src: https://massadas.com/posts/implementing-register-and-login-in-workers-d1/

PBKDF2

export async function hashPassword(
  password: string,
  providedSalt?: Uint8Array
): Promise<string> {
  const encoder = new TextEncoder();
  // Use provided salt if available, otherwise generate a new one
  const salt = providedSalt || 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
  )) as ArrayBuffer;
  const hashBuffer = new Uint8Array(exportedKey);
  const hashArray = Array.from(hashBuffer);
  const hashHex = hashArray
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
  const saltHex = Array.from(salt)
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
  return `${saltHex}:${hashHex}`;
}

export async function verifyPassword(
  storedHash: string,
  passwordAttempt: string
): Promise<boolean> {
  const [saltHex, originalHash] = storedHash.split(":");
  const matchResult = saltHex.match(/.{1,2}/g);
  if (!matchResult) {
    throw new Error("Invalid salt format");
  }
  const salt = new Uint8Array(matchResult.map((byte) => parseInt(byte, 16)));
  const attemptHashWithSalt = await hashPassword(passwordAttempt, salt);
  const [, attemptHash] = attemptHashWithSalt.split(":");
  return attemptHash === originalHash;
}

src: https://lord.technology/2024/02/21/hashing-passwords-on-cloudflare-workers.html