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

Derive encryption key in dashboard using Argon2 #424

Merged
merged 17 commits into from
Jul 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 41 additions & 24 deletions nodecg-io-core/dashboard/crypto.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { PersistentData, EncryptedData, decryptData } from "nodecg-io-core/extension/persistenceManager";
import {
PersistentData,
EncryptedData,
decryptData,
deriveEncryptionKey,
getEncryptionSalt,
} from "nodecg-io-core/extension/persistenceManager";
import { EventEmitter } from "events";
import { ObjectMap, ServiceInstance, ServiceDependency, Service } from "nodecg-io-core/extension/service";
import { isLoaded } from "./authentication";
import { PasswordMessage } from "nodecg-io-core/extension/messageManager";
import { AuthenticationMessage } from "nodecg-io-core/extension/messageManager";
import cryptoJS from "crypto-js";

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

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

// Update the decrypted copy of the data once the encrypted version changes (if a password is available).
// Update the decrypted copy of the data once the encrypted version changes (if a encryption key is available).
// This ensures that the decrypted data is always up-to-date.
encryptedData.on("change", updateDecryptedData);

/**
* Sets the passed password to be used by the crypto module.
* Will try to decrypt encrypted data to tell whether the password is correct,
* if it is wrong the internal password will be set to undefined.
* Uses the password to derive a decryption secret and then tries to decrypt
* the encrypted data to tell whether the password is correct.
* If it is wrong the internal encryption key will be set to undefined.
* Returns whether the password is correct.
* @param pw the password which should be set.
*/
export async function setPassword(pw: string): Promise<boolean> {
await Promise.all([
// Ensures that the `encryptedData` has been declared because it is needed by `setPassword()`
// Ensures that the `encryptedData` has been declared because it is needed to get the encrypted config.
// This is especially needed when handling a re-connect as the replicant takes time to declare
// and the password check is usually faster than that.
NodeCG.waitForReplicants(encryptedData),
fetchServices(),
]);

password = pw;
if (encryptedData.value === undefined) {
encryptedData.value = {};
}

const salt = await getEncryptionSalt(encryptedData.value, pw);
encryptionKey = await deriveEncryptionKey(pw, salt);

// Load framework, returns false if not already loaded and password is wrong
// Load framework, returns false if not already loaded and password/encryption key is wrong
if ((await loadFramework()) === false) return false;

if (encryptedData.value) {
updateDecryptedData(encryptedData.value);
// Password is unset by `updateDecryptedData` if it is wrong.
// This may happen if the framework was already loaded and `loadFramework` didn't check the password.
if (password === undefined) {
// encryption key is unset by `updateDecryptedData` if it is wrong.
// This may happen if the framework was already loaded and `loadFramework` didn't check the password/encryption key.
if (encryptionKey === undefined) {
return false;
}
}

return true;
}

export async function sendAuthenticatedMessage<V>(messageName: string, message: Partial<PasswordMessage>): Promise<V> {
if (password === undefined) throw "No password available";
export async function sendAuthenticatedMessage<V>(
messageName: string,
message: Partial<AuthenticationMessage>,
): Promise<V> {
if (encryptionKey === undefined) throw "Can't send authenticated message: crypto module not authenticated";
const msgWithAuth = Object.assign({}, message);
msgWithAuth.password = password;
msgWithAuth.encryptionKey = encryptionKey;
return await nodecg.sendMessage(messageName, msgWithAuth);
}

/**
* Returns whether a password has been set in the crypto module aka. whether it is authenticated.
* Returns whether a password derived encryption key has been set in the crypto module aka. whether it is authenticated.
*/
export function isPasswordSet(): boolean {
return password !== undefined;
return encryptionKey !== undefined;
}

/**
* Decrypts the passed data using the global password variable and saves it into `ConfigData`.
* Unsets the password if its wrong and also forwards `undefined` to `ConfigData` if the password is unset.
* Decrypts the passed data using the global encryptionKey variable and saves it into `ConfigData`.
* Clears the encryption key if its wrong and also forwards `undefined` to `ConfigData` if the encryption key is unset.
* @param data the data that should be decrypted.
*/
function updateDecryptedData(data: EncryptedData): void {
let result: PersistentData | undefined = undefined;
if (password !== undefined && data.cipherText) {
const res = decryptData(data.cipherText, password);
if (encryptionKey !== undefined && data.cipherText) {
const passwordWordArray = cryptoJS.enc.Hex.parse(encryptionKey);
const res = decryptData(data.cipherText, passwordWordArray, data.iv);
if (!res.failed) {
result = res.result;
} else {
// Password is wrong
password = undefined;
// Secret is wrong
encryptionKey = undefined;
}
}

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

try {
await nodecg.sendMessage("load", { password });
await nodecg.sendMessage("load", { encryptionKey });
return true;
} catch {
return false;
Expand Down
20 changes: 19 additions & 1 deletion nodecg-io-core/extension/__tests__/mocks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ObjectMap, ServiceInstance } from "../service";
import { ObjectMap, ServiceInstance, Service } from "../service";
import NodeCG from "@nodecg/types";
import { EventEmitter } from "events";

Expand Down Expand Up @@ -162,6 +162,24 @@ export const testService = {
},
};

export const websocketServerService: Service<{ port: number }, void> = {
serviceType: "websocket-server",
validateConfig: jest.fn(),
createClient: jest.fn(),
stopClient: jest.fn(),
reCreateClientToRemoveHandlers: false,
requiresNoConfig: false,
schema: {
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
port: {
type: "integer",
},
},
},
};

export const testServiceInstance: ServiceInstance<string, () => string> = {
serviceType: testService.serviceType,
config: "hello world",
Expand Down
Loading