Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Key verification & generation #276

Merged
merged 2 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/components/Form/FormPasswordGenerator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Controller, type FieldValues } from "react-hook-form";

export interface PasswordGeneratorProps<T> extends BaseFormBuilderProps<T> {
type: "passwordGenerator";
hide?: boolean;
devicePSKBitCount: number;
inputChange: ChangeEventHandler;
selectChange: (event: string) => void;
Expand All @@ -17,13 +18,15 @@ export interface PasswordGeneratorProps<T> extends BaseFormBuilderProps<T> {
export function PasswordGenerator<T extends FieldValues>({
control,
field,
disabled,
}: GenericFormElementProps<T, PasswordGeneratorProps<T>>) {
return (
<Controller
name={field.name}
control={control}
render={({ field: { value, ...rest } }) => (
<Generator
hide={field.hide}
devicePSKBitCount={field.devicePSKBitCount}
inputChange={field.inputChange}
selectChange={field.selectChange}
Expand All @@ -33,6 +36,7 @@ export function PasswordGenerator<T extends FieldValues>({
buttonText="Generate"
{...field.properties}
{...rest}
disabled={disabled}
/>
)}
/>
Expand Down
112 changes: 105 additions & 7 deletions src/components/PageComponents/Config/Security.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -13,15 +14,27 @@ export const Security = (): JSX.Element => {
fromByteArray(config.security?.privateKey ?? new Uint8Array(0)),
);
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>(
fromByteArray(config.security?.publicKey ?? new Uint8Array(0)),
);
const [adminKey, setAdminKey] = useState<string>(
fromByteArray(config.security?.adminKey ?? new Uint8Array(0)),
);
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) => {
if (privateKeyValidationText || adminKeyValidationText) return;

setWorkingConfig(
new Protobuf.Config.Config({
payloadVariant: {
Expand All @@ -36,32 +49,105 @@ 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 (
<DynamicForm<SecurityValidation>
onSubmit={onSubmit}
defaultValues={{
...config.security,
adminKey: adminKey,
privateKey: privateKey,
publicKey: publicKey,
...{
adminKey: adminKey,
privateKey: privateKey,
publicKey: publicKey,
},
}}
fieldGroups={[
{
label: "Security Settings",
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",
invert: true,
},
],
properties: {
value: privateKey,
action: {
icon: privateKeyVisible ? EyeOff : Eye,
onClick: () => setPrivateKeyVisible(!privateKeyVisible),
Expand Down Expand Up @@ -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",
},
],
},
Expand Down
16 changes: 14 additions & 2 deletions src/components/UI/Generator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,45 +9,57 @@ import {
SelectTrigger,
SelectValue,
} from "@components/UI/Select.js";
import type { LucideIcon } from "lucide-react";

export interface GeneratorProps extends React.BaseHTMLAttributes<HTMLElement> {
hide?: boolean;
devicePSKBitCount?: number;
value: string;
variant: "default" | "invalid";
buttonText?: string;
selectChange: (event: string) => void;
inputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
buttonClick: React.MouseEventHandler<HTMLButtonElement>;
action?: {
icon: LucideIcon;
onClick: () => void;
};
disabled?: boolean;
}

const Generator = React.forwardRef<HTMLInputElement, GeneratorProps>(
(
{
hide = true,
devicePSKBitCount,
variant,
value,
buttonText,
selectChange,
inputChange,
buttonClick,
action,
disabled,
...props
},
ref,
) => {
return (
<>
<Input
type="text"
type={hide ? "password" : "text"}
id="pskInput"
variant={variant}
value={value}
onChange={inputChange}
action={action}
disabled={disabled}
/>
<Select
value={devicePSKBitCount?.toString()}
onValueChange={(e) => selectChange(e)}
>
<SelectTrigger>
<SelectTrigger className="!max-w-max">
<SelectValue />
</SelectTrigger>
<SelectContent>
Expand Down