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