|
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