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