Created
August 20, 2024 20:40
-
-
Save himanshupal/e0a1907823ca5b44dee92be020d2e5fa to your computer and use it in GitHub Desktop.
Quick & Dirty SCRAM implementation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @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..."); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @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"); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @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