Created
August 18, 2020 22:42
-
-
Save mxmauro/3cf47107cd4d58f0ef9d7cef258ed00d to your computer and use it in GitHub Desktop.
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
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