1
1
import * as crypto from "crypto-js" ;
2
2
import { BundleManager } from "../bundleManager" ;
3
3
import { InstanceManager } from "../instanceManager" ;
4
- import { decryptData , EncryptedData , PersistenceManager , PersistentData } from "../persistenceManager" ;
4
+ import {
5
+ decryptData ,
6
+ deriveEncryptionKey ,
7
+ EncryptedData ,
8
+ PersistenceManager ,
9
+ PersistentData ,
10
+ } from "../persistenceManager" ;
5
11
import { ServiceManager } from "../serviceManager" ;
6
12
import { ServiceProvider } from "../serviceProvider" ;
7
13
import { emptySuccess , error } from "../utils/result" ;
8
14
import { MockNodeCG , testBundle , testInstance , testService , testServiceInstance } from "./mocks" ;
9
15
10
16
describe ( "PersistenceManager" , ( ) => {
11
17
const validPassword = "myPassword" ;
12
- const invalidPassword = "someOtherPassword" ;
18
+ const invalidPassword = "myInvalidPassword" ;
19
+ const salt = crypto . lib . WordArray . random ( 128 / 8 ) . toString ( ) ;
20
+ const validEncryptionKey = deriveEncryptionKey ( validPassword , salt ) . toString ( ) ;
21
+ const invalidEncryptionKey = deriveEncryptionKey ( invalidPassword , salt ) . toString ( ) ;
13
22
14
23
const nodecg = new MockNodeCG ( ) ;
15
24
const serviceManager = new ServiceManager ( nodecg ) ;
@@ -39,7 +48,7 @@ describe("PersistenceManager", () => {
39
48
* Creates a basic config and encrypts it. Used to check whether load decrypts and more importantly
40
49
* restores the same configuration again.
41
50
*/
42
- function generateEncryptedConfig ( data ?: PersistentData ) {
51
+ function generateEncryptedConfig ( data ?: PersistentData ) : EncryptedData {
43
52
const d : PersistentData = data
44
53
? data
45
54
: {
@@ -56,22 +65,28 @@ describe("PersistenceManager", () => {
56
65
[ testInstance ] : testServiceInstance ,
57
66
} ,
58
67
} ;
59
- return crypto . AES . encrypt ( JSON . stringify ( d ) , validPassword ) . toString ( ) ;
68
+
69
+ const iv = crypto . lib . WordArray . random ( 16 ) ;
70
+ const encryptionKeyArray = crypto . enc . Hex . parse ( validEncryptionKey ) ;
71
+ return {
72
+ cipherText : crypto . AES . encrypt ( JSON . stringify ( d ) , encryptionKeyArray , { iv } ) . toString ( ) ,
73
+ iv : iv . toString ( ) ,
74
+ } ;
60
75
}
61
76
62
- describe ( "checkPassword " , ( ) => {
77
+ describe ( "checkEncryptionKey " , ( ) => {
63
78
test ( "should return false if not loaded" , ( ) => {
64
- expect ( persistenceManager . checkPassword ( validPassword ) ) . toBe ( false ) ;
79
+ expect ( persistenceManager . checkEncryptionKey ( validEncryptionKey ) ) . toBe ( false ) ;
65
80
} ) ;
66
81
67
82
test ( "should return false if loaded but password is wrong" , async ( ) => {
68
- await persistenceManager . load ( validPassword ) ;
69
- expect ( persistenceManager . checkPassword ( invalidPassword ) ) . toBe ( false ) ;
83
+ await persistenceManager . load ( validEncryptionKey ) ;
84
+ expect ( persistenceManager . checkEncryptionKey ( invalidEncryptionKey ) ) . toBe ( false ) ;
70
85
} ) ;
71
86
72
87
test ( "should return true if loaded and password is correct" , async ( ) => {
73
- await persistenceManager . load ( validPassword ) ;
74
- expect ( persistenceManager . checkPassword ( validPassword ) ) . toBe ( true ) ;
88
+ await persistenceManager . load ( validEncryptionKey ) ;
89
+ expect ( persistenceManager . checkEncryptionKey ( validEncryptionKey ) ) . toBe ( true ) ;
75
90
} ) ;
76
91
} ) ;
77
92
@@ -81,15 +96,15 @@ describe("PersistenceManager", () => {
81
96
} ) ;
82
97
83
98
test ( "should return false if load was called but failed" , async ( ) => {
84
- encryptedDataReplicant . value . cipherText = generateEncryptedConfig ( ) ;
85
- const res = await persistenceManager . load ( invalidPassword ) ; // Will fail because the password is invalid
99
+ encryptedDataReplicant . value = generateEncryptedConfig ( ) ;
100
+ const res = await persistenceManager . load ( invalidEncryptionKey ) ; // Will fail because the password is invalid
86
101
expect ( res . failed ) . toBe ( true ) ;
87
102
expect ( persistenceManager . isLoaded ( ) ) . toBe ( false ) ;
88
103
} ) ;
89
104
90
105
test ( "should return true if load was called and succeeded" , async ( ) => {
91
- encryptedDataReplicant . value . cipherText = generateEncryptedConfig ( ) ;
92
- const res = await persistenceManager . load ( validPassword ) ; // password is correct, should work
106
+ encryptedDataReplicant . value = generateEncryptedConfig ( ) ;
107
+ const res = await persistenceManager . load ( validEncryptionKey ) ; // password is correct, should work
93
108
expect ( res . failed ) . toBe ( false ) ;
94
109
expect ( persistenceManager . isLoaded ( ) ) . toBe ( true ) ;
95
110
} ) ;
@@ -102,49 +117,49 @@ describe("PersistenceManager", () => {
102
117
} ) ;
103
118
104
119
test ( "should return false if an encrypted config exists" , ( ) => {
105
- encryptedDataReplicant . value . cipherText = generateEncryptedConfig ( ) ; // config = not a first startup
120
+ encryptedDataReplicant . value = generateEncryptedConfig ( ) ; // config = not a first startup
106
121
expect ( persistenceManager . isFirstStartup ( ) ) . toBe ( false ) ;
107
122
} ) ;
108
123
} ) ;
109
124
110
125
describe ( "load" , ( ) => {
111
- beforeEach ( ( ) => ( encryptedDataReplicant . value . cipherText = generateEncryptedConfig ( ) ) ) ;
126
+ beforeEach ( ( ) => ( encryptedDataReplicant . value = generateEncryptedConfig ( ) ) ) ;
112
127
113
128
// General
114
129
115
130
test ( "should error if called after configuration already has been loaded" , async ( ) => {
116
- const res1 = await persistenceManager . load ( validPassword ) ;
131
+ const res1 = await persistenceManager . load ( validEncryptionKey ) ;
117
132
expect ( res1 . failed ) . toBe ( false ) ;
118
- const res2 = await persistenceManager . load ( validPassword ) ;
133
+ const res2 = await persistenceManager . load ( validEncryptionKey ) ;
119
134
expect ( res2 . failed ) . toBe ( true ) ;
120
135
if ( res2 . failed ) {
121
136
expect ( res2 . errorMessage ) . toContain ( "already been decrypted and loaded" ) ;
122
137
}
123
138
} ) ;
124
139
125
140
test ( "should save current state if no encrypted config was found" , async ( ) => {
126
- const res = await persistenceManager . load ( validPassword ) ;
141
+ const res = await persistenceManager . load ( validEncryptionKey ) ;
127
142
expect ( res . failed ) . toBe ( false ) ;
128
143
expect ( encryptedDataReplicant . value . cipherText ) . toBeDefined ( ) ;
129
144
} ) ;
130
145
131
146
test ( "should error if password is wrong" , async ( ) => {
132
- const res = await persistenceManager . load ( invalidPassword ) ;
147
+ const res = await persistenceManager . load ( invalidEncryptionKey ) ;
133
148
expect ( res . failed ) . toBe ( true ) ;
134
149
if ( res . failed ) {
135
150
expect ( res . errorMessage ) . toContain ( "Password isn't correct" ) ;
136
151
}
137
152
} ) ;
138
153
139
154
test ( "should succeed if password is correct" , async ( ) => {
140
- const res = await persistenceManager . load ( validPassword ) ;
155
+ const res = await persistenceManager . load ( validEncryptionKey ) ;
141
156
expect ( res . failed ) . toBe ( false ) ;
142
157
} ) ;
143
158
144
159
// Service instances
145
160
146
161
test ( "should load service instances including configuration" , async ( ) => {
147
- await persistenceManager . load ( validPassword ) ;
162
+ await persistenceManager . load ( validEncryptionKey ) ;
148
163
const inst = instanceManager . getServiceInstance ( testInstance ) ;
149
164
expect ( inst ) . toBeDefined ( ) ;
150
165
if ( ! inst ) return ;
@@ -153,21 +168,21 @@ describe("PersistenceManager", () => {
153
168
} ) ;
154
169
155
170
test ( "should log failures when creating service instances" , async ( ) => {
156
- encryptedDataReplicant . value . cipherText = generateEncryptedConfig ( {
171
+ encryptedDataReplicant . value = generateEncryptedConfig ( {
157
172
instances : {
158
173
"" : testServiceInstance , // This is invalid because the instance name is empty
159
174
} ,
160
175
bundleDependencies : { } ,
161
176
} ) ;
162
- await persistenceManager . load ( validPassword ) ;
177
+ await persistenceManager . load ( validEncryptionKey ) ;
163
178
expect ( nodecg . log . warn ) . toHaveBeenCalledTimes ( 1 ) ;
164
179
expect ( nodecg . log . warn . mock . calls [ 0 ] [ 0 ] ) . toContain ( "Couldn't load instance" ) ;
165
180
expect ( nodecg . log . warn . mock . calls [ 0 ] [ 0 ] ) . toContain ( "name must not be empty" ) ;
166
181
} ) ;
167
182
168
183
test ( "should not set instance config when no config is required" , async ( ) => {
169
184
testService . requiresNoConfig = true ;
170
- await persistenceManager . load ( validPassword ) ;
185
+ await persistenceManager . load ( validEncryptionKey ) ;
171
186
172
187
const inst = instanceManager . getServiceInstance ( testInstance ) ;
173
188
if ( ! inst ) throw new Error ( "instance was not re-created" ) ;
@@ -182,7 +197,7 @@ describe("PersistenceManager", () => {
182
197
test ( "should log failures when setting service instance configs" , async ( ) => {
183
198
const errorMsg = "client error message" ;
184
199
testService . createClient . mockImplementationOnce ( ( ) => error ( errorMsg ) ) ;
185
- await persistenceManager . load ( validPassword ) ;
200
+ await persistenceManager . load ( validEncryptionKey ) ;
186
201
187
202
// Wait for all previous promises created by loading to settle.
188
203
await new Promise ( ( res ) => setImmediate ( res ) ) ;
@@ -194,7 +209,7 @@ describe("PersistenceManager", () => {
194
209
// Service dependency assignments
195
210
196
211
test ( "should load service dependency assignments" , async ( ) => {
197
- await persistenceManager . load ( validPassword ) ;
212
+ await persistenceManager . load ( validEncryptionKey ) ;
198
213
const deps = bundleManager . getBundleDependencies ( ) [ testBundle ] ;
199
214
expect ( deps ) . toBeDefined ( ) ;
200
215
if ( ! deps ) return ;
@@ -204,7 +219,7 @@ describe("PersistenceManager", () => {
204
219
} ) ;
205
220
206
221
test ( "should unset service dependencies when the underlying instance was deleted" , async ( ) => {
207
- encryptedDataReplicant . value . cipherText = generateEncryptedConfig ( {
222
+ encryptedDataReplicant . value = generateEncryptedConfig ( {
208
223
instances : { } ,
209
224
bundleDependencies : {
210
225
[ testBundle ] : [
@@ -216,15 +231,15 @@ describe("PersistenceManager", () => {
216
231
] ,
217
232
} ,
218
233
} ) ;
219
- await persistenceManager . load ( validPassword ) ;
234
+ await persistenceManager . load ( validEncryptionKey ) ;
220
235
221
236
const deps = bundleManager . getBundleDependencies ( ) [ testBundle ] ;
222
237
expect ( deps ?. [ 0 ] ) . toBeDefined ( ) ;
223
238
expect ( deps ?. [ 0 ] ?. serviceInstance ) . toBeUndefined ( ) ;
224
239
} ) ;
225
240
226
241
test ( "should support unassigned service dependencies" , async ( ) => {
227
- encryptedDataReplicant . value . cipherText = generateEncryptedConfig ( {
242
+ encryptedDataReplicant . value = generateEncryptedConfig ( {
228
243
instances : { } ,
229
244
bundleDependencies : {
230
245
[ testBundle ] : [
@@ -236,7 +251,7 @@ describe("PersistenceManager", () => {
236
251
] ,
237
252
} ,
238
253
} ) ;
239
- await persistenceManager . load ( validPassword ) ;
254
+ await persistenceManager . load ( validEncryptionKey ) ;
240
255
241
256
const deps = bundleManager . getBundleDependencies ( ) [ testBundle ] ;
242
257
expect ( deps ?. [ 0 ] ) . toBeDefined ( ) ;
@@ -251,7 +266,7 @@ describe("PersistenceManager", () => {
251
266
} ) ;
252
267
253
268
test ( "should encrypt and save configuration if framework is loaded" , async ( ) => {
254
- const res = await persistenceManager . load ( validPassword ) ;
269
+ const res = await persistenceManager . load ( validEncryptionKey ) ;
255
270
expect ( res . failed ) . toBe ( false ) ;
256
271
257
272
instanceManager . createServiceInstance ( testService . serviceType , testInstance ) ;
@@ -267,8 +282,12 @@ describe("PersistenceManager", () => {
267
282
if ( ! encryptedDataReplicant . value . cipherText ) return ;
268
283
269
284
// Decrypt and check that the information that was saved is correct
270
- const data = decryptData ( encryptedDataReplicant . value . cipherText , validPassword ) ;
271
- if ( data . failed ) throw new Error ( "could not decrypt newly encrypted data" ) ;
285
+ const data = decryptData (
286
+ encryptedDataReplicant . value . cipherText ,
287
+ crypto . enc . Hex . parse ( validEncryptionKey ) ,
288
+ encryptedDataReplicant . value . iv ,
289
+ ) ;
290
+ if ( data . failed ) throw new Error ( "could not decrypt newly encrypted data: " + data . errorMessage ) ;
272
291
273
292
expect ( data . result . instances [ testInstance ] ?. serviceType ) . toBe ( testService . serviceType ) ;
274
293
expect ( data . result . instances [ testInstance ] ?. config ) . toBe ( testService . defaultConfig ) ;
@@ -286,13 +305,19 @@ describe("PersistenceManager", () => {
286
305
nodecg . log . error . mockReset ( ) ;
287
306
288
307
persistenceManager = new PersistenceManager ( nodecg , serviceManager , instanceManager , bundleManager ) ;
289
- persistenceManager . load = jest . fn ( ) . mockImplementation ( async ( password : string ) => {
290
- if ( password === validPassword ) return emptySuccess ( ) ;
291
- else return error ( "password invalid" ) ;
308
+ persistenceManager . load = jest . fn ( ) . mockImplementation ( async ( encryptionKey : string ) => {
309
+ if ( encryptionKey === validEncryptionKey ) return emptySuccess ( ) ;
310
+ else return error ( "encryption key invalid" ) ;
292
311
} ) ;
293
312
nodecgBundleReplicant . value = bundleRepValue ?? [ nodecg . bundleName ] ;
294
313
}
295
314
315
+ beforeEach ( ( ) => {
316
+ encryptedDataReplicant . value = {
317
+ salt,
318
+ } ;
319
+ } ) ;
320
+
296
321
afterEach ( ( ) => {
297
322
nodecg . bundleConfig = { } ;
298
323
nodecgBundleReplicant . removeAllListeners ( ) ;
@@ -377,7 +402,7 @@ describe("PersistenceManager", () => {
377
402
} ) ;
378
403
379
404
test ( "should automatically save if BundleManager or InstanceManager emit a change event" , async ( ) => {
380
- await persistenceManager . load ( validPassword ) ; // Set password so that we can save stuff
405
+ await persistenceManager . load ( validEncryptionKey ) ; // Set password so that we can save stuff
381
406
382
407
encryptedDataReplicant . value . cipherText = undefined ;
383
408
bundleManager . emit ( "change" ) ;
0 commit comments