diff --git a/package.json b/package.json index b65359d9..5ef708e7 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "dependencies": { "@bufbuild/protobuf": "^1.10.0", "@emeraldpay/hashicon-react": "^0.5.2", - "@meshtastic/js": "2.3.7-0", + "@meshtastic/js": "2.3.7-1", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-checkbox": "^1.1.0", "@radix-ui/react-dialog": "^1.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62578557..8a5e82b6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: specifier: ^0.5.2 version: 0.5.2 '@meshtastic/js': - specifier: 2.3.7-0 - version: 2.3.7-0 + specifier: 2.3.7-1 + version: 2.3.7-1 '@radix-ui/react-accordion': specifier: ^1.2.0 version: 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -581,8 +581,8 @@ packages: resolution: {integrity: sha512-eSiQ3E5LUSxAOY9ABXGyfNhout2iEa6mUxKeaQ9nJ8NL1NuaQYU7zKqzx/LEYcXe1neT4uYAgM1wYZj3fTSXtA==} hasBin: true - '@meshtastic/js@2.3.7-0': - resolution: {integrity: sha512-XTNyUXj3SWQ91XqwgrTZT7rTQsiI3d8noRaxnpxRw6Ck7WtjjPF0ygnPA8eQ6kastyUkgpXzcjtD9a6Qz6n+WQ==} + '@meshtastic/js@2.3.7-1': + resolution: {integrity: sha512-pv+Xk6HkKrScCrQp31k5QOUYozabXn6NhXN7c7Cc9ysG94U1wGtfueRbEbFxXCHO3JshNz0CdE1FcSMnrLMjsQ==} '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -3477,7 +3477,7 @@ snapshots: sort-object: 3.0.3 tinyqueue: 2.0.3 - '@meshtastic/js@2.3.7-0': + '@meshtastic/js@2.3.7-1': dependencies: crc: 4.3.2 ste-simple-events: 3.0.11 diff --git a/src/components/CommandPalette.tsx b/src/components/CommandPalette.tsx index c101e6b0..2062b740 100644 --- a/src/components/CommandPalette.tsx +++ b/src/components/CommandPalette.tsx @@ -200,17 +200,17 @@ export const CommandPalette = (): JSX.Element => { }, }, { - label: "Soft Factory Reset", + label: "Factory Reset Device", icon: FactoryIcon, action() { - connection?.factoryResetConfig(); + connection?.factoryResetDevice(); }, }, { - label: "Hard Factory Reset", + label: "Factory Reset Config", icon: FactoryIcon, action() { - connection?.factoryResetDevice(); + connection?.factoryResetConfig(); }, }, ], diff --git a/src/components/Form/FormPasswordGenerator.tsx b/src/components/Form/FormPasswordGenerator.tsx index d015d3af..a94b0215 100644 --- a/src/components/Form/FormPasswordGenerator.tsx +++ b/src/components/Form/FormPasswordGenerator.tsx @@ -8,6 +8,7 @@ import { Controller, type FieldValues } from "react-hook-form"; export interface PasswordGeneratorProps extends BaseFormBuilderProps { type: "passwordGenerator"; + hide?: boolean; devicePSKBitCount: number; inputChange: ChangeEventHandler; selectChange: (event: string) => void; @@ -17,6 +18,7 @@ export interface PasswordGeneratorProps extends BaseFormBuilderProps { export function PasswordGenerator({ control, field, + disabled, }: GenericFormElementProps>) { return ( ({ control={control} render={({ field: { value, ...rest } }) => ( ({ buttonText="Generate" {...field.properties} {...rest} + disabled={disabled} /> )} /> diff --git a/src/components/PageComponents/Config/Security.tsx b/src/components/PageComponents/Config/Security.tsx index ba4d3d8b..2087f4b9 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,21 @@ 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, + hide: !privateKeyVisible, + buttonClick: () => + clickEvent( + setPrivateKey, + privateKeyBitCount, + setPrivateKeyValidationText, + ), disabledBy: [ { fieldName: "adminChannelEnabled", @@ -62,6 +147,7 @@ export const Security = (): JSX.Element => { }, ], properties: { + value: privateKey, action: { icon: privateKeyVisible ? EyeOff : Eye, onClick: () => setPrivateKeyVisible(!privateKeyVisible), @@ -97,18 +183,30 @@ 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, + hide: !adminKeyVisible, + 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", }, ], }, diff --git a/src/components/UI/Generator.tsx b/src/components/UI/Generator.tsx index 344e89bb..0e8b1e0d 100644 --- a/src/components/UI/Generator.tsx +++ b/src/components/UI/Generator.tsx @@ -9,8 +9,10 @@ import { SelectTrigger, SelectValue, } from "@components/UI/Select.js"; +import type { LucideIcon } from "lucide-react"; export interface GeneratorProps extends React.BaseHTMLAttributes { + hide?: boolean; devicePSKBitCount?: number; value: string; variant: "default" | "invalid"; @@ -18,11 +20,17 @@ export interface GeneratorProps extends React.BaseHTMLAttributes { selectChange: (event: string) => void; inputChange: (event: React.ChangeEvent) => void; buttonClick: React.MouseEventHandler; + action?: { + icon: LucideIcon; + onClick: () => void; + }; + disabled?: boolean; } const Generator = React.forwardRef( ( { + hide = true, devicePSKBitCount, variant, value, @@ -30,6 +38,8 @@ const Generator = React.forwardRef( selectChange, inputChange, buttonClick, + action, + disabled, ...props }, ref, @@ -37,17 +47,19 @@ const Generator = React.forwardRef( return ( <>