Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 205 additions & 0 deletions lib/accesskey.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

/*
* Copyright 2025 Edgecast Cloud LLC.
*/

/*
* Routines for generating and validating secret access keys.
*/

var crypto = require('crypto');
var crc32 = require('crc').buffer.crc32;
var assert = require('assert-plus');

var TO_B64_REG = new RegExp('[+/=]', 'g');
var FROM_B64_REG = new RegExp('[-_]', 'g');

var DEFAULT_PREFIX = 'tdc_';
var DEFAULT_BYTE_LENGTH = 32;

// Don't have base64url encoded Buffers until Node v14
function toBase64url(input) {
return input
.toString('base64')
.replace(TO_B64_REG, function (c) {
if (c === '+') {
return '-';
}
if (c === '/') {
return '_';
}
if (c === '=') {
return '';
}
return null;
});
}

function fromBase64url(input) {
var base64 = input.replace(FROM_B64_REG, function (c) {
if (c === '-') {
return '+';
}
if (c === '_') {
return '/';
}
return null;
});

// Restore padding
while (base64.length % 4 !== 0) {
base64 += '=';
}

// Buffer.from not available until Node v5
if (typeof (Buffer.from) === 'function') {
return Buffer.from(base64, 'base64');
}

return new Buffer(base64, 'base64');
}

/*
* Node v0.10 didn't yet have `Buffer.alloc()` and `new Buffer()` was the only
* option until v5. However, using `new Buffer()` in newer Node versions emits a
* deprecation message with a security warning so `Buffer.alloc` is used where
* available.
*/
function newBuffer(size) {
assert.number(size, 'size');
if (typeof (Buffer.alloc) === 'function') {
return Buffer.alloc(size);
}
return new Buffer(size);
}

/**
* Generate a random secret access key inspired by suggestions from Github's
* Secret Scanning Partner Program[0] and how they structure their keys[1]:
* [0] https://i.no.de/c12f50d544eececf
* [1] https://i.no.de/5a4e8cea87c0a873
*
* Instead of using Base62 as Github does, base64url encoding is used instead.
*
* Keys generated from this function have:
* - A uniquely defined prefix (e.g. "tdc_" for "Triton DataCenter")
* - High entropy random strings (32 random bytes from node crypto)
* - A 32-bit crc checksum (to validate token structure)
*
* An example key:
*
* tdc_SU4xWXL-HzrMIDM_A8GH94sl-uc-aX8mqsEMiK4JSVdAGyjH
*
* +--------+--------------------------------------------+--------+
* | PREFIX | RANDOM BYTES | CRC32 |
* +--------+--------------------------------------------+--------+
* | tdc_ | SU4xWXL-HzrMIDM_A8GH94sl-uc-aX8mqsEMiK4JSV | dAGyjH |---+
* +--------+--------------------------------------------+--------+ |
* | BASE64 URL ENCODED | |
* +--------+--------------------------------------------+--------+ |
* | CRC32 coverage (PREFIX + RANDOM BYTES) | <----------+
* +-----------------------------------------------------+
*
* @param {String} prefix string for the token.
* @param {Number} byte count to randomly generate.
* @param {Function} callback of the form fn(err, key).
* @throws {TypeError} on bad input.
*/
function generate(prefix, bytes, done) {
assert.string(prefix, 'prefix');
assert.number(bytes, 'bytes');
assert.func(done, 'done');

crypto.randomBytes(bytes, function generateBytes(err, randBytes) {
if (err) {
done(err);
return;
}

// Create a buffer containing the prefix and random bytes
var prefixBuf = newBuffer(prefix.length);
prefixBuf.write(prefix);

var tokenBuf = Buffer.concat([prefixBuf, randBytes]);

// Obtain CRC32 from prefix + random bytes
var crc = crc32(tokenBuf);

// Write the CRC32 into a new buffer encoded as a 32-bit signed int
var crcBuf = newBuffer(4);

// Some anicent versions of Node return undefined for writeInt32LE (at
// least v0.10.48 but not v0.12.14)
var wrote = crcBuf.writeInt32LE(crc, 0);
if (wrote !== undefined && wrote !== 4) {
done(new Error('Failed to generate access key'));
return;
}

// Base64 URL the encode random bytes + CRC32, prepend the prefix
var key = prefix + toBase64url(Buffer.concat([randBytes, crcBuf]));

done(null, key);
return;
});
}

/**
* Validates the structure of a secret access key. Does NOT validate that the
* token is active and valid for authentication purposes it only validates that
* the token structure is correct. This function can be used to toss out a
* garbage token before attempting to look it up against UFDS.
*
* @param {String} prefix string for the token.
* @param {Number} byte count expected in the token.
* @param {String} secret key string.
* @throws {TypeError} on bad input.
*/
function validate(prefix, bytes, secret) {
assert.string(prefix, 'prefix');
assert.number(bytes, 'bytes');
assert.string(secret, 'secret');

if (secret.indexOf(prefix) !== 0) {
return false;
}

// Remove prefix from the secret
var body = secret.slice(prefix.length);

// Base64 URL decode the body containing random bytes + CRC32
var parts = fromBase64url(body);

// Must contain the expected number of random bytes + 4 bytes for the CRC32
if (parts.length !== (bytes + 4)) {
return false;
}

// Create a buffer containg the prefix
var prefixBuf = newBuffer(prefix.length);
prefixBuf.write(secret.slice(0, prefix.length));

// Create a buffer containing the random bytes
var randBytesBuf = parts.slice(0, -4);

// Create a buffer from the CRC32 at the end of the secret
var crc32Buf = parts.slice(-4);

// Create a new buffer containing the prefix + random bytes
var tokenBuf = Buffer.concat([prefixBuf, randBytesBuf]);

// Recompute CRC32 and compare with the CRC32 obtained from the secret
return (crc32(tokenBuf) === crc32Buf.readInt32LE(0));
}

module.exports = {
generate: generate,
validate: validate,
DEFAULT_PREFIX: DEFAULT_PREFIX,
DEFAULT_BYTE_LENGTH: DEFAULT_BYTE_LENGTH
};
Loading