@@ -2,6 +2,7 @@ import { NodeCG, ReplicantServer } from "nodecg-types/types/server";
2
2
import { InstanceManager } from "./instanceManager" ;
3
3
import { BundleManager } from "./bundleManager" ;
4
4
import crypto from "crypto-js" ;
5
+ import * as argon2 from "argon2-browser" ;
5
6
import { emptySuccess , error , Result , success } from "./utils/result" ;
6
7
import { ObjectMap , ServiceDependency , ServiceInstance } from "./service" ;
7
8
import { ServiceManager } from "./serviceManager" ;
@@ -28,13 +29,13 @@ export interface PersistentData {
28
29
* Salt and iv are managed by crypto.js and all AES defaults with a password are used (PBKDF1 using 1 MD5 iteration).
29
30
* All this happens in the nodecg-io-core extension and the password is sent using NodeCG Messages.
30
31
*
31
- * For nodecg-io >= 0.3 this was changed. PBKDF2 using SHA256 is directly run inside the browser when logging in.
32
+ * For nodecg-io >= 0.3 this was changed. A encryption key is derived using argon2id directly inside the browser when logging in.
32
33
* Only the derived AES encryption key is sent to the extension using NodeCG messages.
33
34
* That way analyzed network traffic and malicious bundles that listen for the same NodeCG message only allow getting
34
35
* the encryption key and not the plain text password that may be used somewhere else.
35
36
*
36
37
* Still with this security upgrade you should only use trusted bundles with your NodeCG installation
37
- * and use https if your using the dashboard over a untrusted network.
38
+ * and use https if you're using the dashboard over a untrusted network.
38
39
*
39
40
*/
40
41
export interface EncryptedData {
@@ -101,36 +102,27 @@ export function encryptData(data: PersistentData, encryptionKey: crypto.lib.Word
101
102
* Derives a key suitable for encrypting the config from the given password.
102
103
*
103
104
* @param password the password from which the encryption key will be derived.
104
- * @param salt the salt that is used for key derivation.
105
+ * @param salt the hex encoded salt that is used for key derivation.
105
106
* @returns a hex encoded string of the derived key.
106
107
*/
107
- export function deriveEncryptionKey ( password : string , salt : string ) : string {
108
- const saltWordArray = crypto . enc . Hex . parse ( salt ) ;
109
-
110
- return crypto
111
- . PBKDF2 ( password , saltWordArray , {
112
- // Generate a 256 bit long key for AES-256.
113
- keySize : 256 / 32 ,
114
- // Iterations should ideally be as high as possible.
115
- // OWASP recommends 310.000 iterations for PBKDF2 with SHA-256 [https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2].
116
- // The problem that we have here is that this is run inside the browser
117
- // and we must use the JavaScript implementation which is slow.
118
- // There is the SubtleCrypto API in browsers that is implemented in native code inside the browser and can use cryptographic CPU extensions.
119
- // However SubtleCrypto is only available in secure contexts (https) so we cannot use it
120
- // because nodecg-io should be usable on e.g. raspberry pi on a local trusted network.
121
- // So were left with only 5000 iterations which were determined
122
- // by checking how many iterations are possible on a AMD Ryzen 5 1600 in a single second
123
- // which should be acceptable time for logging in. Slower CPUs will take longer,
124
- // so I didn't want to increase this any further.
125
-
126
- // For comparison: the crypto.js internal key generation function that was used in nodecg.io <0.3 configs
127
- // used PBKDF1 based on a single MD5 iteration (yes, that is really the default in crypto.js...).
128
- // So this is still a big improvement in comparison to the old config format.
129
- iterations : 5000 ,
130
- // Use SHA-256 as the hashing algorithm. crypto.js defaults to SHA-1 which is less secure.
131
- hasher : crypto . algo . SHA256 ,
132
- } )
133
- . toString ( crypto . enc . Hex ) ;
108
+ export async function deriveEncryptionKey ( password : string , salt : string ) : Promise < string > {
109
+ const saltBytes = Uint8Array . from ( salt . match ( / .{ 1 , 2 } / g) ?. map ( ( byte ) => parseInt ( byte , 16 ) ) ?? [ ] ) ;
110
+
111
+ const hash = await argon2 . hash ( {
112
+ pass : password ,
113
+ salt : saltBytes ,
114
+ // OWASP reccomends either t=1,m=37MiB or t=2,m=37MiB for argon2id:
115
+ // https://www.owasp.org/index.php/Password_Storage_Cheat_Sheet#Argon2id
116
+ // On a Ryzen 5 5500u a single iteration is about 220 ms. Two iterations would make that about 440 ms, which is still fine.
117
+ // This is run inside the browser when logging in, therefore 37 MiB is acceptable too.
118
+ // To future proof this we use 37 MiB ram and 2 iterations.
119
+ time : 2 ,
120
+ mem : 37 * 1024 ,
121
+ hashLen : 32 , // Output size: 32 bytes = 256 bits as a key for AES-256
122
+ type : argon2 . ArgonType . Argon2id ,
123
+ } ) ;
124
+
125
+ return hash . hashHex ;
134
126
}
135
127
136
128
/**
@@ -166,17 +158,18 @@ export function reEncryptData(
166
158
* The salt attribute is not set when either this is the first start of nodecg-io
167
159
* or if this is a old config from nodecg-io <= 0.2.
168
160
*
169
- * If this is a new configuration a new salt will be generated and set inside the EncryptedData object.
161
+ * If this is a new configuration a new salt will be generated, set inside the EncryptedData object and returned .
170
162
* If this is a old configuration from nodecg-io <= 0.2 it will be migrated to the new format as well.
171
163
*
172
164
* @param data the encrypted data where the salt should be ensured to be available
173
165
* @param password the password of the encrypted data. Used if this config needs to be migrated
166
+ * @return returns the either retrieved or generated salt
174
167
*/
175
- export function ensureEncryptionSaltIsSet ( data : EncryptedData , password : string ) : void {
168
+ export async function getEncryptionSalt ( data : EncryptedData , password : string ) : Promise < string > {
176
169
if ( data . salt !== undefined ) {
177
170
// We already have a salt, so we have the new (nodecg-io >=0.3) format too.
178
171
// We don't need to do anything then.
179
- return ;
172
+ return data . salt ;
180
173
}
181
174
182
175
// No salt is present, which is the case for the nodecg-io <=0.2 configs
@@ -191,7 +184,7 @@ export function ensureEncryptionSaltIsSet(data: EncryptedData, password: string)
191
184
// This means that this is a old config (nodecg-io <=0.2), that we need to migrate to the new format.
192
185
193
186
// Re-encrypt the configuration using our own derived key instead of the password.
194
- const newEncryptionKey = deriveEncryptionKey ( password , salt ) ;
187
+ const newEncryptionKey = await deriveEncryptionKey ( password , salt ) ;
195
188
const newEncryptionKeyArr = crypto . enc . Hex . parse ( newEncryptionKey ) ;
196
189
const res = reEncryptData ( data , password , newEncryptionKeyArr ) ;
197
190
if ( res . failed ) {
@@ -200,6 +193,7 @@ export function ensureEncryptionSaltIsSet(data: EncryptedData, password: string)
200
193
}
201
194
202
195
data . salt = salt ;
196
+ return salt ;
203
197
}
204
198
205
199
/**
@@ -455,8 +449,8 @@ export class PersistenceManager {
455
449
try {
456
450
this . nodecg . log . info ( "Attempting to automatically login..." ) ;
457
451
458
- ensureEncryptionSaltIsSet ( this . encryptedData . value , password ) ;
459
- const encryptionKey = deriveEncryptionKey ( password , this . encryptedData . value . salt ?? "" ) ;
452
+ const salt = await getEncryptionSalt ( this . encryptedData . value , password ) ;
453
+ const encryptionKey = await deriveEncryptionKey ( password , salt ) ;
460
454
const loadResult = await this . load ( encryptionKey ) ;
461
455
462
456
if ( ! loadResult . failed ) {
0 commit comments