Skip to content
This repository was archived by the owner on Apr 13, 2025. It is now read-only.

Commit a2b8cae

Browse files
committed
Merge branch 'main' of github.com:codeoverflow-org/nodecg-io
2 parents 160a258 + 8eea596 commit a2b8cae

File tree

7 files changed

+7002
-1960
lines changed

7 files changed

+7002
-1960
lines changed

nodecg-io-core/dashboard/crypto.ts

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
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";
28
import { EventEmitter } from "events";
39
import { ObjectMap, ServiceInstance, ServiceDependency, Service } from "nodecg-io-core/extension/service";
410
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";
613

714
const encryptedData = nodecg.Replicant<EncryptedData>("encryptedConfig");
815
let services: Service<unknown, never>[] | undefined;
9-
let password: string | undefined;
16+
let encryptionKey: string | undefined;
1017

1118
/**
1219
* Layer between the actual dashboard and `PersistentData`.
@@ -40,71 +47,81 @@ class Config extends EventEmitter {
4047
}
4148
export const config = new Config();
4249

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).
4451
// This ensures that the decrypted data is always up-to-date.
4552
encryptedData.on("change", updateDecryptedData);
4653

4754
/**
4855
* 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.
5159
* Returns whether the password is correct.
5260
* @param pw the password which should be set.
5361
*/
5462
export async function setPassword(pw: string): Promise<boolean> {
5563
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.
5765
// This is especially needed when handling a re-connect as the replicant takes time to declare
5866
// and the password check is usually faster than that.
5967
NodeCG.waitForReplicants(encryptedData),
6068
fetchServices(),
6169
]);
6270

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);
6477

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
6679
if ((await loadFramework()) === false) return false;
6780

6881
if (encryptedData.value) {
6982
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) {
7386
return false;
7487
}
7588
}
7689

7790
return true;
7891
}
7992

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";
8298
const msgWithAuth = Object.assign({}, message);
83-
msgWithAuth.password = password;
99+
msgWithAuth.encryptionKey = encryptionKey;
84100
return await nodecg.sendMessage(messageName, msgWithAuth);
85101
}
86102

87103
/**
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.
89105
*/
90106
export function isPasswordSet(): boolean {
91-
return password !== undefined;
107+
return encryptionKey !== undefined;
92108
}
93109

94110
/**
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.
97113
* @param data the data that should be decrypted.
98114
*/
99115
function updateDecryptedData(data: EncryptedData): void {
100116
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);
103120
if (!res.failed) {
104121
result = res.result;
105122
} else {
106-
// Password is wrong
107-
password = undefined;
123+
// Secret is wrong
124+
encryptionKey = undefined;
108125
}
109126
}
110127

@@ -135,7 +152,7 @@ async function loadFramework(): Promise<boolean> {
135152
if (await isLoaded()) return true;
136153

137154
try {
138-
await nodecg.sendMessage("load", { password });
155+
await nodecg.sendMessage("load", { encryptionKey });
139156
return true;
140157
} catch {
141158
return false;

nodecg-io-core/extension/__tests__/mocks.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ObjectMap, ServiceInstance } from "../service";
1+
import { ObjectMap, ServiceInstance, Service } from "../service";
22
import NodeCG from "@nodecg/types";
33
import { EventEmitter } from "events";
44

@@ -162,6 +162,24 @@ export const testService = {
162162
},
163163
};
164164

165+
export const websocketServerService: Service<{ port: number }, void> = {
166+
serviceType: "websocket-server",
167+
validateConfig: jest.fn(),
168+
createClient: jest.fn(),
169+
stopClient: jest.fn(),
170+
reCreateClientToRemoveHandlers: false,
171+
requiresNoConfig: false,
172+
schema: {
173+
$schema: "http://json-schema.org/draft-07/schema#",
174+
type: "object",
175+
properties: {
176+
port: {
177+
type: "integer",
178+
},
179+
},
180+
},
181+
};
182+
165183
export const testServiceInstance: ServiceInstance<string, () => string> = {
166184
serviceType: testService.serviceType,
167185
config: "hello world",

0 commit comments

Comments
 (0)