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