Skip to content

Commit 4dadfe2

Browse files
authored
TRITON-2513 - Update Access Keys to better support Manta S3 (#12)
Reviewed by: Neirac <[email protected]> Reviewed by: Dan McDonald <[email protected]>
1 parent 2784da2 commit 4dadfe2

File tree

6 files changed

+3281
-196
lines changed

6 files changed

+3281
-196
lines changed

lib/accesskey.js

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
5+
*/
6+
7+
/*
8+
* Copyright 2025 Edgecast Cloud LLC.
9+
*/
10+
11+
/*
12+
* Routines for generating and validating secret access keys.
13+
*/
14+
15+
var crypto = require('crypto');
16+
var crc32 = require('crc').buffer.crc32;
17+
var assert = require('assert-plus');
18+
19+
var TO_B64_REG = new RegExp('[+/=]', 'g');
20+
var FROM_B64_REG = new RegExp('[-_]', 'g');
21+
22+
var DEFAULT_PREFIX = 'tdc_';
23+
var DEFAULT_BYTE_LENGTH = 32;
24+
25+
// Don't have base64url encoded Buffers until Node v14
26+
function toBase64url(input) {
27+
return input
28+
.toString('base64')
29+
.replace(TO_B64_REG, function (c) {
30+
if (c === '+') {
31+
return '-';
32+
}
33+
if (c === '/') {
34+
return '_';
35+
}
36+
if (c === '=') {
37+
return '';
38+
}
39+
return null;
40+
});
41+
}
42+
43+
function fromBase64url(input) {
44+
var base64 = input.replace(FROM_B64_REG, function (c) {
45+
if (c === '-') {
46+
return '+';
47+
}
48+
if (c === '_') {
49+
return '/';
50+
}
51+
return null;
52+
});
53+
54+
// Restore padding
55+
while (base64.length % 4 !== 0) {
56+
base64 += '=';
57+
}
58+
59+
// Buffer.from not available until Node v5
60+
if (typeof (Buffer.from) === 'function') {
61+
return Buffer.from(base64, 'base64');
62+
}
63+
64+
return new Buffer(base64, 'base64');
65+
}
66+
67+
/*
68+
* Node v0.10 didn't yet have `Buffer.alloc()` and `new Buffer()` was the only
69+
* option until v5. However, using `new Buffer()` in newer Node versions emits a
70+
* deprecation message with a security warning so `Buffer.alloc` is used where
71+
* available.
72+
*/
73+
function newBuffer(size) {
74+
assert.number(size, 'size');
75+
if (typeof (Buffer.alloc) === 'function') {
76+
return Buffer.alloc(size);
77+
}
78+
return new Buffer(size);
79+
}
80+
81+
/**
82+
* Generate a random secret access key inspired by suggestions from Github's
83+
* Secret Scanning Partner Program[0] and how they structure their keys[1]:
84+
* [0] https://i.no.de/c12f50d544eececf
85+
* [1] https://i.no.de/5a4e8cea87c0a873
86+
*
87+
* Instead of using Base62 as Github does, base64url encoding is used instead.
88+
*
89+
* Keys generated from this function have:
90+
* - A uniquely defined prefix (e.g. "tdc_" for "Triton DataCenter")
91+
* - High entropy random strings (32 random bytes from node crypto)
92+
* - A 32-bit crc checksum (to validate token structure)
93+
*
94+
* An example key:
95+
*
96+
* tdc_SU4xWXL-HzrMIDM_A8GH94sl-uc-aX8mqsEMiK4JSVdAGyjH
97+
*
98+
* +--------+--------------------------------------------+--------+
99+
* | PREFIX | RANDOM BYTES | CRC32 |
100+
* +--------+--------------------------------------------+--------+
101+
* | tdc_ | SU4xWXL-HzrMIDM_A8GH94sl-uc-aX8mqsEMiK4JSV | dAGyjH |---+
102+
* +--------+--------------------------------------------+--------+ |
103+
* | BASE64 URL ENCODED | |
104+
* +--------+--------------------------------------------+--------+ |
105+
* | CRC32 coverage (PREFIX + RANDOM BYTES) | <----------+
106+
* +-----------------------------------------------------+
107+
*
108+
* @param {String} prefix string for the token.
109+
* @param {Number} byte count to randomly generate.
110+
* @param {Function} callback of the form fn(err, key).
111+
* @throws {TypeError} on bad input.
112+
*/
113+
function generate(prefix, bytes, done) {
114+
assert.string(prefix, 'prefix');
115+
assert.number(bytes, 'bytes');
116+
assert.func(done, 'done');
117+
118+
crypto.randomBytes(bytes, function generateBytes(err, randBytes) {
119+
if (err) {
120+
done(err);
121+
return;
122+
}
123+
124+
// Create a buffer containing the prefix and random bytes
125+
var prefixBuf = newBuffer(prefix.length);
126+
prefixBuf.write(prefix);
127+
128+
var tokenBuf = Buffer.concat([prefixBuf, randBytes]);
129+
130+
// Obtain CRC32 from prefix + random bytes
131+
var crc = crc32(tokenBuf);
132+
133+
// Write the CRC32 into a new buffer encoded as a 32-bit signed int
134+
var crcBuf = newBuffer(4);
135+
136+
// Some anicent versions of Node return undefined for writeInt32LE (at
137+
// least v0.10.48 but not v0.12.14)
138+
var wrote = crcBuf.writeInt32LE(crc, 0);
139+
if (wrote !== undefined && wrote !== 4) {
140+
done(new Error('Failed to generate access key'));
141+
return;
142+
}
143+
144+
// Base64 URL the encode random bytes + CRC32, prepend the prefix
145+
var key = prefix + toBase64url(Buffer.concat([randBytes, crcBuf]));
146+
147+
done(null, key);
148+
return;
149+
});
150+
}
151+
152+
/**
153+
* Validates the structure of a secret access key. Does NOT validate that the
154+
* token is active and valid for authentication purposes it only validates that
155+
* the token structure is correct. This function can be used to toss out a
156+
* garbage token before attempting to look it up against UFDS.
157+
*
158+
* @param {String} prefix string for the token.
159+
* @param {Number} byte count expected in the token.
160+
* @param {String} secret key string.
161+
* @throws {TypeError} on bad input.
162+
*/
163+
function validate(prefix, bytes, secret) {
164+
assert.string(prefix, 'prefix');
165+
assert.number(bytes, 'bytes');
166+
assert.string(secret, 'secret');
167+
168+
if (secret.indexOf(prefix) !== 0) {
169+
return false;
170+
}
171+
172+
// Remove prefix from the secret
173+
var body = secret.slice(prefix.length);
174+
175+
// Base64 URL decode the body containing random bytes + CRC32
176+
var parts = fromBase64url(body);
177+
178+
// Must contain the expected number of random bytes + 4 bytes for the CRC32
179+
if (parts.length !== (bytes + 4)) {
180+
return false;
181+
}
182+
183+
// Create a buffer containg the prefix
184+
var prefixBuf = newBuffer(prefix.length);
185+
prefixBuf.write(secret.slice(0, prefix.length));
186+
187+
// Create a buffer containing the random bytes
188+
var randBytesBuf = parts.slice(0, -4);
189+
190+
// Create a buffer from the CRC32 at the end of the secret
191+
var crc32Buf = parts.slice(-4);
192+
193+
// Create a new buffer containing the prefix + random bytes
194+
var tokenBuf = Buffer.concat([prefixBuf, randBytesBuf]);
195+
196+
// Recompute CRC32 and compare with the CRC32 obtained from the secret
197+
return (crc32(tokenBuf) === crc32Buf.readInt32LE(0));
198+
}
199+
200+
module.exports = {
201+
generate: generate,
202+
validate: validate,
203+
DEFAULT_PREFIX: DEFAULT_PREFIX,
204+
DEFAULT_BYTE_LENGTH: DEFAULT_BYTE_LENGTH
205+
};

0 commit comments

Comments
 (0)