You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
301 lines
9.4 KiB
301 lines
9.4 KiB
import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig";
|
|
import {
|
|
type ParsedSecurity,
|
|
type RawSecurity,
|
|
RawSecuritySchema,
|
|
} from "@app/validation/config/security.ts";
|
|
import { create } from "@bufbuild/protobuf";
|
|
import { ManagedModeDialog } from "@components/Dialog/ManagedModeDialog.tsx";
|
|
import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx";
|
|
import { createZodResolver } from "@components/Form/createZodResolver.ts";
|
|
import {
|
|
DynamicForm,
|
|
type DynamicFormFormInit,
|
|
} from "@components/Form/DynamicForm.tsx";
|
|
import { useAppStore, useDevice } from "@core/stores";
|
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
|
import { getX25519PrivateKey, getX25519PublicKey } from "@core/utils/x25519.ts";
|
|
import { Protobuf } from "@meshtastic/core";
|
|
import { fromByteArray, toByteArray } from "base64-js";
|
|
import { useEffect, useState } from "react";
|
|
import { type DefaultValues, useForm } from "react-hook-form";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
interface SecurityConfigProps {
|
|
onFormInit: DynamicFormFormInit<RawSecurity>;
|
|
}
|
|
export const Security = ({ onFormInit }: SecurityConfigProps) => {
|
|
useWaitForConfig({ configCase: "security" });
|
|
|
|
const {
|
|
config,
|
|
setWorkingConfig,
|
|
setDialogOpen,
|
|
getEffectiveConfig,
|
|
removeWorkingConfig,
|
|
} = useDevice();
|
|
|
|
const { removeError } = useAppStore();
|
|
const { t } = useTranslation("deviceConfig");
|
|
|
|
const securityConfig = getEffectiveConfig("security");
|
|
const defaultValues = {
|
|
...securityConfig,
|
|
...{
|
|
privateKey: fromByteArray(
|
|
securityConfig?.privateKey ?? new Uint8Array(0),
|
|
),
|
|
publicKey: fromByteArray(securityConfig?.publicKey ?? new Uint8Array(0)),
|
|
adminKey: [
|
|
fromByteArray(securityConfig?.adminKey?.at(0) ?? new Uint8Array(0)),
|
|
fromByteArray(securityConfig?.adminKey?.at(1) ?? new Uint8Array(0)),
|
|
fromByteArray(securityConfig?.adminKey?.at(2) ?? new Uint8Array(0)),
|
|
],
|
|
},
|
|
};
|
|
|
|
const formMethods = useForm<RawSecurity>({
|
|
mode: "onChange",
|
|
defaultValues: defaultValues as DefaultValues<RawSecurity>,
|
|
resolver: createZodResolver(RawSecuritySchema),
|
|
shouldFocusError: false,
|
|
resetOptions: { keepDefaultValues: true },
|
|
});
|
|
const { setValue, formState } = formMethods;
|
|
|
|
useEffect(() => {
|
|
onFormInit?.(formMethods);
|
|
}, [onFormInit, formMethods]);
|
|
|
|
const [privateKeyDialogOpen, setPrivateKeyDialogOpen] =
|
|
useState<boolean>(false);
|
|
const [managedModeDialogOpen, setManagedModeDialogOpen] =
|
|
useState<boolean>(false);
|
|
|
|
const onSubmit = (data: RawSecurity) => {
|
|
if (!formState.isReady) {
|
|
return;
|
|
}
|
|
|
|
const payload: ParsedSecurity = {
|
|
...data,
|
|
privateKey: toByteArray(data.privateKey),
|
|
publicKey: toByteArray(data.publicKey),
|
|
adminKey: [
|
|
toByteArray(data.adminKey.at(0) ?? ""),
|
|
toByteArray(data.adminKey.at(1) ?? ""),
|
|
toByteArray(data.adminKey.at(2) ?? ""),
|
|
],
|
|
};
|
|
|
|
if (deepCompareConfig(config.security, payload, true)) {
|
|
removeWorkingConfig("security");
|
|
return;
|
|
}
|
|
|
|
setWorkingConfig(
|
|
create(Protobuf.Config.ConfigSchema, {
|
|
payloadVariant: {
|
|
case: "security",
|
|
value: payload,
|
|
},
|
|
}),
|
|
);
|
|
};
|
|
|
|
const pkiRegenerate = () => {
|
|
const privateKey = getX25519PrivateKey();
|
|
updatePublicKey(fromByteArray(privateKey));
|
|
};
|
|
|
|
const updatePublicKey = (privateKey: string) => {
|
|
try {
|
|
const publicKey = fromByteArray(
|
|
getX25519PublicKey(toByteArray(privateKey)),
|
|
);
|
|
setValue("privateKey", privateKey);
|
|
setValue("publicKey", publicKey);
|
|
|
|
removeError("privateKey");
|
|
removeError("publicKey");
|
|
setPrivateKeyDialogOpen(false);
|
|
} catch (_e) {
|
|
setValue("privateKey", privateKey);
|
|
}
|
|
};
|
|
|
|
const bits = [
|
|
{
|
|
text: t("security.256bit"),
|
|
value: "32",
|
|
key: "bit256",
|
|
},
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<DynamicForm<RawSecurity>
|
|
propMethods={formMethods}
|
|
onSubmit={onSubmit}
|
|
formId="Config_SecurityConfig"
|
|
fieldGroups={[
|
|
{
|
|
label: t("security.title"),
|
|
description: t("security.description"),
|
|
fields: [
|
|
{
|
|
type: "passwordGenerator",
|
|
id: "pskInput",
|
|
name: "privateKey",
|
|
label: t("security.privateKey.label"),
|
|
description: t("security.privateKey.description"),
|
|
bits,
|
|
devicePSKBitCount: 32,
|
|
hide: true,
|
|
inputChange: (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
updatePublicKey(e.target.value);
|
|
},
|
|
actionButtons: [
|
|
{
|
|
text: t("button.generate"),
|
|
onClick: () => setPrivateKeyDialogOpen(true),
|
|
variant: "success",
|
|
},
|
|
{
|
|
text: t("button.backupKey"),
|
|
onClick: () => setDialogOpen("pkiBackup", true),
|
|
variant: "subtle",
|
|
},
|
|
],
|
|
properties: {
|
|
showCopyButton: true,
|
|
showPasswordToggle: true,
|
|
},
|
|
},
|
|
{
|
|
type: "text",
|
|
name: "publicKey",
|
|
label: t("security.publicKey.label"),
|
|
disabled: true,
|
|
description: t("security.publicKey.description"),
|
|
properties: {
|
|
showCopyButton: true,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
label: t("security.adminSettings.label"),
|
|
description: t("security.adminSettings.description"),
|
|
fields: [
|
|
{
|
|
type: "passwordGenerator",
|
|
name: "adminKey.0",
|
|
id: "adminKey0Input",
|
|
label: t("security.primaryAdminKey.label"),
|
|
description: t("security.primaryAdminKey.description"),
|
|
bits,
|
|
devicePSKBitCount: 32,
|
|
actionButtons: [],
|
|
disabledBy: [
|
|
{ fieldName: "adminChannelEnabled", invert: true },
|
|
],
|
|
properties: {
|
|
showCopyButton: true,
|
|
},
|
|
},
|
|
{
|
|
type: "passwordGenerator",
|
|
name: "adminKey.1",
|
|
id: "adminKey1Input",
|
|
label: t("security.secondaryAdminKey.label"),
|
|
description: t("security.secondaryAdminKey.description"),
|
|
bits,
|
|
devicePSKBitCount: 32,
|
|
actionButtons: [],
|
|
disabledBy: [
|
|
{ fieldName: "adminChannelEnabled", invert: true },
|
|
],
|
|
properties: {
|
|
showCopyButton: true,
|
|
},
|
|
},
|
|
{
|
|
type: "passwordGenerator",
|
|
name: "adminKey.2",
|
|
id: "adminKey2Input",
|
|
label: t("security.tertiaryAdminKey.label"),
|
|
description: t("security.tertiaryAdminKey.description"),
|
|
bits,
|
|
devicePSKBitCount: 32,
|
|
actionButtons: [],
|
|
disabledBy: [
|
|
{ fieldName: "adminChannelEnabled", invert: true },
|
|
],
|
|
properties: {
|
|
showCopyButton: true,
|
|
},
|
|
},
|
|
{
|
|
type: "toggle",
|
|
name: "isManaged",
|
|
label: t("security.managed.label"),
|
|
description: t("security.managed.description"),
|
|
inputChange: (checked) => {
|
|
if (checked) {
|
|
setManagedModeDialogOpen(true);
|
|
}
|
|
|
|
setValue("isManaged", false);
|
|
},
|
|
},
|
|
{
|
|
type: "toggle",
|
|
name: "adminChannelEnabled",
|
|
label: t("security.adminChannelEnabled.label"),
|
|
description: t("security.adminChannelEnabled.description"),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
label: t("security.loggingSettings.label"),
|
|
description: t("security.loggingSettings.description"),
|
|
fields: [
|
|
{
|
|
type: "toggle",
|
|
name: "debugLogApiEnabled",
|
|
label: t("security.enableDebugLogApi.label"),
|
|
description: t("security.enableDebugLogApi.description"),
|
|
},
|
|
{
|
|
type: "toggle",
|
|
name: "serialEnabled",
|
|
label: t("security.serialOutputEnabled.label"),
|
|
description: t("security.serialOutputEnabled.description"),
|
|
},
|
|
],
|
|
},
|
|
]}
|
|
/>
|
|
<PkiRegenerateDialog
|
|
text={{
|
|
button: t("button.regenerate"),
|
|
title: t("pkiRegenerate.title"),
|
|
description: t("pkiRegenerate.description"),
|
|
}}
|
|
open={privateKeyDialogOpen}
|
|
onOpenChange={() => setPrivateKeyDialogOpen((prev) => !prev)}
|
|
onSubmit={pkiRegenerate}
|
|
/>
|
|
|
|
<ManagedModeDialog
|
|
open={managedModeDialogOpen}
|
|
onOpenChange={() => setManagedModeDialogOpen((prev) => !prev)}
|
|
onSubmit={() => {
|
|
setValue("isManaged", true);
|
|
setManagedModeDialogOpen(false);
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|