1
1
import { NodeCG , ReplicantServer } from "nodecg-types/types/server" ;
2
2
import { InstanceManager } from "./instanceManager" ;
3
3
import { BundleManager } from "./bundleManager" ;
4
- import * as crypto from "crypto-js" ;
4
+ import crypto from "crypto-js" ;
5
5
import { emptySuccess , error , Result , success } from "./utils/result" ;
6
6
import { ObjectMap , ServiceDependency , ServiceInstance } from "./service" ;
7
7
import { ServiceManager } from "./serviceManager" ;
@@ -28,6 +28,8 @@ export interface EncryptedData {
28
28
* The encrypted format of the data that needs to be stored.
29
29
*/
30
30
cipherText ?: string ;
31
+ salt ?: string ;
32
+ iv ?: string ;
31
33
}
32
34
33
35
/**
@@ -37,9 +39,14 @@ export interface EncryptedData {
37
39
* @param cipherText the ciphertext that needs to be decrypted.
38
40
* @param password the password for the encrypted data.
39
41
*/
40
- export function decryptData ( cipherText : string , password : string ) : Result < PersistentData > {
42
+ export function decryptData (
43
+ cipherText : string ,
44
+ password : string | crypto . lib . WordArray ,
45
+ iv : string | undefined ,
46
+ ) : Result < PersistentData > {
41
47
try {
42
- const decryptedBytes = crypto . AES . decrypt ( cipherText , password ) ;
48
+ const ivWordArray = iv ? crypto . enc . Hex . parse ( iv ) : undefined ;
49
+ const decryptedBytes = crypto . AES . decrypt ( cipherText , password , { iv : ivWordArray } ) ;
43
50
const decryptedText = decryptedBytes . toString ( crypto . enc . Utf8 ) ;
44
51
const data : PersistentData = JSON . parse ( decryptedText ) ;
45
52
return success ( data ) ;
@@ -48,6 +55,48 @@ export function decryptData(cipherText: string, password: string): Result<Persis
48
55
}
49
56
}
50
57
58
+ export function encryptData ( data : PersistentData , password : string | crypto . lib . WordArray ) : [ string , string ] {
59
+ const iv = crypto . lib . WordArray . random ( 16 ) ;
60
+ const ivText = iv . toString ( ) ;
61
+ const encrypted = crypto . AES . encrypt ( JSON . stringify ( data ) , password , { iv } ) ;
62
+ return [ encrypted . toString ( ) , ivText ] ;
63
+ }
64
+
65
+ export function deriveEncryptionSecret ( password : string , salt : string | undefined ) : string {
66
+ if ( salt === undefined ) {
67
+ return password ;
68
+ }
69
+
70
+ const saltWordArray = crypto . enc . Hex . parse ( salt ) ;
71
+
72
+ return crypto
73
+ . PBKDF2 ( password , saltWordArray , {
74
+ keySize : 256 / 32 ,
75
+ iterations : 5000 ,
76
+ } )
77
+ . toString ( crypto . enc . Hex ) ;
78
+ }
79
+
80
+ export function reEncryptData (
81
+ data : EncryptedData ,
82
+ oldSecret : string | crypto . lib . WordArray ,
83
+ newSecret : string | crypto . lib . WordArray ,
84
+ ) : Result < void > {
85
+ if ( data . cipherText === undefined ) {
86
+ return error ( "Cannot re-encrypt empty cipher text." ) ;
87
+ }
88
+
89
+ const decryptedData = decryptData ( data . cipherText , oldSecret , data . iv ) ;
90
+ if ( decryptedData . failed ) {
91
+ return error ( decryptedData . errorMessage ) ;
92
+ }
93
+
94
+ const [ newCipherText , iv ] = encryptData ( decryptedData . result , newSecret ) ;
95
+ data . cipherText = newCipherText ;
96
+ data . iv = iv ;
97
+ return emptySuccess ( ) ;
98
+ }
99
+
51
100
/**
52
101
* Manages encrypted persistence of data that is held by the instance and bundle managers.
53
102
*/
@@ -116,8 +165,14 @@ export class PersistenceManager {
116
165
} else {
117
166
// Decrypt config
118
167
this . nodecg . log . info ( "Decrypting and loading saved configuration." ) ;
119
- const data = decryptData ( this . encryptedData . value . cipherText , password ) ;
168
+ const passwordWordArray = crypto . enc . Hex . parse ( password ) ;
169
+ const data = decryptData (
170
+ this . encryptedData . value . cipherText ,
171
+ passwordWordArray ,
172
+ this . encryptedData . value . iv ,
173
+ ) ;
120
174
if ( data . failed ) {
175
+ this . nodecg . log . error ( "Could not decrypt configuration: password is invalid." ) ;
121
176
return data ;
122
177
}
123
178
@@ -215,8 +270,10 @@ export class PersistenceManager {
215
270
} ;
216
271
217
272
// Encrypt and save data to persistent replicant.
218
- const cipherText = crypto . AES . encrypt ( JSON . stringify ( data ) , this . password ) ;
219
- this . encryptedData . value . cipherText = cipherText . toString ( ) ;
273
+ const passwordWordArray = crypto . enc . Hex . parse ( this . password ) ;
274
+ const [ cipherText , iv ] = encryptData ( data , passwordWordArray ) ;
275
+ this . encryptedData . value . cipherText = cipherText ;
276
+ this . encryptedData . value . iv = iv ;
220
277
}
221
278
222
279
/**
@@ -292,7 +349,22 @@ export class PersistenceManager {
292
349
if ( bundles . length > 0 ) {
293
350
try {
294
351
this . nodecg . log . info ( "Attempting to automatically login..." ) ;
295
- const loadResult = await this . load ( password ) ;
352
+
353
+ const salt =
354
+ this . encryptedData . value . salt ?? crypto . lib . WordArray . random ( 128 / 8 ) . toString ( crypto . enc . Hex ) ;
355
+ if ( this . encryptedData . value . salt === undefined ) {
356
+ const newSecret = deriveEncryptionSecret ( password , salt ) ;
357
+
358
+ if ( this . encryptedData . value . cipherText !== undefined ) {
359
+ const newSecretWordArray = crypto . enc . Hex . parse ( newSecret ) ;
360
+ reEncryptData ( this . encryptedData . value , password , newSecretWordArray ) ;
361
+ }
362
+
363
+ this . encryptedData . value . salt = salt ;
364
+ }
365
+
366
+ const encryptionSecret = deriveEncryptionSecret ( password , salt ) ;
367
+ const loadResult = await this . load ( encryptionSecret ) ;
296
368
297
369
if ( ! loadResult . failed ) {
298
370
this . nodecg . log . info ( "Automatic login successful." ) ;
0 commit comments