import { PkiRegenerateDialog } from "@app/components/Dialog/PkiRegenerateDialog"; import { DynamicForm } from "@app/components/Form/DynamicForm.tsx"; import { getX25519PrivateKey, getX25519PublicKey, } from "@app/core/utils/x25519"; import type { SecurityValidation } from "@app/validation/config/security.tsx"; import { useDevice } from "@core/stores/deviceStore.ts"; import { Protobuf } from "@meshtastic/js"; import { fromByteArray, toByteArray } from "base64-js"; import { Eye, EyeOff } from "lucide-react"; import { useState } from "react"; export const Security = (): JSX.Element => { const { config, nodes, hardware, setWorkingConfig, setDialogOpen } = useDevice(); const [privateKey, setPrivateKey] = useState( fromByteArray(config.security?.privateKey ?? new Uint8Array(0)), ); const [privateKeyVisible, setPrivateKeyVisible] = useState(false); const [privateKeyBitCount, setPrivateKeyBitCount] = useState( config.security?.privateKey.length ?? 32, ); const [privateKeyValidationText, setPrivateKeyValidationText] = useState(); const [publicKey, setPublicKey] = useState( fromByteArray(config.security?.publicKey ?? new Uint8Array(0)), ); const [adminKey, setAdminKey] = useState( fromByteArray(config.security?.adminKey[0] ?? new Uint8Array(0)), ); const [adminKeyValidationText, setAdminKeyValidationText] = useState(); const [privateKeyDialogOpen, setPrivateKeyDialogOpen] = useState(false); const onSubmit = (data: SecurityValidation) => { if (privateKeyValidationText || adminKeyValidationText) return; setWorkingConfig( new Protobuf.Config.Config({ payloadVariant: { case: "security", value: { ...data, adminKey: [toByteArray(adminKey)], privateKey: toByteArray(privateKey), publicKey: toByteArray(publicKey), }, }, }), ); }; const validateKey = ( input: string, count: number, setValidationText: ( value: React.SetStateAction, ) => void, ) => { try { if (input.length % 4 !== 0 || toByteArray(input).length !== count) { setValidationText(`Please enter a valid ${count * 8} bit PSK.`); } else { setValidationText(undefined); } } catch (e) { console.error(e); setValidationText(`Please enter a valid ${count * 8} bit PSK.`); } }; const privateKeyClickEvent = () => { setPrivateKeyDialogOpen(true); }; const pkiBackupClickEvent = () => { setDialogOpen("pkiBackup", true); } const pkiRegenerate = () => { const privateKey = getX25519PrivateKey(); const publicKey = getX25519PublicKey(privateKey); setPrivateKey(fromByteArray(privateKey)); setPublicKey(fromByteArray(publicKey)); validateKey( fromByteArray(privateKey), privateKeyBitCount, setPrivateKeyValidationText, ); setPrivateKeyDialogOpen(false); }; const privateKeyInputChangeEvent = ( e: React.ChangeEvent, ) => { const privateKeyB64String = e.target.value; setPrivateKey(privateKeyB64String); validateKey( privateKeyB64String, privateKeyBitCount, setPrivateKeyValidationText, ); const publicKey = getX25519PublicKey(toByteArray(privateKeyB64String)); setPublicKey(fromByteArray(publicKey)); }; const adminKeyInputChangeEvent = (e: React.ChangeEvent) => { const psk = e.currentTarget?.value; setAdminKey(psk); validateKey(psk, privateKeyBitCount, setAdminKeyValidationText); }; const privateKeySelectChangeEvent = (e: string) => { const count = Number.parseInt(e); setPrivateKeyBitCount(count); validateKey(privateKey, count, setPrivateKeyValidationText); }; return ( <> onSubmit={onSubmit} submitType="onChange" defaultValues={{ ...config.security, ...{ adminKey: adminKey, privateKey: privateKey, publicKey: publicKey, adminChannelEnabled: config.security?.adminChannelEnabled ?? false, isManaged: config.security?.isManaged ?? false, debugLogApiEnabled: config.security?.debugLogApiEnabled ?? false, serialEnabled: config.security?.serialEnabled ?? false, }, }} fieldGroups={[ { label: "Security Settings", description: "Settings for the Security configuration", fields: [ { type: "passwordGenerator", name: "privateKey", label: "Private Key", description: "Used to create a shared key with a remote device", bits: [{ text: "256 bit", value: "32", key: "bit256" }], validationText: privateKeyValidationText, devicePSKBitCount: privateKeyBitCount, inputChange: privateKeyInputChangeEvent, selectChange: privateKeySelectChangeEvent, hide: !privateKeyVisible, actionButtons: [ { text: "Generate", onClick: privateKeyClickEvent, variant: "success", }, { text: "Backup Key", onClick: pkiBackupClickEvent, variant: "subtle", }, ], properties: { value: privateKey, action: { icon: privateKeyVisible ? EyeOff : Eye, onClick: () => setPrivateKeyVisible(!privateKeyVisible), }, }, }, { type: "text", name: "publicKey", label: "Public Key", disabled: true, description: "Sent out to other nodes on the mesh to allow them to compute a shared secret key", properties: { value: publicKey, }, }, ], }, { label: "Admin Settings", description: "Settings for Admin", fields: [ { type: "toggle", name: "adminChannelEnabled", label: "Allow Legacy Admin", description: "Allow incoming device control over the insecure legacy admin channel", }, { 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: "text", name: "adminKey", label: "Admin Key", description: "The public key authorized to send admin messages to this node", validationText: adminKeyValidationText, inputChange: adminKeyInputChangeEvent, disabledBy: [ { fieldName: "adminChannelEnabled", invert: true }, ], properties: { value: adminKey, }, }, ], }, { label: "Logging Settings", description: "Settings for Logging", fields: [ { type: "toggle", name: "debugLogApiEnabled", label: "Enable Debug Log API", description: "Output live debug logging over serial, view and export position-redacted device logs over Bluetooth", }, { type: "toggle", name: "serialEnabled", label: "Serial Output Enabled", description: "Serial Console over the Stream API", }, ], }, ]} /> setPrivateKeyDialogOpen(false)} onSubmit={() => pkiRegenerate()} /> ); };