constructor no longer accepts logger, emit statistics events instead
[squeep-mystery-box] / test / lib / mystery-box.js
1 /* eslint-env mocha */
2 /* eslint-disable capitalized-comments */
3 'use strict';
4
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 crypto = require('crypto');
9
10 function _verbose(mb) {
11 if (process.env.VERBOSE_TESTS) {
12 mb.on('statistics', (...args) => console.log(...args));
13 }
14 }
15
16 describe('MysteryBox', function () {
17 let mb, options, object;
18 beforeEach(function () {
19 options = {
20 encryptionSecret: 'this is not a very good secret',
21 };
22 });
23 afterEach(function () {
24 sinon.restore();
25 });
26
27 describe('constructor', function () {
28 it('needs a secret', async function () {
29 options = {};
30 assert.rejects(() => new MysteryBox(options));
31 });
32
33 it('accepts multiple secrets', async function () {
34 this.slow(500);
35 options = {
36 encryptionSecret: ['first poor secret', 'second poor secret'],
37 };
38 mb = new MysteryBox(options);
39 _verbose(mb);
40 object = {
41 foo: 'bar',
42 baz: 'quux',
43 flarp: 13,
44 };
45 const encryptedResult = await mb.pack(object);
46 const decryptedResult = await mb.unpack(encryptedResult);
47 assert.deepStrictEqual(decryptedResult, object);
48 });
49
50 it('covers options', function () {
51 assert.rejects(() => new MysteryBox());
52 });
53
54 it('covers bad flags', function () {
55 options.defaultFlags = 300;
56 assert.rejects(() => new MysteryBox(options), RangeError);
57 });
58
59 it('covers missing ciphers', function () {
60 sinon.stub(crypto, 'getCiphers').returns(['rot13']);
61 assert.rejects(() => new MysteryBox(options));
62 });
63 }); // constructor
64
65 describe('_keyFromSecret', function () {
66 it('covers invalid', async function () {
67 assert.rejects(() => MysteryBox._keyFromSecret('unknown deriver', 'secret', 'salt', 32), RangeError);
68 });
69 }); // _keyFromSecret
70
71 describe('_versionHeaderDecode', function () {
72 function _check(firstByte, numBytes, value) {
73 const result = MysteryBox._versionHeaderDecode(firstByte);
74 assert.strictEqual(result.numBytes, numBytes);
75 assert.strictEqual(result.firstByte, value);
76 }
77 it('decodes single byte, min', function () {
78 _check(0x00, 1, 0x00);
79 });
80 it('decodes single byte, max', function () {
81 _check(0x7f, 1, 0x7f);
82 });
83 it('decodes double byte, min', function () {
84 _check(0x80, 2, 0x00);
85 });
86 it('decodes double byte, max', function () {
87 _check(0xbf, 2, 0x3f);
88 });
89 it('decodes triple byte, min', function () {
90 _check(0xc0, 3, 0x00);
91 });
92 it('decodes triple byte, max', function () {
93 _check(0xdf, 3, 0x1f);
94 });
95 it('decodes quadruple byte, min', function () {
96 _check(0xe0, 4, 0x00);
97 });
98 it('decodes quadruple byte, max', function () {
99 _check(0xef, 4, 0x0f);
100 });
101 it('decodes quintuple byte, min', function () {
102 _check(0xf0, 5, 0x00);
103 });
104 it('decodes quintuple byte, max', function () {
105 _check(0xf7, 5, 0x07);
106 });
107 it('decodes sextuple byte, min', function () {
108 _check(0xf8, 6, 0x00);
109 });
110 it('decodes sextuple byte, max', function () {
111 _check(0xfb, 6, 0x03);
112 });
113 it('decodes septuple byte, min', function () {
114 _check(0xfc, 7, 0x00);
115 });
116 it('decodes septuple byte, max', function () {
117 _check(0xfd, 7, 0x01);
118 });
119 it('decodes double byte, min', function () {
120 _check(0xfe, 8, 0x00);
121 });
122 it('decodes double byte, max', function () {
123 _check(0xfe, 8, 0x00);
124 });
125 it('covers unsupported', function () {
126 assert.throws(() => MysteryBox._versionHeaderDecode(0xff), RangeError);
127 });
128 }); // _versionHeaderDecode
129
130 describe('_versionDecode', function () {
131 function _checkDecodeRange(start, end, numBytes, headerByte) {
132 const headerMask = ((0xff << (8 - numBytes)) & 0xff) >>> 0;
133 const hByte = ((0xff << (8 - numBytes + 1)) & 0xff) >>> 0;
134 assert.strictEqual(hByte, headerByte, `TEST ERROR: unexpected header for length, computed: ${hByte.toString(16)} passed: ${headerByte.toString(16)}`);
135 for (let v = start; v <= end; v++) {
136 const buffer = Buffer.alloc(numBytes);
137 buffer.writeUIntBE(v, 0, numBytes);
138 assert((buffer[0] & headerMask) === 0, `TEST ERROR: version ${v} encroached on headerByte 0x${headerByte.toString(16)} (${headerByte.toString(2)} & ${buffer[0].toString(2)})`);
139 buffer[0] = (buffer[0] | headerByte) >>> 0;
140 const { version, versionBytes } = MysteryBox._versionDecode(buffer);
141 assert.strictEqual(versionBytes, numBytes);
142 assert.strictEqual(version, v);
143 }
144 }
145 it('covers single-byte versions', function () {
146 _checkDecodeRange(0, 127, 1, 0x00);
147 });
148 it('covers double-byte versions', function () {
149 _checkDecodeRange(128, 136, 2, 0x80);
150 // ...
151 _checkDecodeRange(16375, 16383, 2, 0x80);
152 });
153 it('covers triple-byte versions', function () {
154 _checkDecodeRange(16384, 16390, 3, 0xc0);
155 // ...
156 _checkDecodeRange(2097145, 2097151, 3, 0xc0);
157 });
158 it('covers quadruple-byte versions', function () {
159 _checkDecodeRange(2097151, 2097160, 4, 0xe0);
160 // ...
161 _checkDecodeRange(268435445, 268435455, 4, 0xe0);
162 });
163 it('covers quintuple-byte versions', function () {
164 _checkDecodeRange(268435445, 268435445, 5, 0xf0);
165 // ...
166 _checkDecodeRange(34359738360, 34359738367, 5, 0xf0);
167 });
168 it('covers sextuple-byte versions', function () {
169 _checkDecodeRange(34359738367, 34359738375, 6, 0xf8);
170 // ...
171 _checkDecodeRange(4398046511093, 4398046511103, 6, 0xf8);
172 });
173 it('covers too big', function () {
174 const buffer = Buffer.from([0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
175 assert.throws(() => MysteryBox._versionDecode(buffer), RangeError);
176 });
177 }); // _versionDecode
178
179 describe('_versionEncode', function () {
180 function _checkEncodeRange(start, end, bytes, headerBits) {
181 for (let version = start; version <= end; version++) {
182 const expected = Buffer.alloc(bytes);
183 expected.writeUIntBE(version, 0, bytes);
184 expected[0] = (expected[0] | headerBits) >>> 0;
185
186 const { buffer, versionBytes } = MysteryBox._versionEncode(version);
187 assert.deepStrictEqual(versionBytes, bytes, `version ${version} has ${versionBytes} bytes instead of expected ${bytes}`);
188 assert.deepStrictEqual(buffer, expected, `version ${version} buffer not expected: ${JSON.stringify(buffer)} vs ${JSON.stringify(expected)}`);
189 }
190 }
191 function _cheeckReciprical(version) {
192 const { buffer, versionBytes: encVB } = MysteryBox._versionEncode(version);
193 const { version: decV, versionBytes: decVB } = MysteryBox._versionDecode(buffer);
194 assert.strictEqual(encVB, decVB, `differing lengths for ${version}: enc:${encVB} dec:${decVB}`);
195 assert.strictEqual(decV, version, `failed for ${version}: ${JSON.stringify({ buffer, versionBytes: encVB })}`);
196 }
197 it('covers single-byte-packable versions', function () {
198 _checkEncodeRange(0, 127, 1, 0x00);
199 });
200 it('covers double-byte-packable versions', function () {
201 _checkEncodeRange(128, 200, 2, 0x80);
202 /* ... */
203 _checkEncodeRange(16380, 16383, 2, 0x80);
204 });
205 it('covers triple-byte-packable versions', function () {
206 _checkEncodeRange(16384, 16390, 3, 0xc0);
207 /* ... */
208 _checkEncodeRange(2097141, 2097151, 3, 0xc0);
209 });
210 it('covers quadruple-byte-packable versions', function () {
211 _checkEncodeRange(2097152, 2097161, 4, 0xe0);
212 /* ... */
213 _checkEncodeRange(268435445, 268435455, 4, 0xe0);
214 });
215 it('covers quintuple-byte-packable versions', function () {
216 _checkEncodeRange(268435456, 268435465, 5, 0xf0)
217 /* ... */
218 _checkEncodeRange(4294967294, 4294967296, 5, 0xf0)
219 /* ... */
220 _checkEncodeRange(34359738360, 34359738367, 5, 0xf0)
221 });
222 it('covers sextuple-byte-packable versions', function () {
223 _checkEncodeRange(34359738368, 34359738377, 6, 0xf8)
224 /* ... */
225 _checkEncodeRange(4398046511093, 4398046511103, 6, 0xf8)
226 });
227 it('covers too large', function () {
228 const version = 277076930199552;
229 assert.rejects(() => MysteryBox._versionEncode(version), RangeError);
230 });
231 it('recipricates', function () {
232 [
233 0, 127,
234 128, 16383,
235 16384, 2097151,
236 2097152, 268435455,
237 268435456, 34359738367,
238 34359738368, 4398046511103,
239 ].forEach((v) => _cheeckReciprical(v));
240 });
241 }); // _versionEncode
242
243 describe('pack, unpack', function () {
244 beforeEach(function () {
245 mb = new MysteryBox(options);
246 _verbose(mb);
247 });
248
249 it('covers packing unsupported version', async function () {
250 assert.rejects(() => mb.pack({}, 0), RangeError);
251 });
252
253 it('covers unpacking unsupported version', async function () {
254 const badBuffer = Buffer.alloc(128);
255 badBuffer.writeUInt8(0, 0); // No such thing as version 0
256 const badPayload = badBuffer.toString('base64url');
257 assert.rejects(() => mb.unpack(badPayload), RangeError);
258 });
259
260 it('encrypts and decrypts default version', async function () {
261 this.slow(500);
262 object = {
263 foo: 'bar',
264 baz: 'quux',
265 flarp: 13,
266 };
267 const encryptedResult = await mb.pack(object);
268 const decryptedResult = await mb.unpack(encryptedResult);
269 assert.deepStrictEqual(decryptedResult, object);
270 });
271
272 it('encrypts and decrypts default version, buffer contents', async function () {
273 this.slow(500);
274 object = Buffer.from('a fine little buffer');
275 const encryptedResult = await mb.pack(object);
276 const decryptedResult = await mb.unpack(encryptedResult);
277 assert.deepStrictEqual(decryptedResult, object);
278 });
279
280 it('encrypts and decrypts default version, coerced buffer contents', async function () {
281 this.slow(500);
282 object = 'a string a string';
283 const encryptedResult = await mb.pack(object, undefined, mb.Flags.BufferPayload | mb.defaultFlags);
284 const decryptedResult = await mb.unpack(encryptedResult);
285 assert.deepStrictEqual(decryptedResult, Buffer.from(object));
286 });
287
288 it('decrypts secondary (older) secret', async function () {
289 this.slow(500);
290 const oldmb = new MysteryBox({ encryptionSecret: 'old secret' });
291 _verbose(oldmb);
292 const newmb = new MysteryBox({ encryptionSecret: ['new secret', 'old secret'] });
293 _verbose(newmb);
294 object = {
295 foo: 'bar',
296 baz: 'quux',
297 flarp: 13,
298 };
299 const oldEncrypted = await oldmb.pack(object);
300 const newDecrypted = await newmb.unpack(oldEncrypted);
301 assert.deepStrictEqual(newDecrypted, object);
302 });
303
304 it('fails to decrypt invalid secret', async function () {
305 this.slow(500);
306 const oldmb = new MysteryBox({ encryptionSecret: 'very old secret' });
307 _verbose(oldmb);
308 const newmb = new MysteryBox({ encryptionSecret: ['new secret', 'old secret'] });
309 _verbose(newmb);
310 object = {
311 foo: 'bar',
312 baz: 'quux',
313 flarp: 13,
314 };
315 const oldEncrypted = await oldmb.pack(object);
316 assert.rejects(() => newmb.unpack(oldEncrypted));
317 });
318
319 it('encrypts and decrypts all available versions no compression', async function () {
320 Object.keys(mb.versionParameters).map((v) => Number(v)).forEach(async (version) => {
321 object = {
322 foo: 'bar',
323 baz: 'quux',
324 flarp: 13,
325 };
326 const encryptedResult = await mb.pack(object, version, 0x00);
327 const decryptedResult = await mb.unpack(encryptedResult);
328 assert.deepStrictEqual(decryptedResult, object, `${version} results not symmetric`);
329 });
330 });
331
332 it('encrypts and decrypts all available versions +brotli', async function () {
333 Object.keys(mb.versionParameters).map((v) => Number(v)).forEach(async (version) => {
334 object = {
335 foo: 'bar',
336 baz: 'quux',
337 flarp: 13,
338 };
339 const encryptedResult = await mb.pack(object, version, mb.Flags.Brotli);
340 const decryptedResult = await mb.unpack(encryptedResult);
341 assert.deepStrictEqual(decryptedResult, object, `${version} results not symmetric`);
342 });
343 });
344
345 it('encrypts and decrypts all available versions +flate', async function () {
346 Object.keys(mb.versionParameters).map((v) => Number(v)).forEach(async (version) => {
347 object = {
348 foo: 'bar',
349 baz: 'quux',
350 flarp: 13,
351 };
352 const encryptedResult = await mb.pack(object, version, mb.Flags.Flate);
353 const decryptedResult = await mb.unpack(encryptedResult);
354 assert.deepStrictEqual(decryptedResult, object, `${version} results not symmetric`);
355 });
356 });
357
358 it('handles large object +brotli', async function () {
359 this.timeout(5000);
360 this.slow(2000);
361 const firstChar = 32, lastChar = 126;
362 const rnd = () => {
363 return Math.floor(firstChar + (lastChar - firstChar + 1) * Math.random());
364 }
365 object = {
366 longProperty: 'x'.repeat(384 * 1024).split('').map(() => String.fromCharCode(rnd())).join(''),
367 };
368 const encryptedResult = await mb.pack(object, mb.bestVersion, mb.Flags.Brotli);
369 const decryptedResult = await mb.unpack(encryptedResult);
370 assert.deepStrictEqual(decryptedResult, object);
371 });
372
373 it('handles large object +flate', async function () {
374 this.timeout(5000);
375 this.slow(2000);
376 const firstChar = 32, lastChar = 126;
377 const rnd = () => {
378 return Math.floor(firstChar + (lastChar - firstChar + 1) * Math.random());
379 }
380 object = {
381 longProperty: 'x'.repeat(384 * 1024).split('').map(() => String.fromCharCode(rnd())).join(''),
382 };
383 const encryptedResult = await mb.pack(object, mb.bestVersion, mb.Flags.Flate);
384 const decryptedResult = await mb.unpack(encryptedResult);
385 assert.deepStrictEqual(decryptedResult, object);
386 });
387
388 it('handles undefined', async function () {
389 assert.rejects(() => mb.unpack(), RangeError);
390 });
391
392 it('handles incomplete', async function () {
393 this.slow(500);
394 const encryptedResult = await mb.pack({ foo: 'bar' });
395 assert.rejects(() => mb.unpack(encryptedResult.slice(0, 6)), RangeError);
396 });
397
398 it('covers internal error, incorrect version byte size, pack', async function () {
399 this.slow(500);
400 const version = 1;
401 sinon.stub(mb.versionParameters[version], 'versionBytes').value(10);
402 assert.rejects(() => mb.pack({}, version), Error);
403 });
404
405 it('covers internal error, incorrect version byte size, unpack', async function () {
406 this.slow(500);
407 const version = 1;
408 const encryptedResult = await mb.pack({}, version);
409 sinon.stub(mb.versionParameters[version], 'versionBytes').value(10);
410 assert.rejects(() => mb.unpack(encryptedResult), Error);
411 });
412
413 }); // pack, unpack
414
415 }); // MysteryBox