2 /* eslint-disable capitalized-comments */
5 const assert
= require('assert');
6 const sinon
= require('sinon'); // eslint-disable-line node/no-unpublished-require
7 const MysteryBox
= require('../../lib/mystery-box');
8 const { MysteryBoxError
} = require('../../lib/errors');
9 const crypto
= require('crypto');
11 function _verbose(mb
) {
12 if (process
.env
.VERBOSE_TESTS
) {
13 mb
.on('statistics', (...args
) => console
.log(...args
));
17 describe('MysteryBox', function () {
18 let mb
, options
, object
;
19 beforeEach(function () {
21 encryptionSecret: 'this is not a very good secret',
24 afterEach(function () {
28 describe('constructor', function () {
29 it('needs a secret', async
function () {
31 assert
.rejects(() => new MysteryBox(options
));
34 it('accepts multiple secrets', async
function () {
37 encryptionSecret: ['first poor secret', 'second poor secret'],
39 mb
= new MysteryBox(options
);
46 const encryptedResult
= await mb
.pack(object
);
47 const decryptedResult
= await mb
.unpack(encryptedResult
);
48 assert
.deepStrictEqual(decryptedResult
, object
);
51 it('covers options', function () {
52 assert
.rejects(() => new MysteryBox());
55 it('covers bad flags', function () {
56 options
.defaultFlags
= 300;
57 assert
.rejects(() => new MysteryBox(options
), MysteryBoxError
);
60 it('covers missing ciphers', function () {
61 sinon
.stub(crypto
, 'getCiphers').returns(['rot13']);
62 assert
.rejects(() => new MysteryBox(options
));
66 describe('_ensureArray', function () {
67 it('returns empty array for no data', function () {
68 const result
= MysteryBox
._ensureArray();
69 assert
.deepStrictEqual(result
, []);
71 it('returns same array passed in', function () {
72 const expected
= [1, 2, 3, 'foo'];
73 const result
= MysteryBox
._ensureArray(expected
);
74 assert
.deepStrictEqual(result
, expected
);
76 it('returns array containing non-array data', function () {
78 const result
= MysteryBox
._ensureArray(data
);
79 assert
.deepStrictEqual(result
, [data
]);
83 describe('_keyFromSecret', function () {
84 it('covers invalid', async
function () {
85 assert
.rejects(() => MysteryBox
._keyFromSecret('unknown deriver', 'secret', 'salt', 32), MysteryBoxError
);
89 describe('_versionHeaderDecode', function () {
90 function _check(firstByte
, numBytes
, value
) {
91 const result
= MysteryBox
._versionHeaderDecode(firstByte
);
92 assert
.strictEqual(result
.numBytes
, numBytes
);
93 assert
.strictEqual(result
.firstByte
, value
);
95 it('decodes single byte, min', function () {
96 _check(0x00, 1, 0x00);
98 it('decodes single byte, max', function () {
99 _check(0x7f, 1, 0x7f);
101 it('decodes double byte, min', function () {
102 _check(0x80, 2, 0x00);
104 it('decodes double byte, max', function () {
105 _check(0xbf, 2, 0x3f);
107 it('decodes triple byte, min', function () {
108 _check(0xc0, 3, 0x00);
110 it('decodes triple byte, max', function () {
111 _check(0xdf, 3, 0x1f);
113 it('decodes quadruple byte, min', function () {
114 _check(0xe0, 4, 0x00);
116 it('decodes quadruple byte, max', function () {
117 _check(0xef, 4, 0x0f);
119 it('decodes quintuple byte, min', function () {
120 _check(0xf0, 5, 0x00);
122 it('decodes quintuple byte, max', function () {
123 _check(0xf7, 5, 0x07);
125 it('decodes sextuple byte, min', function () {
126 _check(0xf8, 6, 0x00);
128 it('decodes sextuple byte, max', function () {
129 _check(0xfb, 6, 0x03);
131 it('decodes septuple byte, min', function () {
132 _check(0xfc, 7, 0x00);
134 it('decodes septuple byte, max', function () {
135 _check(0xfd, 7, 0x01);
137 it('decodes double byte, min', function () {
138 _check(0xfe, 8, 0x00);
140 it('decodes double byte, max', function () {
141 _check(0xfe, 8, 0x00);
143 it('covers unsupported', function () {
144 assert
.throws(() => MysteryBox
._versionHeaderDecode(0xff), MysteryBoxError
);
146 }); // _versionHeaderDecode
148 describe('_versionDecode', function () {
149 function _checkDecodeRange(start
, end
, numBytes
, headerByte
) {
150 const headerMask
= ((0xff << (8 - numBytes
)) & 0xff) >>> 0;
151 const hByte
= ((0xff << (8 - numBytes
+ 1)) & 0xff) >>> 0;
152 assert
.strictEqual(hByte
, headerByte
, `TEST ERROR: unexpected header for length, computed: ${hByte.toString(16)} passed: ${headerByte.toString(16)}`);
153 for (let v
= start
; v
<= end
; v
++) {
154 const buffer
= Buffer
.alloc(numBytes
);
155 buffer
.writeUIntBE(v
, 0, numBytes
);
156 assert((buffer
[0] & headerMask
) === 0, `TEST ERROR: version ${v} encroached on headerByte 0x${headerByte.toString(16)} (${headerByte.toString(2)} & ${buffer[0].toString(2)})`);
157 buffer
[0] = (buffer
[0] | headerByte
) >>> 0;
158 const { version
, versionBytes
} = MysteryBox
._versionDecode(buffer
);
159 assert
.strictEqual(versionBytes
, numBytes
);
160 assert
.strictEqual(version
, v
);
163 it('covers single-byte versions', function () {
164 _checkDecodeRange(0, 127, 1, 0x00);
166 it('covers double-byte versions', function () {
167 _checkDecodeRange(128, 136, 2, 0x80);
169 _checkDecodeRange(16375, 16383, 2, 0x80);
171 it('covers triple-byte versions', function () {
172 _checkDecodeRange(16384, 16390, 3, 0xc0);
174 _checkDecodeRange(2097145, 2097151, 3, 0xc0);
176 it('covers quadruple-byte versions', function () {
177 _checkDecodeRange(2097151, 2097160, 4, 0xe0);
179 _checkDecodeRange(268435445, 268435455, 4, 0xe0);
181 it('covers quintuple-byte versions', function () {
182 _checkDecodeRange(268435445, 268435445, 5, 0xf0);
184 _checkDecodeRange(34359738360, 34359738367, 5, 0xf0);
186 it('covers sextuple-byte versions', function () {
187 _checkDecodeRange(34359738367, 34359738375, 6, 0xf8);
189 _checkDecodeRange(4398046511093, 4398046511103, 6, 0xf8);
191 it('covers too big', function () {
192 const buffer
= Buffer
.from([0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
193 assert
.throws(() => MysteryBox
._versionDecode(buffer
), MysteryBoxError
);
195 }); // _versionDecode
197 describe('_versionEncode', function () {
198 function _checkEncodeRange(start
, end
, bytes
, headerBits
) {
199 for (let version
= start
; version
<= end
; version
++) {
200 const expected
= Buffer
.alloc(bytes
);
201 expected
.writeUIntBE(version
, 0, bytes
);
202 expected
[0] = (expected
[0] | headerBits
) >>> 0;
204 const { buffer
, versionBytes
} = MysteryBox
._versionEncode(version
);
205 assert
.deepStrictEqual(versionBytes
, bytes
, `version ${version} has ${versionBytes} bytes instead of expected ${bytes}`);
206 assert
.deepStrictEqual(buffer
, expected
, `version ${version} buffer not expected: ${JSON.stringify(buffer)} vs ${JSON.stringify(expected)}`);
209 function _cheeckReciprical(version
) {
210 const { buffer
, versionBytes: encVB
} = MysteryBox
._versionEncode(version
);
211 const { version: decV
, versionBytes: decVB
} = MysteryBox
._versionDecode(buffer
);
212 assert
.strictEqual(encVB
, decVB
, `differing lengths for ${version}: enc:${encVB} dec:${decVB}`);
213 assert
.strictEqual(decV
, version
, `failed for ${version}: ${JSON.stringify({ buffer, versionBytes: encVB })}`);
215 it('covers single-byte-packable versions', function () {
216 _checkEncodeRange(0, 127, 1, 0x00);
218 it('covers double-byte-packable versions', function () {
219 _checkEncodeRange(128, 200, 2, 0x80);
221 _checkEncodeRange(16380, 16383, 2, 0x80);
223 it('covers triple-byte-packable versions', function () {
224 _checkEncodeRange(16384, 16390, 3, 0xc0);
226 _checkEncodeRange(2097141, 2097151, 3, 0xc0);
228 it('covers quadruple-byte-packable versions', function () {
229 _checkEncodeRange(2097152, 2097161, 4, 0xe0);
231 _checkEncodeRange(268435445, 268435455, 4, 0xe0);
233 it('covers quintuple-byte-packable versions', function () {
234 _checkEncodeRange(268435456, 268435465, 5, 0xf0)
236 _checkEncodeRange(4294967294, 4294967296, 5, 0xf0)
238 _checkEncodeRange(34359738360, 34359738367, 5, 0xf0)
240 it('covers sextuple-byte-packable versions', function () {
241 _checkEncodeRange(34359738368, 34359738377, 6, 0xf8)
243 _checkEncodeRange(4398046511093, 4398046511103, 6, 0xf8)
245 it('covers too large', function () {
246 const version
= 277076930199552;
247 assert
.rejects(() => MysteryBox
._versionEncode(version
), MysteryBoxError
);
249 it('recipricates', function () {
255 268435456, 34359738367,
256 34359738368, 4398046511103,
257 ].forEach((v
) => _cheeckReciprical(v
));
259 }); // _versionEncode
261 describe('pack, unpack', function () {
262 beforeEach(function () {
263 mb
= new MysteryBox(options
);
267 it('covers packing unsupported version', async
function () {
268 assert
.rejects(() => mb
.pack({}, 0), MysteryBoxError
);
271 it('covers unpacking unsupported version', async
function () {
272 const badBuffer
= Buffer
.alloc(128);
273 badBuffer
.writeUInt8(0, 0); // No such thing as version 0
274 const badPayload
= badBuffer
.toString('base64url');
275 assert
.rejects(() => mb
.unpack(badPayload
), MysteryBoxError
);
278 it('encrypts and decrypts default version', async
function () {
285 const encryptedResult
= await mb
.pack(object
);
286 const decryptedResult
= await mb
.unpack(encryptedResult
);
287 assert
.deepStrictEqual(decryptedResult
, object
);
290 it('encrypts and decrypts default version, buffer contents', async
function () {
292 object
= Buffer
.from('a fine little buffer');
293 const encryptedResult
= await mb
.pack(object
);
294 const decryptedResult
= await mb
.unpack(encryptedResult
);
295 assert
.deepStrictEqual(decryptedResult
, object
);
298 it('encrypts and decrypts default version, coerced buffer contents', async
function () {
300 object
= 'a string a string';
301 const encryptedResult
= await mb
.pack(object
, undefined, mb
.Flags
.BufferPayload
| mb
.defaultFlags
);
302 const decryptedResult
= await mb
.unpack(encryptedResult
);
303 assert
.deepStrictEqual(decryptedResult
, Buffer
.from(object
));
306 it('decrypts secondary (older) secret', async
function () {
308 const oldmb
= new MysteryBox({ encryptionSecret: 'old secret' });
310 const newmb
= new MysteryBox({ encryptionSecret: ['new secret', 'old secret'] });
317 const oldEncrypted
= await oldmb
.pack(object
);
318 const newDecrypted
= await newmb
.unpack(oldEncrypted
);
319 assert
.deepStrictEqual(newDecrypted
, object
);
322 it('fails to decrypt invalid secret', async
function () {
324 const oldmb
= new MysteryBox({ encryptionSecret: 'very old secret' });
326 const newmb
= new MysteryBox({ encryptionSecret: ['new secret', 'old secret'] });
333 const oldEncrypted
= await oldmb
.pack(object
);
334 assert
.rejects(() => newmb
.unpack(oldEncrypted
));
337 it('encrypts and decrypts all available versions no compression', async
function () {
338 Object
.keys(mb
.versionParameters
).map((v
) => Number(v
)).forEach(async (version
) => {
344 const encryptedResult
= await mb
.pack(object
, version
, 0x00);
345 const decryptedResult
= await mb
.unpack(encryptedResult
);
346 assert
.deepStrictEqual(decryptedResult
, object
, `${version} results not symmetric`);
350 it('encrypts and decrypts all available versions +brotli', async
function () {
351 Object
.keys(mb
.versionParameters
).map((v
) => Number(v
)).forEach(async (version
) => {
357 const encryptedResult
= await mb
.pack(object
, version
, mb
.Flags
.Brotli
);
358 const decryptedResult
= await mb
.unpack(encryptedResult
);
359 assert
.deepStrictEqual(decryptedResult
, object
, `${version} results not symmetric`);
363 it('encrypts and decrypts all available versions +flate', async
function () {
364 Object
.keys(mb
.versionParameters
).map((v
) => Number(v
)).forEach(async (version
) => {
370 const encryptedResult
= await mb
.pack(object
, version
, mb
.Flags
.Flate
);
371 const decryptedResult
= await mb
.unpack(encryptedResult
);
372 assert
.deepStrictEqual(decryptedResult
, object
, `${version} results not symmetric`);
376 it('handles large object +brotli', async
function () {
379 const firstChar
= 32, lastChar
= 126;
381 return Math
.floor(firstChar
+ (lastChar
- firstChar
+ 1) * Math
.random());
384 longProperty: 'x'.repeat(384 * 1024).split('').map(() => String
.fromCharCode(rnd())).join(''),
386 const encryptedResult
= await mb
.pack(object
, mb
.bestVersion
, mb
.Flags
.Brotli
);
387 const decryptedResult
= await mb
.unpack(encryptedResult
);
388 assert
.deepStrictEqual(decryptedResult
, object
);
391 it('handles large object +flate', async
function () {
394 const firstChar
= 32, lastChar
= 126;
396 return Math
.floor(firstChar
+ (lastChar
- firstChar
+ 1) * Math
.random());
399 longProperty: 'x'.repeat(384 * 1024).split('').map(() => String
.fromCharCode(rnd())).join(''),
401 const encryptedResult
= await mb
.pack(object
, mb
.bestVersion
, mb
.Flags
.Flate
);
402 const decryptedResult
= await mb
.unpack(encryptedResult
);
403 assert
.deepStrictEqual(decryptedResult
, object
);
406 it('handles undefined', async
function () {
407 assert
.rejects(() => mb
.unpack(), MysteryBoxError
);
410 it('handles incomplete', async
function () {
412 const encryptedResult
= await mb
.pack({ foo: 'bar' });
413 assert
.rejects(() => mb
.unpack(encryptedResult
.slice(0, 6)), MysteryBoxError
);
416 it('covers internal error, incorrect version byte size, pack', async
function () {
419 sinon
.stub(mb
.versionParameters
[version
], 'versionBytes').value(10);
420 assert
.rejects(() => mb
.pack({}, version
), Error
);
423 it('covers internal error, incorrect version byte size, unpack', async
function () {
426 const encryptedResult
= await mb
.pack({}, version
);
427 sinon
.stub(mb
.versionParameters
[version
], 'versionBytes').value(10);
428 assert
.rejects(() => mb
.unpack(encryptedResult
), Error
);