|
1 | | -import { PersistentData, EncryptedData, decryptData } from "nodecg-io-core/extension/persistenceManager"; |
| 1 | +import { |
| 2 | + PersistentData, |
| 3 | + EncryptedData, |
| 4 | + decryptData, |
| 5 | + deriveEncryptionKey, |
| 6 | + getEncryptionSalt, |
| 7 | +} from "nodecg-io-core/extension/persistenceManager"; |
2 | 8 | import { EventEmitter } from "events"; |
3 | 9 | import { ObjectMap, ServiceInstance, ServiceDependency, Service } from "nodecg-io-core/extension/service"; |
4 | 10 | import { isLoaded } from "./authentication"; |
5 | | -import { PasswordMessage } from "nodecg-io-core/extension/messageManager"; |
| 11 | +import { AuthenticationMessage } from "nodecg-io-core/extension/messageManager"; |
| 12 | +import cryptoJS from "crypto-js"; |
6 | 13 |
|
7 | 14 | const encryptedData = nodecg.Replicant<EncryptedData>("encryptedConfig"); |
8 | 15 | let services: Service<unknown, never>[] | undefined; |
9 | | -let password: string | undefined; |
| 16 | +let encryptionKey: string | undefined; |
10 | 17 |
|
11 | 18 | /** |
12 | 19 | * Layer between the actual dashboard and `PersistentData`. |
@@ -40,71 +47,81 @@ class Config extends EventEmitter { |
40 | 47 | } |
41 | 48 | export const config = new Config(); |
42 | 49 |
|
43 | | -// Update the decrypted copy of the data once the encrypted version changes (if a password is available). |
| 50 | +// Update the decrypted copy of the data once the encrypted version changes (if a encryption key is available). |
44 | 51 | // This ensures that the decrypted data is always up-to-date. |
45 | 52 | encryptedData.on("change", updateDecryptedData); |
46 | 53 |
|
47 | 54 | /** |
48 | 55 | * Sets the passed password to be used by the crypto module. |
49 | | - * Will try to decrypt encrypted data to tell whether the password is correct, |
50 | | - * if it is wrong the internal password will be set to undefined. |
| 56 | + * Uses the password to derive a decryption secret and then tries to decrypt |
| 57 | + * the encrypted data to tell whether the password is correct. |
| 58 | + * If it is wrong the internal encryption key will be set to undefined. |
51 | 59 | * Returns whether the password is correct. |
52 | 60 | * @param pw the password which should be set. |
53 | 61 | */ |
54 | 62 | export async function setPassword(pw: string): Promise<boolean> { |
55 | 63 | await Promise.all([ |
56 | | - // Ensures that the `encryptedData` has been declared because it is needed by `setPassword()` |
| 64 | + // Ensures that the `encryptedData` has been declared because it is needed to get the encrypted config. |
57 | 65 | // This is especially needed when handling a re-connect as the replicant takes time to declare |
58 | 66 | // and the password check is usually faster than that. |
59 | 67 | NodeCG.waitForReplicants(encryptedData), |
60 | 68 | fetchServices(), |
61 | 69 | ]); |
62 | 70 |
|
63 | | - password = pw; |
| 71 | + if (encryptedData.value === undefined) { |
| 72 | + encryptedData.value = {}; |
| 73 | + } |
| 74 | + |
| 75 | + const salt = await getEncryptionSalt(encryptedData.value, pw); |
| 76 | + encryptionKey = await deriveEncryptionKey(pw, salt); |
64 | 77 |
|
65 | | - // Load framework, returns false if not already loaded and password is wrong |
| 78 | + // Load framework, returns false if not already loaded and password/encryption key is wrong |
66 | 79 | if ((await loadFramework()) === false) return false; |
67 | 80 |
|
68 | 81 | if (encryptedData.value) { |
69 | 82 | updateDecryptedData(encryptedData.value); |
70 | | - // Password is unset by `updateDecryptedData` if it is wrong. |
71 | | - // This may happen if the framework was already loaded and `loadFramework` didn't check the password. |
72 | | - if (password === undefined) { |
| 83 | + // encryption key is unset by `updateDecryptedData` if it is wrong. |
| 84 | + // This may happen if the framework was already loaded and `loadFramework` didn't check the password/encryption key. |
| 85 | + if (encryptionKey === undefined) { |
73 | 86 | return false; |
74 | 87 | } |
75 | 88 | } |
76 | 89 |
|
77 | 90 | return true; |
78 | 91 | } |
79 | 92 |
|
80 | | -export async function sendAuthenticatedMessage<V>(messageName: string, message: Partial<PasswordMessage>): Promise<V> { |
81 | | - if (password === undefined) throw "No password available"; |
| 93 | +export async function sendAuthenticatedMessage<V>( |
| 94 | + messageName: string, |
| 95 | + message: Partial<AuthenticationMessage>, |
| 96 | +): Promise<V> { |
| 97 | + if (encryptionKey === undefined) throw "Can't send authenticated message: crypto module not authenticated"; |
82 | 98 | const msgWithAuth = Object.assign({}, message); |
83 | | - msgWithAuth.password = password; |
| 99 | + msgWithAuth.encryptionKey = encryptionKey; |
84 | 100 | return await nodecg.sendMessage(messageName, msgWithAuth); |
85 | 101 | } |
86 | 102 |
|
87 | 103 | /** |
88 | | - * Returns whether a password has been set in the crypto module aka. whether it is authenticated. |
| 104 | + * Returns whether a password derived encryption key has been set in the crypto module aka. whether it is authenticated. |
89 | 105 | */ |
90 | 106 | export function isPasswordSet(): boolean { |
91 | | - return password !== undefined; |
| 107 | + return encryptionKey !== undefined; |
92 | 108 | } |
93 | 109 |
|
94 | 110 | /** |
95 | | - * Decrypts the passed data using the global password variable and saves it into `ConfigData`. |
96 | | - * Unsets the password if its wrong and also forwards `undefined` to `ConfigData` if the password is unset. |
| 111 | + * Decrypts the passed data using the global encryptionKey variable and saves it into `ConfigData`. |
| 112 | + * Clears the encryption key if its wrong and also forwards `undefined` to `ConfigData` if the encryption key is unset. |
97 | 113 | * @param data the data that should be decrypted. |
98 | 114 | */ |
99 | 115 | function updateDecryptedData(data: EncryptedData): void { |
100 | 116 | let result: PersistentData | undefined = undefined; |
101 | | - if (password !== undefined && data.cipherText) { |
102 | | - const res = decryptData(data.cipherText, password); |
| 117 | + if (encryptionKey !== undefined && data.cipherText) { |
| 118 | + const passwordWordArray = cryptoJS.enc.Hex.parse(encryptionKey); |
| 119 | + const res = decryptData(data.cipherText, passwordWordArray, data.iv); |
103 | 120 | if (!res.failed) { |
104 | 121 | result = res.result; |
105 | 122 | } else { |
106 | | - // Password is wrong |
107 | | - password = undefined; |
| 123 | + // Secret is wrong |
| 124 | + encryptionKey = undefined; |
108 | 125 | } |
109 | 126 | } |
110 | 127 |
|
@@ -135,7 +152,7 @@ async function loadFramework(): Promise<boolean> { |
135 | 152 | if (await isLoaded()) return true; |
136 | 153 |
|
137 | 154 | try { |
138 | | - await nodecg.sendMessage("load", { password }); |
| 155 | + await nodecg.sendMessage("load", { encryptionKey }); |
139 | 156 | return true; |
140 | 157 | } catch { |
141 | 158 | return false; |
|
0 commit comments