@@ -2,6 +2,7 @@ import { NodeCG, ReplicantServer } from "nodecg-types/types/server";
22import { InstanceManager } from "./instanceManager" ;
33import { BundleManager } from "./bundleManager" ;
44import crypto from "crypto-js" ;
5+ import * as argon2 from "argon2-browser" ;
56import { emptySuccess , error , Result , success } from "./utils/result" ;
67import { ObjectMap , ServiceDependency , ServiceInstance } from "./service" ;
78import { ServiceManager } from "./serviceManager" ;
@@ -28,13 +29,13 @@ export interface PersistentData {
2829 * Salt and iv are managed by crypto.js and all AES defaults with a password are used (PBKDF1 using 1 MD5 iteration).
2930 * All this happens in the nodecg-io-core extension and the password is sent using NodeCG Messages.
3031 *
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.
3233 * Only the derived AES encryption key is sent to the extension using NodeCG messages.
3334 * That way analyzed network traffic and malicious bundles that listen for the same NodeCG message only allow getting
3435 * the encryption key and not the plain text password that may be used somewhere else.
3536 *
3637 * 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.
3839 *
3940 */
4041export interface EncryptedData {
@@ -101,36 +102,28 @@ export function encryptData(data: PersistentData, encryptionKey: crypto.lib.Word
101102 * Derives a key suitable for encrypting the config from the given password.
102103 *
103104 * @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.
105106 * @returns a hex encoded string of the derived key.
106107 */
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+ // TODO: maybe find better way to do the hex string -> Uint8Array conversion
110+ const saltBytes = Uint8Array . from ( salt . match ( / .{ 1 , 2 } / g) ?. map ( ( byte ) => parseInt ( byte , 16 ) ) ?? [ ] ) ;
111+
112+ const hash = await argon2 . hash ( {
113+ pass : password ,
114+ salt : saltBytes ,
115+ // OWASP reccomends either t=1,m=37MiB or t=2,m=37MiB for argon2id:
116+ // https://www.owasp.org/index.php/Password_Storage_Cheat_Sheet#Argon2id
117+ // On a Ryzen 5 5500u a single iteration is about 220 ms. Two iterations would make that about 440 ms, which is still fine.
118+ // This is run inside the browser when logging in, therefore 37 MiB is acceptable too.
119+ // To future proof this we use 37 MiB ram and 2 iterations.
120+ time : 2 ,
121+ mem : 37 * 1024 ,
122+ hashLen : 32 , // Output size: 32 bytes = 256 bits as a key for AES-256
123+ type : argon2 . ArgonType . Argon2id ,
124+ } ) ;
125+
126+ return hash . hashHex ;
134127}
135128
136129/**
@@ -166,17 +159,18 @@ export function reEncryptData(
166159 * The salt attribute is not set when either this is the first start of nodecg-io
167160 * or if this is a old config from nodecg-io <= 0.2.
168161 *
169- * If this is a new configuration a new salt will be generated and set inside the EncryptedData object.
162+ * If this is a new configuration a new salt will be generated, set inside the EncryptedData object and returned .
170163 * If this is a old configuration from nodecg-io <= 0.2 it will be migrated to the new format as well.
171164 *
172165 * @param data the encrypted data where the salt should be ensured to be available
173166 * @param password the password of the encrypted data. Used if this config needs to be migrated
167+ * @return returns the either retrieved or generated salt
174168 */
175- export function ensureEncryptionSaltIsSet ( data : EncryptedData , password : string ) : void {
169+ export async function getEncryptionSalt ( data : EncryptedData , password : string ) : Promise < string > {
176170 if ( data . salt !== undefined ) {
177171 // We already have a salt, so we have the new (nodecg-io >=0.3) format too.
178172 // We don't need to do anything then.
179- return ;
173+ return data . salt ;
180174 }
181175
182176 // No salt is present, which is the case for the nodecg-io <=0.2 configs
@@ -191,7 +185,7 @@ export function ensureEncryptionSaltIsSet(data: EncryptedData, password: string)
191185 // This means that this is a old config (nodecg-io <=0.2), that we need to migrate to the new format.
192186
193187 // Re-encrypt the configuration using our own derived key instead of the password.
194- const newEncryptionKey = deriveEncryptionKey ( password , salt ) ;
188+ const newEncryptionKey = await deriveEncryptionKey ( password , salt ) ;
195189 const newEncryptionKeyArr = crypto . enc . Hex . parse ( newEncryptionKey ) ;
196190 const res = reEncryptData ( data , password , newEncryptionKeyArr ) ;
197191 if ( res . failed ) {
@@ -200,6 +194,7 @@ export function ensureEncryptionSaltIsSet(data: EncryptedData, password: string)
200194 }
201195
202196 data . salt = salt ;
197+ return salt ;
203198}
204199
205200/**
@@ -455,8 +450,8 @@ export class PersistenceManager {
455450 try {
456451 this . nodecg . log . info ( "Attempting to automatically login..." ) ;
457452
458- ensureEncryptionSaltIsSet ( this . encryptedData . value , password ) ;
459- const encryptionKey = deriveEncryptionKey ( password , this . encryptedData . value . salt ?? "" ) ;
453+ const salt = await getEncryptionSalt ( this . encryptedData . value , password ) ;
454+ const encryptionKey = await deriveEncryptionKey ( password , salt ) ;
460455 const loadResult = await this . load ( encryptionKey ) ;
461456
462457 if ( ! loadResult . failed ) {
0 commit comments