Browse Source

Merge branch 'pki' into disabled-dynamicform

pull/290/head
Hunter Thornsberry 2 years ago
committed by GitHub
parent
commit
6375187d50
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      package.json
  2. 10
      pnpm-lock.yaml
  3. 8
      src/components/CommandPalette.tsx
  4. 4
      src/components/Form/FormPasswordGenerator.tsx
  5. 112
      src/components/PageComponents/Config/Security.tsx
  6. 16
      src/components/UI/Generator.tsx

2
package.json

@ -23,7 +23,7 @@
"dependencies": { "dependencies": {
"@bufbuild/protobuf": "^1.10.0", "@bufbuild/protobuf": "^1.10.0",
"@emeraldpay/hashicon-react": "^0.5.2", "@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-accordion": "^1.2.0",
"@radix-ui/react-checkbox": "^1.1.0", "@radix-ui/react-checkbox": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dialog": "^1.1.1",

10
pnpm-lock.yaml

@ -15,8 +15,8 @@ importers:
specifier: ^0.5.2 specifier: ^0.5.2
version: 0.5.2 version: 0.5.2
'@meshtastic/js': '@meshtastic/js':
specifier: 2.3.7-0 specifier: 2.3.7-1
version: 2.3.7-0 version: 2.3.7-1
'@radix-ui/react-accordion': '@radix-ui/react-accordion':
specifier: ^1.2.0 specifier: ^1.2.0
version: 1.2.0(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected]) version: 1.2.0(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
@ -581,8 +581,8 @@ packages:
resolution: {integrity: sha512-eSiQ3E5LUSxAOY9ABXGyfNhout2iEa6mUxKeaQ9nJ8NL1NuaQYU7zKqzx/LEYcXe1neT4uYAgM1wYZj3fTSXtA==} resolution: {integrity: sha512-eSiQ3E5LUSxAOY9ABXGyfNhout2iEa6mUxKeaQ9nJ8NL1NuaQYU7zKqzx/LEYcXe1neT4uYAgM1wYZj3fTSXtA==}
hasBin: true hasBin: true
'@meshtastic/[email protected]0': '@meshtastic/[email protected]1':
resolution: {integrity: sha512-XTNyUXj3SWQ91XqwgrTZT7rTQsiI3d8noRaxnpxRw6Ck7WtjjPF0ygnPA8eQ6kastyUkgpXzcjtD9a6Qz6n+WQ==} resolution: {integrity: sha512-pv+Xk6HkKrScCrQp31k5QOUYozabXn6NhXN7c7Cc9ysG94U1wGtfueRbEbFxXCHO3JshNz0CdE1FcSMnrLMjsQ==}
'@nodelib/[email protected]': '@nodelib/[email protected]':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
@ -3477,7 +3477,7 @@ snapshots:
sort-object: 3.0.3 sort-object: 3.0.3
tinyqueue: 2.0.3 tinyqueue: 2.0.3
'@meshtastic/[email protected]0': '@meshtastic/[email protected]1':
dependencies: dependencies:
crc: 4.3.2 crc: 4.3.2
ste-simple-events: 3.0.11 ste-simple-events: 3.0.11

8
src/components/CommandPalette.tsx

@ -200,17 +200,17 @@ export const CommandPalette = (): JSX.Element => {
}, },
}, },
{ {
label: "Soft Factory Reset", label: "Factory Reset Device",
icon: FactoryIcon, icon: FactoryIcon,
action() { action() {
connection?.factoryResetConfig(); connection?.factoryResetDevice();
}, },
}, },
{ {
label: "Hard Factory Reset", label: "Factory Reset Config",
icon: FactoryIcon, icon: FactoryIcon,
action() { action() {
connection?.factoryResetDevice(); connection?.factoryResetConfig();
}, },
}, },
], ],

4
src/components/Form/FormPasswordGenerator.tsx

@ -8,6 +8,7 @@ import { Controller, type FieldValues } from "react-hook-form";
export interface PasswordGeneratorProps<T> extends BaseFormBuilderProps<T> { export interface PasswordGeneratorProps<T> extends BaseFormBuilderProps<T> {
type: "passwordGenerator"; type: "passwordGenerator";
hide?: boolean;
devicePSKBitCount: number; devicePSKBitCount: number;
inputChange: ChangeEventHandler; inputChange: ChangeEventHandler;
selectChange: (event: string) => void; selectChange: (event: string) => void;
@ -17,6 +18,7 @@ export interface PasswordGeneratorProps<T> extends BaseFormBuilderProps<T> {
export function PasswordGenerator<T extends FieldValues>({ export function PasswordGenerator<T extends FieldValues>({
control, control,
field, field,
disabled,
}: GenericFormElementProps<T, PasswordGeneratorProps<T>>) { }: GenericFormElementProps<T, PasswordGeneratorProps<T>>) {
return ( return (
<Controller <Controller
@ -24,6 +26,7 @@ export function PasswordGenerator<T extends FieldValues>({
control={control} control={control}
render={({ field: { value, ...rest } }) => ( render={({ field: { value, ...rest } }) => (
<Generator <Generator
hide={field.hide}
devicePSKBitCount={field.devicePSKBitCount} devicePSKBitCount={field.devicePSKBitCount}
inputChange={field.inputChange} inputChange={field.inputChange}
selectChange={field.selectChange} selectChange={field.selectChange}
@ -33,6 +36,7 @@ export function PasswordGenerator<T extends FieldValues>({
buttonText="Generate" buttonText="Generate"
{...field.properties} {...field.properties}
{...rest} {...rest}
disabled={disabled}
/> />
)} )}
/> />

112
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 { useDevice } from "@core/stores/deviceStore.js";
import { Protobuf } from "@meshtastic/js"; import { Protobuf } from "@meshtastic/js";
import { fromByteArray, toByteArray } from "base64-js"; import { fromByteArray, toByteArray } from "base64-js";
import cryptoRandomString from "crypto-random-string";
import { Eye, EyeOff } from "lucide-react"; import { Eye, EyeOff } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
@ -13,6 +14,11 @@ export const Security = (): JSX.Element => {
fromByteArray(config.security?.privateKey ?? new Uint8Array(0)), fromByteArray(config.security?.privateKey ?? new Uint8Array(0)),
); );
const [privateKeyVisible, setPrivateKeyVisible] = useState<boolean>(false); const [privateKeyVisible, setPrivateKeyVisible] = useState<boolean>(false);
const [privateKeyBitCount, setPrivateKeyBitCount] = useState<number>(
config.security?.privateKey.length ?? 16,
);
const [privateKeyValidationText, setPrivateKeyValidationText] =
useState<string>();
const [publicKey, setPublicKey] = useState<string>( const [publicKey, setPublicKey] = useState<string>(
fromByteArray(config.security?.publicKey ?? new Uint8Array(0)), fromByteArray(config.security?.publicKey ?? new Uint8Array(0)),
); );
@ -20,8 +26,15 @@ export const Security = (): JSX.Element => {
fromByteArray(config.security?.adminKey ?? new Uint8Array(0)), fromByteArray(config.security?.adminKey ?? new Uint8Array(0)),
); );
const [adminKeyVisible, setAdminKeyVisible] = useState<boolean>(false); const [adminKeyVisible, setAdminKeyVisible] = useState<boolean>(false);
const [adminKeyBitCount, setAdminKeyBitCount] = useState<number>(
config.security?.adminKey.length ?? 16,
);
const [adminKeyValidationText, setAdminKeyValidationText] =
useState<string>();
const onSubmit = (data: SecurityValidation) => { const onSubmit = (data: SecurityValidation) => {
if (privateKeyValidationText || adminKeyValidationText) return;
setWorkingConfig( setWorkingConfig(
new Protobuf.Config.Config({ new Protobuf.Config.Config({
payloadVariant: { payloadVariant: {
@ -36,14 +49,75 @@ export const Security = (): JSX.Element => {
}), }),
); );
}; };
const clickEvent = (
setKey: (value: React.SetStateAction<string>) => void,
bitCount: number,
setValidationText: (
value: React.SetStateAction<string | undefined>,
) => void,
) => {
setKey(
btoa(
cryptoRandomString({
length: bitCount ?? 0,
type: "alphanumeric",
}),
),
);
setValidationText(undefined);
};
const validatePass = (
input: string,
count: number,
setValidationText: (
value: React.SetStateAction<string | undefined>,
) => 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<HTMLInputElement>,
) => {
const psk = e.currentTarget?.value;
setPrivateKey(psk);
validatePass(psk, privateKeyBitCount, setPrivateKeyValidationText);
};
const adminKeyInputChangeEvent = (e: React.ChangeEvent<HTMLInputElement>) => {
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 ( return (
<DynamicForm<SecurityValidation> <DynamicForm<SecurityValidation>
onSubmit={onSubmit} onSubmit={onSubmit}
defaultValues={{ defaultValues={{
...config.security, ...config.security,
adminKey: adminKey, ...{
privateKey: privateKey, adminKey: adminKey,
publicKey: publicKey, privateKey: privateKey,
publicKey: publicKey,
},
}} }}
fieldGroups={[ fieldGroups={[
{ {
@ -51,10 +125,21 @@ export const Security = (): JSX.Element => {
description: "Settings for the Security configuration", description: "Settings for the Security configuration",
fields: [ fields: [
{ {
type: privateKeyVisible ? "text" : "password", type: "passwordGenerator",
name: "privateKey", name: "privateKey",
label: "Private Key", label: "Private Key",
description: "Used to create a shared key with a remote device", 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: [ disabledBy: [
{ {
fieldName: "adminChannelEnabled", fieldName: "adminChannelEnabled",
@ -62,6 +147,7 @@ export const Security = (): JSX.Element => {
}, },
], ],
properties: { properties: {
value: privateKey,
action: { action: {
icon: privateKeyVisible ? EyeOff : Eye, icon: privateKeyVisible ? EyeOff : Eye,
onClick: () => setPrivateKeyVisible(!privateKeyVisible), 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', 'If true, device is considered to be "managed" by a mesh administrator via admin messages',
}, },
{ {
type: adminKeyVisible ? "text" : "password", type: "passwordGenerator",
name: "adminKey", name: "adminKey",
label: "Admin Key", 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" }], disabledBy: [{ fieldName: "adminChannelEnabled" }],
properties: { properties: {
value: adminKey,
action: { action: {
icon: adminKeyVisible ? EyeOff : Eye, icon: adminKeyVisible ? EyeOff : Eye,
onClick: () => setAdminKeyVisible(!adminKeyVisible), onClick: () => setAdminKeyVisible(!adminKeyVisible),
}, },
}, },
description:
"The public key authorized to send admin messages to this node",
}, },
], ],
}, },

16
src/components/UI/Generator.tsx

@ -9,8 +9,10 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@components/UI/Select.js"; } from "@components/UI/Select.js";
import type { LucideIcon } from "lucide-react";
export interface GeneratorProps extends React.BaseHTMLAttributes<HTMLElement> { export interface GeneratorProps extends React.BaseHTMLAttributes<HTMLElement> {
hide?: boolean;
devicePSKBitCount?: number; devicePSKBitCount?: number;
value: string; value: string;
variant: "default" | "invalid"; variant: "default" | "invalid";
@ -18,11 +20,17 @@ export interface GeneratorProps extends React.BaseHTMLAttributes<HTMLElement> {
selectChange: (event: string) => void; selectChange: (event: string) => void;
inputChange: (event: React.ChangeEvent<HTMLInputElement>) => void; inputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
buttonClick: React.MouseEventHandler<HTMLButtonElement>; buttonClick: React.MouseEventHandler<HTMLButtonElement>;
action?: {
icon: LucideIcon;
onClick: () => void;
};
disabled?: boolean;
} }
const Generator = React.forwardRef<HTMLInputElement, GeneratorProps>( const Generator = React.forwardRef<HTMLInputElement, GeneratorProps>(
( (
{ {
hide = true,
devicePSKBitCount, devicePSKBitCount,
variant, variant,
value, value,
@ -30,6 +38,8 @@ const Generator = React.forwardRef<HTMLInputElement, GeneratorProps>(
selectChange, selectChange,
inputChange, inputChange,
buttonClick, buttonClick,
action,
disabled,
...props ...props
}, },
ref, ref,
@ -37,17 +47,19 @@ const Generator = React.forwardRef<HTMLInputElement, GeneratorProps>(
return ( return (
<> <>
<Input <Input
type="text" type={hide ? "password" : "text"}
id="pskInput" id="pskInput"
variant={variant} variant={variant}
value={value} value={value}
onChange={inputChange} onChange={inputChange}
action={action}
disabled={disabled}
/> />
<Select <Select
value={devicePSKBitCount?.toString()} value={devicePSKBitCount?.toString()}
onValueChange={(e) => selectChange(e)} onValueChange={(e) => selectChange(e)}
> >
<SelectTrigger> <SelectTrigger className="!max-w-max">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>

Loading…
Cancel
Save