Skip to content

Instantly share code, notes, and snippets.

@mxmauro
Created August 18, 2020 22:42
Show Gist options
  • Save mxmauro/3cf47107cd4d58f0ef9d7cef258ed00d to your computer and use it in GitHub Desktop.
Save mxmauro/3cf47107cd4d58f0ef9d7cef258ed00d to your computer and use it in GitHub Desktop.
const crypto = require('crypto');
const util = require('util');
const assert = require('assert');
//------------------------------------------------------------------------------
const ITERATIONS = 1000;
const SALT_LENGTH = 32;
const IV_LENGTH = 16;
const GCM_AUTH_TAG_LENGTH = 16;
let accounts = new Map();
//------------------------------------------------------------------------------
main().then(() => {
});
//------------------------------------------------------------------------------
async function main() {
//we assume the main routine is executing in the context of the client's browser and the xxxServerXxxx routines
//are actual restAPI requests to a server
const USERNAME = 'testuser';
const PASSWORD = 'mipassword';
//client sends the password to server when account is created
await createServerAccount(USERNAME, getHash(PASSWORD));
//authentication stage 1 => client generates a nonce and sends to server with user ID
const clientNonce = Date.now().toString();
const challenge = await getServerAuthenticationChallenge(USERNAME, clientNonce);
assert.ok(typeof challenge === 'object');
assert.strictEqual(challenge.combinedNonce.substr(0, clientNonce.length), clientNonce);
const saltedPassword = await util.promisify(crypto.pbkdf2)(getHash(PASSWORD), challenge.salt, challenge.iter, 64, 'sha512');
const clientKey= getHMAC(saltedPassword,"Client Key");
const storedKey=getHash(clientKey);
const authMessage = JSON.stringify(challenge);
const clientSignature= getHMAC(storedKey, authMessage);
const clientProof=xorBuffers(clientKey, clientSignature);
const serverSignature = await getServerSignature(USERNAME, clientProof, challenge.combinedNonce);
assert.ok(challenge);
const serverKey= getHMAC(saltedPassword,"Server Key");
assert.ok(getHMAC(serverKey, authMessage), serverSignature);
//testing encription
const toEncrypt = await getRandom(1024);
const encrypted = await encrypt(toEncrypt, getHash(PASSWORD));
assert.ok(await decrypt(encrypted, getHash(PASSWORD)), toEncrypt);
}
async function createServerAccount(username, password) {
const salt = await getRandom(SALT_LENGTH);
const saltedPassword = await util.promisify(crypto.pbkdf2)(password, salt, ITERATIONS, 64, 'sha512');
const clientKey= getHMAC(saltedPassword,"Client Key");
const storedKey=getHash(clientKey);
const serverKey= getHMAC(saltedPassword,"Server Key");
//this data is actually stored on database in the user's record
accounts.set(username, {
salt,
iter: ITERATIONS,
storedKey,
serverKey,
});
}
async function getServerAuthenticationChallenge(username, clientNonce) {
const record = accounts.get(username);
if (!record) {
return null;
}
const serverNonce = Date.now().toString();
return {
salt: record.salt,
iter: record.iter,
combinedNonce: clientNonce + serverNonce,
};
}
async function getServerSignature(username, clientProof, combinedNonce) {
const record = accounts.get(username);
if (!record) {
return null;
}
const authMessage = JSON.stringify({
salt: record.salt, //salt & iter retrieved from record
iter: record.iter,
combinedNonce, //combinenNonce given by client request
});
const clientSignature= getHMAC(record.storedKey, authMessage);
assert.ok(Buffer.compare(getHash(xorBuffers(clientSignature, clientProof)),record.storedKey) == 0);
const serverSignature= getHMAC(record.serverKey, authMessage);
return serverSignature;
}
async function encrypt(buffer, password) {
const iv = await getRandom(IV_LENGTH);
const salt = await getRandom(SALT_LENGTH);
const derivedKey = await util.promisify(crypto.pbkdf2)(password, salt, ITERATIONS, 32, 'sha256');
const cipher = crypto.createCipheriv('aes-256-gcm', derivedKey, iv);
const encrypted = Buffer.concat([
cipher.update(buffer),
cipher.final()
]);
const authTag = cipher.getAuthTag();
const encryptedBuffer = Buffer.concat([salt, iv, encrypted, authTag]);
return encryptedBuffer;
}
async function decrypt(buffer, password) {
if (buffer.length < SALT_LENGTH + IV_LENGTH + GCM_AUTH_TAG_LENGTH) {
return null;
}
const salt = buffer.subarray(0, SALT_LENGTH);
const iv = buffer.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
const authTag = buffer.subarray(buffer.length - GCM_AUTH_TAG_LENGTH);
const derivedKey = await util.promisify(crypto.pbkdf2)(password, salt, ITERATIONS, 32, 'sha256');
const decipher = crypto.createDecipheriv('aes-256-gcm', derivedKey, iv);
decipher.setAuthTag(authTag);
const decryptedBuffer = Buffer.concat([
decipher.update(buffer.subarray(SALT_LENGTH + IV_LENGTH, buffer.length - GCM_AUTH_TAG_LENGTH)),
decipher.final()
]);
return decryptedBuffer;
}
function getHash(buffer) {
const hash = crypto.createHash('sha256');
hash.update(buffer);
return hash.digest();
}
function getHMAC(buffer, key) {
const hmac = crypto.createHmac('sha256', key);
hmac.update(buffer);
return hmac.digest();
}
function xorBuffers(buffer1, buffer2) {
const length = Math.max(buffer1.length, buffer2.length);
let i;
const xoredBuffer = Buffer.alloc(buffer1.length);
if (buffer1.length <= buffer2.length) {
for (i = 0; i < buffer1.length; i++) {
xoredBuffer[i] = buffer1[i] ^ buffer2[i];
}
for (; i < buffer2.length; i++) {
xoredBuffer[i] = buffer2[i];
}
}
else {
for (i = 0; i < buffer2.length; i++) {
xoredBuffer[i] = buffer1[i] ^ buffer2[i];
}
for (; i < buffer1.length; i++) {
xoredBuffer[i] = buffer1[i];
}
}
return xoredBuffer;
}
async function getRandom(length) {
const buf = await util.promisify(crypto.randomBytes)(length);
return buf;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment