Skip to content

Instantly share code, notes, and snippets.

@himanshupal
Created August 20, 2024 20:40
Show Gist options
  • Save himanshupal/e0a1907823ca5b44dee92be020d2e5fa to your computer and use it in GitHub Desktop.
Save himanshupal/e0a1907823ca5b44dee92be020d2e5fa to your computer and use it in GitHub Desktop.
Quick & Dirty SCRAM implementation
// @ts-check
import { completeAuth, initiateAuth } from "./server.mjs";
import { getClientKeyAndHash, getHMAC, getRandomBytes, hashPassword, xorBuffer } from "./utils.mjs";
/**
* @param {string} username
* @param {string} password
*/
export async function clientAuthentication(username, password) {
const clientNonce = getRandomBytes();
const { salt, nonce } = initiateAuth(username, clientNonce);
const combinedNonce = `${clientNonce}:${nonce}:${username}`;
const hashedPassword = await hashPassword(password, Buffer.from(salt, "base64"));
const { clientKey, storedKey } = getClientKeyAndHash(Buffer.from(hashedPassword));
const clientSignature = getHMAC(storedKey, Buffer.from(combinedNonce));
const clientProof = xorBuffer(clientKey, clientSignature);
const serverSignature = completeAuth(username, clientProof.toString("base64"), combinedNonce);
const serverKey = getHMAC(clientProof, Buffer.from("Server Key"));
const expectedServerSignature = getHMAC(serverKey, Buffer.from(combinedNonce)).toString("base64");
if (serverSignature !== expectedServerSignature) {
throw new Error("Server signature mismatch");
}
console.log("Login successful...");
}
// @ts-check
import { getClientKeyAndHash, getHash, getHMAC, getRandomBytes, hashPassword, xorBuffer } from "./utils.mjs";
/** @type {Map<string, {salt: string, storedKey: Buffer}>} */
const users = new Map();
/**
* Add new user to accounts data
* @param {string} username
* @param {string} password
*/
export async function createAccount(username, password) {
const salt = getRandomBytes();
const hashedPassword = await hashPassword(password, Buffer.from(salt, "base64"));
const { storedKey } = getClientKeyAndHash(Buffer.from(hashedPassword));
users.set(username, {
storedKey,
salt,
});
console.log("Registration successful...");
}
/**
* @param {string} username
* @param {string} _clientNonce
*/
export function initiateAuth(username, _clientNonce) {
const user = users.get(username);
if (!user) throw new Error("User not found");
const nonce = getRandomBytes();
const { salt } = user;
// Set `_clientNonce` + `serverNonce` to user's secure cookie when sending below data
// Upon final authentication, read the values from that cookie, update it based on frontend implementation
// Doing so, user won't have to send the cominedNonce in final submission
// & since cookie can self expire, the login session can be aborted after a few minutes if no further response is received from user
return { nonce, salt };
}
/**
* @param {string} username
* @param {string} clientProof
* @param {string} _combinedNonce
*/
export function completeAuth(username, clientProof, _combinedNonce) {
const user = users.get(username);
if (!user) throw new Error("User not found");
// Read this `combinedNonce` value from user's cookie, update it based on frontend implementation, then use it
// No need to pass in final submission
const clientProofBuffer = Buffer.from(clientProof, "base64");
const clientSignature = getHMAC(user.storedKey, Buffer.from(_combinedNonce));
const clientKey = xorBuffer(clientProofBuffer, clientSignature);
const storedKey = getHash(clientKey);
if (!storedKey.equals(user.storedKey)) {
throw new Error("Incorrect password");
}
const serverKey = getHMAC(clientProofBuffer, Buffer.from("Server Key"));
return getHMAC(serverKey, Buffer.from(_combinedNonce)).toString("base64");
}
// @ts-check
import { hash } from "argon2";
import { createHash, createHmac, randomBytes } from "crypto";
export function getRandomBytes(length = 32) {
return randomBytes(length).toString("base64");
}
/**
* @param {string} plaintextPassword
* @param {Buffer} salt
*/
export function hashPassword(plaintextPassword, salt) {
return hash(plaintextPassword, {
parallelism: 8,
hashLength: 64,
timeCost: 32,
salt,
});
}
/**
* @param {Buffer} a
* @param {Buffer} b
*/
export function xorBuffer(a, b) {
if (a.length !== b.length) throw new Error("Invalid buffer length");
const c = Buffer.alloc(a.length);
for (let i = 0; i < a.length; i++) {
c[i] = a[i] ^ b[i];
}
return c;
}
/**
* @param {Buffer} key
* @param {Buffer} message
*/
export function getHMAC(key, message) {
return createHmac("sha256", key).update(message).digest();
}
/**
*
* @param {Buffer} data
*/
export function getHash(data) {
return createHash("sha256").update(data).digest();
}
/**
* @param {Buffer} hashedPassword
*/
export function getClientKeyAndHash(hashedPassword) {
const clientKey = getHMAC(hashedPassword, Buffer.from("Client Key"));
const storedKey = getHash(clientKey);
return { clientKey, storedKey };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment