remove now-unused function, consolidate remaining common functions
[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 { MysteryBoxError } = require('../../lib/errors');
9 const crypto = require('crypto');
10
11 function _verbose(mb) {
12 if (process.env.VERBOSE_TESTS) {
13 mb.on('statistics', (...args) => console.log(...args));
14 }
15 }
16
17 describe('MysteryBox', function () {
18 let mb, options, object;
19 beforeEach(function () {
20 options = {
21 encryptionSecret: 'this is not a very good secret',
22 };
23 });
24 afterEach(function () {
25 sinon.restore();
26 });
27
28 describe('constructor', function () {
29 it('needs a secret', async function () {
30 options = {};
31 assert.rejects(() => new MysteryBox(options));
32 });
33
34 it('accepts multiple secrets', async function () {
35 this.slow(500);
36 options = {
37 encryptionSecret: ['first poor secret', 'second poor secret'],
38 };
39 mb = new MysteryBox(options);
40 _verbose(mb);
41 object = {
42 foo: 'bar',
43 baz: 'quux',
44 flarp: 13,
45 };
46 const encryptedResult = await mb.pack(object);
47 const decryptedResult = await mb.unpack(encryptedResult);
48 assert.deepStrictEqual(decryptedResult, object);
49 });
50
51 it('covers options', function () {
52 assert.rejects(() => new MysteryBox());
53 });
54
55 it('covers bad flags', function () {
56 options.defaultFlags = 300;
57 assert.rejects(() => new MysteryBox(options), MysteryBoxError);
58 });
59
60 it('covers missing ciphers', function () {
61 sinon.stub(crypto, 'getCiphers').returns(['rot13']);
62 assert.rejects(() => new MysteryBox(options));
63 });
64 }); // constructor
65
66 describe('_ensureArray', function () {
67 it('returns empty array for no data', function () {
68 const result = MysteryBox._ensureArray();
69 assert.deepStrictEqual(result, []);
70 });
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);
75 });
76 it('returns array containing non-array data', function () {
77 const data = 'bar';
78 const result = MysteryBox._ensureArray(data);
79 assert.deepStrictEqual(result, [data]);
80 });
81 }); // _ensureArray
82
83 describe('_keyFromSecret', function () {
84 it('covers invalid', async function () {
85 assert.rejects(() => MysteryBox._keyFromSecret('unknown deriver', 'secret', 'salt', 32), MysteryBoxError);
86 });
87 }); // _keyFromSecret
88
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);
94 }
95 it('decodes single byte, min', function () {
96 _check(0x00, 1, 0x00);
97 });
98 it('decodes single byte, max', function () {
99 _check(0x7f, 1, 0x7f);
100 });
101 it('decodes double byte, min', function () {
102 _check(0x80, 2, 0x00);
103 });
104 it('decodes double byte, max', function () {
105 _check(0xbf, 2, 0x3f);
106 });
107 it('decodes triple byte, min', function () {
108 _check(0xc0, 3, 0x00);
109 });
110 it('decodes triple byte, max', function () {
111 _check(0xdf, 3, 0x1f);
112 });
113 it('decodes quadruple byte, min', function () {
114 _check(0xe0, 4, 0x00);
115 });
116 it('decodes quadruple byte, max', function () {
117 _check(0xef, 4, 0x0f);
118 });
119 it('decodes quintuple byte, min', function () {
120 _check(0xf0, 5, 0x00);
121 });
122 it('decodes quintuple byte, max', function () {
123 _check(0xf7, 5, 0x07);
124 });
125 it('decodes sextuple byte, min', function () {
126 _check(0xf8, 6, 0x00);
127 });
128 it('decodes sextuple byte, max', function () {
129 _check(0xfb, 6, 0x03);
130 });
131 it('decodes septuple byte, min', function () {
132 _check(0xfc, 7, 0x00);
133 });
134 it('decodes septuple byte, max', function () {
135 _check(0xfd, 7, 0x01);
136 });
137 it('decodes double byte, min', function () {
138 _check(0xfe, 8, 0x00);
139 });
140 it('decodes double byte, max', function () {
141 _check(0xfe, 8, 0x00);
142 });
143 it('covers unsupported', function () {
144 assert.throws(() => MysteryBox._versionHeaderDecode(0xff), MysteryBoxError);
145 });
146 }); // _versionHeaderDecode
147
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);
161 }
162 }
163 it('covers single-byte versions', function () {
164 _checkDecodeRange(0, 127, 1, 0x00);
165 });
166 it('covers double-byte versions', function () {
167 _checkDecodeRange(128, 136, 2, 0x80);
168 // ...
169 _checkDecodeRange(16375, 16383, 2, 0x80);
170 });
171 it('covers triple-byte versions', function () {
172 _checkDecodeRange(16384, 16390, 3, 0xc0);
173 // ...
174 _checkDecodeRange(2097145, 2097151, 3, 0xc0);
175 });
176 it('covers quadruple-byte versions', function () {
177 _checkDecodeRange(2097151, 2097160, 4, 0xe0);
178 // ...
179 _checkDecodeRange(268435445, 268435455, 4, 0xe0);
180 });
181 it('covers quintuple-byte versions', function () {
182 _checkDecodeRange(268435445, 268435445, 5, 0xf0);
183 // ...
184 _checkDecodeRange(34359738360, 34359738367, 5, 0xf0);
185 });
186 it('covers sextuple-byte versions', function () {
187 _checkDecodeRange(34359738367, 34359738375, 6, 0xf8);
188 // ...
189 _checkDecodeRange(4398046511093, 4398046511103, 6, 0xf8);
190 });
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);
194 });
195 }); // _versionDecode
196
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;
203
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)}`);
207 }
208 }
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 })}`);
214 }
215 it('covers single-byte-packable versions', function () {
216 _checkEncodeRange(0, 127, 1, 0x00);
217 });
218 it('covers double-byte-packable versions', function () {
219 _checkEncodeRange(128, 200, 2, 0x80);
220 /* ... */
221 _checkEncodeRange(16380, 16383, 2, 0x80);
222 });
223 it('covers triple-byte-packable versions', function () {
224 _checkEncodeRange(16384, 16390, 3, 0xc0);
225 /* ... */
226 _checkEncodeRange(2097141, 2097151, 3, 0xc0);
227 });
228 it('covers quadruple-byte-packable versions', function () {
229 _checkEncodeRange(2097152, 2097161, 4, 0xe0);
230 /* ... */
231 _checkEncodeRange(268435445, 268435455, 4, 0xe0);
232 });
233 it('covers quintuple-byte-packable versions', function () {
234 _checkEncodeRange(268435456, 268435465, 5, 0xf0)
235 /* ... */
236 _checkEncodeRange(4294967294, 4294967296, 5, 0xf0)
237 /* ... */
238 _checkEncodeRange(34359738360, 34359738367, 5, 0xf0)
239 });
240 it('covers sextuple-byte-packable versions', function () {
241 _checkEncodeRange(34359738368, 34359738377, 6, 0xf8)
242 /* ... */
243 _checkEncodeRange(4398046511093, 4398046511103, 6, 0xf8)
244 });
245 it('covers too large', function () {
246 const version = 277076930199552;
247 assert.rejects(() => MysteryBox._versionEncode(version), MysteryBoxError);
248 });
249 it('recipricates', function () {
250 [
251 0, 127,
252 128, 16383,
253 16384, 2097151,
254 2097152, 268435455,
255 268435456, 34359738367,
256 34359738368, 4398046511103,
257 ].forEach((v) => _cheeckReciprical(v));
258 });
259 }); // _versionEncode
260
261 describe('pack, unpack', function () {
262 beforeEach(function () {
263 mb = new MysteryBox(options);
264 _verbose(mb);
265 });
266
267 it('covers packing unsupported version', async function () {
268 assert.rejects(() => mb.pack({}, 0), MysteryBoxError);
269 });
270
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);
276 });
277
278 it('encrypts and decrypts default version', async function () {
279 this.slow(500);
280 object = {
281 foo: 'bar',
282 baz: 'quux',
283 flarp: 13,
284 };
285 const encryptedResult = await mb.pack(object);
286 const decryptedResult = await mb.unpack(encryptedResult);
287 assert.deepStrictEqual(decryptedResult, object);
288 });
289
290 it('encrypts and decrypts default version, buffer contents', async function () {
291 this.slow(500);
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);
296 });
297
298 it('encrypts and decrypts default version, coerced buffer contents', async function () {
299 this.slow(500);
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));
304 });
305
306 it('decrypts secondary (older) secret', async function () {
307 this.slow(500);
308 const oldmb = new MysteryBox({ encryptionSecret: 'old secret' });
309 _verbose(oldmb);
310 const newmb = new MysteryBox({ encryptionSecret: ['new secret', 'old secret'] });
311 _verbose(newmb);
312 object = {
313 foo: 'bar',
314 baz: 'quux',
315 flarp: 13,
316 };
317 const oldEncrypted = await oldmb.pack(object);
318 const newDecrypted = await newmb.unpack(oldEncrypted);
319 assert.deepStrictEqual(newDecrypted, object);
320 });
321
322 it('fails to decrypt invalid secret', async function () {
323 this.slow(500);
324 const oldmb = new MysteryBox({ encryptionSecret: 'very old secret' });
325 _verbose(oldmb);
326 const newmb = new MysteryBox({ encryptionSecret: ['new secret', 'old secret'] });
327 _verbose(newmb);
328 object = {
329 foo: 'bar',
330 baz: 'quux',
331 flarp: 13,
332 };
333 const oldEncrypted = await oldmb.pack(object);
334 assert.rejects(() => newmb.unpack(oldEncrypted));
335 });
336
337 it('encrypts and decrypts all available versions no compression', async function () {
338 Object.keys(mb.versionParameters).map((v) => Number(v)).forEach(async (version) => {
339 object = {
340 foo: 'bar',
341 baz: 'quux',
342 flarp: 13,
343 };
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`);
347 });
348 });
349
350 it('encrypts and decrypts all available versions +brotli', async function () {
351 Object.keys(mb.versionParameters).map((v) => Number(v)).forEach(async (version) => {
352 object = {
353 foo: 'bar',
354 baz: 'quux',
355 flarp: 13,
356 };
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`);
360 });
361 });
362
363 it('encrypts and decrypts all available versions +flate', async function () {
364 Object.keys(mb.versionParameters).map((v) => Number(v)).forEach(async (version) => {
365 object = {
366 foo: 'bar',
367 baz: 'quux',
368 flarp: 13,
369 };
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`);
373 });
374 });
375
376 it('handles large object +brotli', async function () {
377 this.timeout(5000);
378 this.slow(2000);
379 const firstChar = 32, lastChar = 126;
380 const rnd = () => {
381 return Math.floor(firstChar + (lastChar - firstChar + 1) * Math.random());
382 }
383 object = {
384 longProperty: 'x'.repeat(384 * 1024).split('').map(() => String.fromCharCode(rnd())).join(''),
385 };
386 const encryptedResult = await mb.pack(object, mb.bestVersion, mb.Flags.Brotli);
387 const decryptedResult = await mb.unpack(encryptedResult);
388 assert.deepStrictEqual(decryptedResult, object);
389 });
390
391 it('handles large object +flate', async function () {
392 this.timeout(5000);
393 this.slow(2000);
394 const firstChar = 32, lastChar = 126;
395 const rnd = () => {
396 return Math.floor(firstChar + (lastChar - firstChar + 1) * Math.random());
397 }
398 object = {
399 longProperty: 'x'.repeat(384 * 1024).split('').map(() => String.fromCharCode(rnd())).join(''),
400 };
401 const encryptedResult = await mb.pack(object, mb.bestVersion, mb.Flags.Flate);
402 const decryptedResult = await mb.unpack(encryptedResult);
403 assert.deepStrictEqual(decryptedResult, object);
404 });
405
406 it('handles undefined', async function () {
407 assert.rejects(() => mb.unpack(), MysteryBoxError);
408 });
409
410 it('handles incomplete', async function () {
411 this.slow(500);
412 const encryptedResult = await mb.pack({ foo: 'bar' });
413 assert.rejects(() => mb.unpack(encryptedResult.slice(0, 6)), MysteryBoxError);
414 });
415
416 it('covers internal error, incorrect version byte size, pack', async function () {
417 this.slow(500);
418 const version = 1;
419 sinon.stub(mb.versionParameters[version], 'versionBytes').value(10);
420 assert.rejects(() => mb.pack({}, version), Error);
421 });
422
423 it('covers internal error, incorrect version byte size, unpack', async function () {
424 this.slow(500);
425 const version = 1;
426 const encryptedResult = await mb.pack({}, version);
427 sinon.stub(mb.versionParameters[version], 'versionBytes').value(10);
428 assert.rejects(() => mb.unpack(encryptedResult), Error);
429 });
430
431 }); // pack, unpack
432
433 }); // MysteryBox