From 96eb63dc2cb71a1f0864b9e4630b52fc597972cb Mon Sep 17 00:00:00 2001 From: philon- Date: Tue, 13 May 2025 00:10:18 +0200 Subject: [PATCH] Add admin PKI fields --- .../Config/Security/Security.tsx | 152 ++++++++++++------ .../Config/Security/securityReducer.tsx | 13 +- .../PageComponents/Config/Security/types.ts | 14 +- src/validation/config/security.ts | 16 +- 4 files changed, 136 insertions(+), 59 deletions(-) diff --git a/src/components/PageComponents/Config/Security/Security.tsx b/src/components/PageComponents/Config/Security/Security.tsx index 6b06ba2e..f8488525 100644 --- a/src/components/PageComponents/Config/Security/Security.tsx +++ b/src/components/PageComponents/Config/Security/Security.tsx @@ -7,7 +7,7 @@ import { create } from "@bufbuild/protobuf"; import { useDevice } from "@core/stores/deviceStore.ts"; import { Protobuf } from "@meshtastic/core"; import { fromByteArray, toByteArray } from "base64-js"; -import { useReducer } from "react"; +import { useEffect, useReducer, useRef } from "react"; import { securityReducer } from "@components/PageComponents/Config/Security/securityReducer.tsx"; export const Security = () => { @@ -25,19 +25,31 @@ export const Security = () => { privateKey: fromByteArray(config.security?.privateKey ?? new Uint8Array(0)), privateKeyVisible: false, adminKeyVisible: false, + adminKey2Visible: false, + adminKey3Visible: false, privateKeyBitCount: config.security?.privateKey?.length ?? 32, - adminKeyBitCount: config.security?.adminKey?.at(0)?.length ?? 32, publicKey: fromByteArray(config.security?.publicKey ?? new Uint8Array(0)), - adminKey: fromByteArray( + adminKey1: fromByteArray( config.security?.adminKey?.at(0) ?? new Uint8Array(0), ), + adminKey2: fromByteArray( + config.security?.adminKey?.at(1) ?? new Uint8Array(0), + ), + adminKey3: fromByteArray( + config.security?.adminKey?.at(2) ?? new Uint8Array(0), + ), privateKeyDialogOpen: false, }); + const stateRef = useRef(state); + useEffect(() => { + stateRef.current = state; + }, [state]); + const validateKey = ( input: string, count: number, - fieldName: "privateKey" | "adminKey", + fieldName: "privateKey" | "adminKey1" | "adminKey2" | "adminKey3", ) => { try { removeError(fieldName); @@ -47,7 +59,10 @@ export const Security = () => { return; } - if (fieldName === "adminKey" && input === "") { + if ( + (fieldName === "adminKey1" || fieldName === "adminKey2" || + fieldName === "adminKey3") && input === "" + ) { return; } @@ -81,15 +96,22 @@ export const Security = () => { if (hasErrors()) { return; } + + const current = stateRef.current; + setWorkingConfig( create(Protobuf.Config.ConfigSchema, { payloadVariant: { case: "security", value: { ...data, - adminKey: [new Uint8Array(0)], - privateKey: toByteArray(state.privateKey), - publicKey: toByteArray(state.publicKey), + adminKey: [ + toByteArray(current.adminKey1), + toByteArray(current.adminKey2), + toByteArray(current.adminKey3), + ], + privateKey: toByteArray(current.privateKey), + publicKey: toByteArray(current.publicKey), }, }, }), @@ -129,8 +151,12 @@ export const Security = () => { const adminKeyInputChangeEvent = (e: React.ChangeEvent) => { const psk = e.currentTarget?.value; - dispatch({ type: "SET_ADMIN_KEY", payload: psk }); - validateKey(psk, state.privateKeyBitCount, "adminKey"); + const id = e.currentTarget?.id.match(/^adminKey(\d+)(?=Input$)/)?.[1]; + + if (!(id === "1" || id === "2" || id === "3")) return; + + dispatch({ type: `SET_ADMIN${id}_KEY`, payload: psk }); + validateKey(psk, state.privateKeyBitCount, `adminKey${id}`); }; const privateKeySelectChangeEvent = (e: string) => { @@ -147,7 +173,9 @@ export const Security = () => { defaultValues={{ ...config.security, ...{ - adminKey: state.adminKey, + adminKey1: state.adminKey1, + adminKey2: state.adminKey2, + adminKey3: state.adminKey3, privateKey: state.privateKey, publicKey: state.publicKey, adminChannelEnabled: config.security?.adminChannelEnabled ?? false, @@ -216,60 +244,94 @@ export const Security = () => { description: "Settings for Admin", fields: [ { - type: "toggle", - name: "adminChannelEnabled", - label: "Allow Legacy Admin", + type: "passwordGenerator", + name: "adminKey1", + id: "adminKey1Input", + label: "Primary Admin Key", description: - "Allow incoming device control over the insecure legacy admin channel", + "The primary public key authorized to send admin messages to this node", + validationText: hasFieldError("adminKey1") + ? getErrorMessage("adminKey1") + : "", + inputChange: adminKeyInputChangeEvent, + selectChange: () => {}, + bits: [{ text: "256 bit", value: "32", key: "bit256" }], + devicePSKBitCount: state.privateKeyBitCount, + hide: !state.adminKeyVisible, + actionButtons: [], + disabledBy: [ + { fieldName: "adminChannelEnabled", invert: true }, + ], + properties: { + value: state.adminKey1, + showCopyButton: true, + showPasswordToggle: true, + }, }, { - type: "toggle", - name: "isManaged", - label: "Managed", + type: "passwordGenerator", + name: "adminKey2", + id: "adminKey2Input", + label: "Secondary Admin Key", description: - "If true, device configuration options are only able to be changed remotely by a Remote Admin node via admin messages. Do not enable this option unless a suitable Remote Admin node has been setup, and the public key stored in the field below.", + "The secondary public key authorized to send admin messages to this node", + validationText: hasFieldError("adminKey2") + ? getErrorMessage("adminKey2") + : "", + inputChange: adminKeyInputChangeEvent, + selectChange: () => {}, + bits: [{ text: "256 bit", value: "32", key: "bit256" }], + devicePSKBitCount: state.privateKeyBitCount, + hide: !state.adminKey2Visible, + actionButtons: [], + disabledBy: [ + { fieldName: "adminChannelEnabled", invert: true }, + ], + properties: { + value: state.adminKey2, + showCopyButton: true, + showPasswordToggle: true, + }, }, { type: "passwordGenerator", - name: "adminKey", - id: "adminKeyInput", - label: "Admin Key", + name: "adminKey3", + id: "adminKey3Input", + label: "Tertiary Admin Key", description: - "The public key authorized to send admin messages to this node", - validationText: hasFieldError("adminKey") - ? getErrorMessage("adminKey") + "The tertiary public key authorized to send admin messages to this node", + validationText: hasFieldError("adminKey3") + ? getErrorMessage("adminKey3") : "", inputChange: adminKeyInputChangeEvent, selectChange: () => {}, bits: [{ text: "256 bit", value: "32", key: "bit256" }], devicePSKBitCount: state.privateKeyBitCount, - hide: !state.adminKeyVisible, - actionButtons: [ - { - text: "Generate", - variant: "success", - onClick: () => { - const adminKey = getX25519PrivateKey(); - dispatch({ - type: "REGENERATE_ADMIN_KEY", - payload: { adminKey: fromByteArray(adminKey) }, - }); - validateKey( - fromByteArray(adminKey), - state.adminKeyBitCount, - "adminKey", - ); - }, - }, - ], + hide: !state.adminKey3Visible, + actionButtons: [], disabledBy: [ { fieldName: "adminChannelEnabled", invert: true }, ], properties: { - value: state.adminKey, + value: state.adminKey3, showCopyButton: true, + showPasswordToggle: true, }, }, + { + type: "toggle", + name: "isManaged", + label: "Managed", + description: + "If true, device configuration options are only able to be changed remotely by a Remote Admin node via admin messages. Do not enable this option unless a suitable Remote Admin node has been setup, and the public key stored in the field below.", + }, + { + type: "toggle", + name: "adminChannelEnabled", + label: "Allow Legacy Admin", + description: + "Allow incoming device control over the insecure legacy admin channel", + }, ], }, { diff --git a/src/components/PageComponents/Config/Security/securityReducer.tsx b/src/components/PageComponents/Config/Security/securityReducer.tsx index d8e72406..781eefc5 100644 --- a/src/components/PageComponents/Config/Security/securityReducer.tsx +++ b/src/components/PageComponents/Config/Security/securityReducer.tsx @@ -15,8 +15,12 @@ export function securityReducer( return { ...state, privateKeyBitCount: action.payload }; case "SET_PUBLIC_KEY": return { ...state, publicKey: action.payload }; - case "SET_ADMIN_KEY": - return { ...state, adminKey: action.payload }; + case "SET_ADMIN1_KEY": + return { ...state, adminKey1: action.payload }; + case "SET_ADMIN2_KEY": + return { ...state, adminKey2: action.payload }; + case "SET_ADMIN3_KEY": + return { ...state, adminKey3: action.payload }; case "SHOW_PRIVATE_KEY_DIALOG": return { ...state, privateKeyDialogOpen: action.payload }; case "REGENERATE_PRIV_PUB_KEY": @@ -26,11 +30,6 @@ export function securityReducer( publicKey: action.payload.publicKey, privateKeyDialogOpen: false, }; - case "REGENERATE_ADMIN_KEY": - return { - ...state, - adminKey: action.payload.adminKey, - }; default: return state; } diff --git a/src/components/PageComponents/Config/Security/types.ts b/src/components/PageComponents/Config/Security/types.ts index 61a2a07b..fd56d206 100644 --- a/src/components/PageComponents/Config/Security/types.ts +++ b/src/components/PageComponents/Config/Security/types.ts @@ -2,10 +2,13 @@ export interface SecurityState { privateKey: string; privateKeyVisible: boolean; adminKeyVisible: boolean; + adminKey2Visible: boolean; + adminKey3Visible: boolean; privateKeyBitCount: number; - adminKeyBitCount: number; publicKey: string; - adminKey: string; + adminKey1: string; + adminKey2: string; + adminKey3: string; privateKeyDialogOpen: boolean; } @@ -15,10 +18,11 @@ export type SecurityAction = | { type: "TOGGLE_ADMIN_KEY_VISIBILITY" } | { type: "SET_PRIVATE_KEY_BIT_COUNT"; payload: number } | { type: "SET_PUBLIC_KEY"; payload: string } - | { type: "SET_ADMIN_KEY"; payload: string } + | { type: "SET_ADMIN1_KEY"; payload: string } + | { type: "SET_ADMIN2_KEY"; payload: string } + | { type: "SET_ADMIN3_KEY"; payload: string } | { type: "SHOW_PRIVATE_KEY_DIALOG"; payload: boolean } | { type: "REGENERATE_PRIV_PUB_KEY"; payload: { privateKey: string; publicKey: string }; - } - | { type: "REGENERATE_ADMIN_KEY"; payload: { adminKey: string } }; + }; diff --git a/src/validation/config/security.ts b/src/validation/config/security.ts index b856d521..160d7541 100644 --- a/src/validation/config/security.ts +++ b/src/validation/config/security.ts @@ -5,13 +5,25 @@ import { IsBoolean, IsString } from "class-validator"; export class SecurityValidation implements Omit< Protobuf.Config.Config_SecurityConfig, - keyof Message | "adminKey" | "privateKey" | "publicKey" + | keyof Message + | "adminKey" + | "adminKey1" + | "adminKey2" + | "adminKey3" + | "privateKey" + | "publicKey" > { @IsBoolean() adminChannelEnabled: boolean; @IsString() - adminKey: string; + adminKey1: string; + + @IsString() + adminKey2: string; + + @IsString() + adminKey3: string; @IsBoolean() bluetoothLoggingEnabled: boolean;