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 ornpx 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
- argon2 - very secure
- bcrypt - secure
- scrypt - ?
- 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