support new key derivations, add new preferred versions using such
[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 const noExpectedException = 'did not get expected exception';
12 let mb, options, object;
13 beforeEach(function () {
14 options = {
15 encryptionSecret: 'this is not a very good secret',
16 };
17 });
18 afterEach(function () {
19 sinon.restore();
20 });
21
22 describe('constructor', function () {
23 it('needs a secret', async function () {
24 options = {};
25 try {
26 mb = new MysteryBox(stubLogger, options);
27 assert.fail(noExpectedException);
28 } catch (e) {
29 assert.strictEqual(e.message, 'missing encryption secret', noExpectedException);
30 }
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(stubLogger, options);
39 object = {
40 foo: 'bar',
41 baz: 'quux',
42 flarp: 13,
43 };
44 const encryptedResult = await mb.pack(object);
45 const decryptedResult = await mb.unpack(encryptedResult);
46 assert.deepStrictEqual(decryptedResult, object);
47 });
48
49 it('covers options', function () {
50 try {
51 mb = new MysteryBox(stubLogger);
52 assert.fail(noExpectedException);
53 } catch (e) {
54 assert.strictEqual(e.message, 'missing encryption secret', noExpectedException);
55 }
56 });
57
58 it('covers bad flags', function () {
59 options.defaultFlags = 300;
60 try {
61 mb = new MysteryBox(stubLogger, options);
62 assert.fail(noExpectedException);
63 } catch (e) {
64 assert(e instanceof RangeError, noExpectedException);
65 }
66 });
67
68 it('covers missing ciphers', function () {
69 sinon.stub(MysteryBox._test.crypto, 'getCiphers').returns(['rot13']);
70 try {
71 mb = new MysteryBox(stubLogger, options);
72 assert.fail(noExpectedException);
73 } catch (e) {
74 assert.strictEqual(e.message, 'no supported versions available', noExpectedException);
75 }
76 });
77 }); // constructor
78
79 describe('_keyFromSecret', function () {
80 it('covers invalid', async function () {
81 try {
82 await MysteryBox._keyFromSecret('unknown deriver', 'secret', 'salt', 32);
83 } catch (e) {
84 assert(e instanceof RangeError);
85 }
86 });
87 }); // _keyFromSecret
88
89 describe('pack, unpack', function () {
90 beforeEach(function () {
91 mb = new MysteryBox(stubLogger, options);
92 });
93
94 it('covers packing unsupported version', async function () {
95 try {
96 await mb.pack({}, 0);
97 assert.fail(noExpectedException);
98 } catch (e) {
99 assert(e instanceof RangeError, noExpectedException);
100 }
101 });
102
103 it('covers unpacking unsupported version', async function () {
104 const badBuffer = Buffer.alloc(128);
105 badBuffer.writeUInt8(0, 0); // No such thing as version 0
106 const badPayload = badBuffer.toString('base64');
107 try {
108 await mb.unpack(badPayload);
109 assert.fail(noExpectedException);
110 } catch (e) {
111 assert(e instanceof RangeError, noExpectedException);
112 }
113 });
114
115 it('encrypts and decrypts default version', async function () {
116 this.slow(500);
117 object = {
118 foo: 'bar',
119 baz: 'quux',
120 flarp: 13,
121 };
122 const encryptedResult = await mb.pack(object);
123 const decryptedResult = await mb.unpack(encryptedResult);
124 assert.deepStrictEqual(decryptedResult, object);
125 });
126
127 it('encrypts and decrypts default version, buffer contents', async function () {
128 this.slow(500);
129 object = Buffer.from('a fine little buffer');
130 const encryptedResult = await mb.pack(object);
131 const decryptedResult = await mb.unpack(encryptedResult);
132 assert.deepStrictEqual(decryptedResult, object);
133 });
134
135 it('decrypts secondary (older) secret', async function () {
136 this.slow(500);
137 const oldmb = new MysteryBox(stubLogger, { encryptionSecret: 'old secret' });
138 const newmb = new MysteryBox(stubLogger, { encryptionSecret: ['new secret', 'old secret'] });
139 object = {
140 foo: 'bar',
141 baz: 'quux',
142 flarp: 13,
143 };
144 const oldEncrypted = await oldmb.pack(object);
145 const newDecrypted = await newmb.unpack(oldEncrypted);
146 assert.deepStrictEqual(newDecrypted, object);
147 });
148
149 it('fails to decrypt invalid secret', async function () {
150 this.slow(500);
151 const oldmb = new MysteryBox(stubLogger, { encryptionSecret: 'very old secret' });
152 const newmb = new MysteryBox(stubLogger, { encryptionSecret: ['new secret', 'old secret'] });
153 object = {
154 foo: 'bar',
155 baz: 'quux',
156 flarp: 13,
157 };
158 const oldEncrypted = await oldmb.pack(object);
159 try {
160 await newmb.unpack(oldEncrypted);
161 assert.fail(noExpectedException);
162 } catch (e) {
163 assert(e instanceof Error);
164 }
165 });
166
167 it('encrypts and decrypts all available versions +brotli', async function () {
168 Object.keys(mb.versionParameters).map((v) => Number(v)).forEach(async (version) => {
169 object = {
170 foo: 'bar',
171 baz: 'quux',
172 flarp: 13,
173 };
174 const encryptedResult = await mb.pack(object, version, 0x00);
175 const decryptedResult = await mb.unpack(encryptedResult);
176 assert.deepStrictEqual(decryptedResult, object, `${version} results not symmetric`);
177 });
178 });
179
180 it('encrypts and decrypts all available versions +flate', async function () {
181 Object.keys(mb.versionParameters).map((v) => Number(v)).forEach(async (version) => {
182 object = {
183 foo: 'bar',
184 baz: 'quux',
185 flarp: 13,
186 };
187 const encryptedResult = await mb.pack(object, version, 0x01);
188 const decryptedResult = await mb.unpack(encryptedResult);
189 assert.deepStrictEqual(decryptedResult, object, `${version} results not symmetric`);
190 });
191 });
192
193 it('handles large object +brotli', async function () {
194 this.timeout(5000);
195 this.slow(2000);
196 const firstChar = 32, lastChar = 126;
197 const rnd = () => {
198 return Math.floor(firstChar + (lastChar - firstChar + 1) * Math.random());
199 }
200 object = {
201 longProperty: 'x'.repeat(384 * 1024).split('').map(() => String.fromCharCode(rnd())).join(''),
202 };
203 const encryptedResult = await mb.pack(object, mb.bestVersion, 0x00);
204 const decryptedResult = await mb.unpack(encryptedResult);
205 assert.deepStrictEqual(decryptedResult, object);
206 });
207
208 it('handles large object +flate', async function () {
209 this.timeout(5000);
210 this.slow(2000);
211 const firstChar = 32, lastChar = 126;
212 const rnd = () => {
213 return Math.floor(firstChar + (lastChar - firstChar + 1) * Math.random());
214 }
215 object = {
216 longProperty: 'x'.repeat(384 * 1024).split('').map(() => String.fromCharCode(rnd())).join(''),
217 };
218 const encryptedResult = await mb.pack(object, mb.bestVersion, 0x01);
219 const decryptedResult = await mb.unpack(encryptedResult);
220 assert.deepStrictEqual(decryptedResult, object);
221 });
222
223 it('handles undefined', async function () {
224 try {
225 await mb.unpack();
226 assert.fail(noExpectedException);
227 } catch (e) {
228 assert(e instanceof RangeError, noExpectedException);
229 }
230 });
231
232 it('handles incomplete', async function () {
233 this.slow(500);
234 const encryptedResult = await mb.pack({ foo: 'bar' });
235 try {
236 await mb.unpack(encryptedResult.slice(0, 6));
237 assert.fail(noExpectedException);
238 } catch (e) {
239 assert(e instanceof RangeError, noExpectedException);
240 }
241 });
242
243 }); // pack, unpack
244
245 }); // MysteryBox