diff --git a/src/components/PageComponents/Config/Security.tsx b/src/components/PageComponents/Config/Security.tsx index c9e1175c..c1739aba 100644 --- a/src/components/PageComponents/Config/Security.tsx +++ b/src/components/PageComponents/Config/Security.tsx @@ -3,6 +3,7 @@ import type { SecurityValidation } from "@app/validation/config/security.js"; import { useDevice } from "@core/stores/deviceStore.js"; import { Protobuf } from "@meshtastic/js"; import { fromByteArray, toByteArray } from "base64-js"; +import cryptoRandomString from "crypto-random-string"; import { Eye, EyeOff } from "lucide-react"; import { useState } from "react"; @@ -13,6 +14,11 @@ export const Security = (): JSX.Element => { fromByteArray(config.security?.privateKey ?? new Uint8Array(0)), ); const [privateKeyVisible, setPrivateKeyVisible] = useState(false); + const [privateKeyBitCount, setPrivateKeyBitCount] = useState( + config.security?.privateKey.length ?? 16, + ); + const [privateKeyValidationText, setPrivateKeyValidationText] = + useState(); const [publicKey, setPublicKey] = useState( fromByteArray(config.security?.publicKey ?? new Uint8Array(0)), ); @@ -20,8 +26,15 @@ export const Security = (): JSX.Element => { fromByteArray(config.security?.adminKey ?? new Uint8Array(0)), ); const [adminKeyVisible, setAdminKeyVisible] = useState(false); + const [adminKeyBitCount, setAdminKeyBitCount] = useState( + config.security?.adminKey.length ?? 16, + ); + const [adminKeyValidationText, setAdminKeyValidationText] = + useState(); const onSubmit = (data: SecurityValidation) => { + if (privateKeyValidationText || adminKeyValidationText) return; + setWorkingConfig( new Protobuf.Config.Config({ payloadVariant: { @@ -36,14 +49,75 @@ export const Security = (): JSX.Element => { }), ); }; + + const clickEvent = ( + setKey: (value: React.SetStateAction) => void, + bitCount: number, + setValidationText: ( + value: React.SetStateAction, + ) => void, + ) => { + setKey( + btoa( + cryptoRandomString({ + length: bitCount ?? 0, + type: "alphanumeric", + }), + ), + ); + setValidationText(undefined); + }; + + const validatePass = ( + input: string, + count: number, + setValidationText: ( + value: React.SetStateAction, + ) => void, + ) => { + if (input.length % 4 !== 0 || toByteArray(input).length !== count) { + setValidationText(`Please enter a valid ${count * 8} bit PSK.`); + } else { + setValidationText(undefined); + } + }; + + const privateKeyInputChangeEvent = ( + e: React.ChangeEvent, + ) => { + const psk = e.currentTarget?.value; + setPrivateKey(psk); + validatePass(psk, privateKeyBitCount, setPrivateKeyValidationText); + }; + + const adminKeyInputChangeEvent = (e: React.ChangeEvent) => { + const psk = e.currentTarget?.value; + setAdminKey(psk); + validatePass(psk, privateKeyBitCount, setAdminKeyValidationText); + }; + + const privateKeySelectChangeEvent = (e: string) => { + const count = Number.parseInt(e); + setPrivateKeyBitCount(count); + validatePass(privateKey, count, setPrivateKeyValidationText); + }; + + const adminKeySelectChangeEvent = (e: string) => { + const count = Number.parseInt(e); + setAdminKeyBitCount(count); + validatePass(privateKey, count, setAdminKeyValidationText); + }; + return ( onSubmit={onSubmit} defaultValues={{ ...config.security, - adminKey: adminKey, - privateKey: privateKey, - publicKey: publicKey, + ...{ + adminKey: adminKey, + privateKey: privateKey, + publicKey: publicKey, + }, }} fieldGroups={[ { @@ -51,10 +125,20 @@ export const Security = (): JSX.Element => { description: "Settings for the Security configuration", fields: [ { - type: privateKeyVisible ? "text" : "password", + type: "passwordGenerator", name: "privateKey", label: "Private Key", description: "Used to create a shared key with a remote device", + validationText: privateKeyValidationText, + devicePSKBitCount: privateKeyBitCount, + inputChange: privateKeyInputChangeEvent, + selectChange: privateKeySelectChangeEvent, + buttonClick: () => + clickEvent( + setPrivateKey, + privateKeyBitCount, + setPrivateKeyValidationText, + ), disabledBy: [ { fieldName: "adminChannelEnabled", @@ -62,6 +146,7 @@ export const Security = (): JSX.Element => { }, ], properties: { + value: privateKey, action: { icon: privateKeyVisible ? EyeOff : Eye, onClick: () => setPrivateKeyVisible(!privateKeyVisible), @@ -97,18 +182,29 @@ export const Security = (): JSX.Element => { 'If true, device is considered to be "managed" by a mesh administrator via admin messages', }, { - type: adminKeyVisible ? "text" : "password", + type: "passwordGenerator", name: "adminKey", label: "Admin Key", + description: + "The public key authorized to send admin messages to this node", + validationText: adminKeyValidationText, + devicePSKBitCount: adminKeyBitCount, + inputChange: adminKeyInputChangeEvent, + selectChange: adminKeySelectChangeEvent, + buttonClick: () => + clickEvent( + setAdminKey, + adminKeyBitCount, + setAdminKeyValidationText, + ), disabledBy: [{ fieldName: "adminChannelEnabled" }], properties: { + value: adminKey, action: { icon: adminKeyVisible ? EyeOff : Eye, onClick: () => setAdminKeyVisible(!adminKeyVisible), }, }, - description: - "The public key authorized to send admin messages to this node", }, ], },