@ -1,5 +1,6 @@
import { PkiRegenerateDialog } from "@app/components/Dialog/PkiRegenerateDialog" ;
import { PkiRegenerateDialog } from "@app/components/Dialog/PkiRegenerateDialog" ;
import { DynamicForm } from "@app/components/Form/DynamicForm.tsx" ;
import { DynamicForm } from "@app/components/Form/DynamicForm.tsx" ;
import { useAppStore } from "@app/core/stores/appStore" ;
import {
import {
getX25519PrivateKey ,
getX25519PrivateKey ,
getX25519PublicKey ,
getX25519PublicKey ,
@ -9,117 +10,130 @@ import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/js" ;
import { Protobuf } from "@meshtastic/js" ;
import { fromByteArray , toByteArray } from "base64-js" ;
import { fromByteArray , toByteArray } from "base64-js" ;
import { Eye , EyeOff } from "lucide-react" ;
import { Eye , EyeOff } from "lucide-react" ;
import { useState } from "react" ;
import { useReducer } from "react" ;
import { securityReducer } from "./securityReducer" ;
export const Security = ( ) : JSX . Element = > {
export const Security = ( ) = > {
const { config , nodes , hardware , setWorkingConfig , setDialogOpen } =
const { config , setWorkingConfig , setDialogOpen } = useDevice ( ) ;
useDevice ( ) ;
const {
hasErrors ,
getErrorMessage ,
hasFieldError ,
addError ,
removeError ,
clearErrors ,
} = useAppStore ( ) ;
const [ privateKey , setPrivateKey ] = useState < string > (
const [ state , dispatch ] = useReducer ( securityReducer , {
fromByteArray ( config . security ? . privateKey ? ? new Uint8Array ( 0 ) ) ,
privateKey : fromByteArray ( config . security ? . privateKey ? ? new Uint8Array ( 0 ) ) ,
) ;
privateKeyVisible : false ,
const [ privateKeyVisible , setPrivateKeyVisible ] = useState < boolean > ( false ) ;
adminKeyVisible : false ,
const [ privateKeyBitCount , setPrivateKeyBitCount ] = useState < number > (
privateKeyBitCount : config.security?.privateKey.length ? ? 32 ,
config . security ? . privateKey . length ? ? 32 ,
adminKeyBitCount : config.security?.adminKey [ 0 ] . length ? ? 32 ,
) ;
publicKey : fromByteArray ( config . security ? . publicKey ? ? new Uint8Array ( 0 ) ) ,
const [ privateKeyValidationText , setPrivateKeyValidationText ] =
adminKey : fromByteArray ( config . security ? . adminKey [ 0 ] ) ,
useState < string > ( ) ;
privateKeyDialogOpen : false ,
const [ publicKey , setPublicKey ] = useState < string > (
} ) ;
fromByteArray ( config . security ? . publicKey ? ? new Uint8Array ( 0 ) ) ,
) ;
const [ adminKey , setAdminKey ] = useState < string > (
fromByteArray ( config . security ? . adminKey [ 0 ] ? ? new Uint8Array ( 0 ) ) ,
) ;
const [ adminKeyValidationText , setAdminKeyValidationText ] =
useState < string > ( ) ;
const [ privateKeyDialogOpen , setPrivateKeyDialogOpen ] =
useState < boolean > ( false ) ;
const onSubmit = ( data : SecurityValidation ) = > {
if ( privateKeyValidationText || adminKeyValidationText ) return ;
setWorkingConfig (
new Protobuf . Config . Config ( {
payloadVariant : {
case : "security" ,
value : {
. . . data ,
adminKey : [ toByteArray ( adminKey ) ] ,
privateKey : toByteArray ( privateKey ) ,
publicKey : toByteArray ( publicKey ) ,
} ,
} ,
} ) ,
) ;
} ;
const validateKey = (
const validateKey = (
input : string ,
input : string ,
count : number ,
count : number ,
setValidationText : (
fieldName : "privateKey" | "adminKey" ,
value : React.SetStateAction < string | undefined > ,
) = > void ,
) = > {
) = > {
try {
try {
if ( input . length % 4 !== 0 || toByteArray ( input ) . length !== count ) {
removeError ( fieldName ) ;
setValidationText ( ` Please enter a valid ${ count * 8 } bit PSK. ` ) ;
} else {
if ( input === "" ) {
setValidationText ( undefined ) ;
addError (
fieldName ,
` ${ fieldName === "privateKey" ? "Private" : "Admin" } Key is required ` ,
) ;
return ;
}
if ( input . length % 4 !== 0 ) {
addError (
fieldName ,
` ${ fieldName === "privateKey" ? "Private" : "Admin" } Key is required to 256 bit pre-shared key (PSK) ` ,
) ;
return ;
}
const decoded = toByteArray ( input ) ;
if ( decoded . length !== count ) {
addError ( fieldName , ` Please enter a valid ${ count * 8 } bit PSK ` ) ;
return ;
}
}
} catch ( e ) {
} catch ( e ) {
console . error ( e ) ;
console . error ( e ) ;
setValidationText ( ` Please enter a valid ${ count * 8 } bit PSK. ` ) ;
addError (
fieldName ,
` Invalid ${ fieldName === "privateKey" ? "Private" : "Admin" } Key format ` ,
) ;
}
}
} ;
} ;
const privateKeyClickEvent = ( ) = > {
const onSubmit = ( data : SecurityValidation ) = > {
setPrivateKeyDialogOpen ( true ) ;
if ( hasErrors ( ) ) {
} ;
return ;
}
const pkiBackupClickEvent = ( ) = > {
setWorkingConfig (
setDialogOpen ( "pkiBackup" , true ) ;
new Protobuf . Config . Config ( {
payloadVariant : {
case : "security" ,
value : {
. . . data ,
adminKey : [ toByteArray ( state . adminKey ) ] ,
privateKey : toByteArray ( state . privateKey ) ,
publicKey : toByteArray ( state . publicKey ) ,
} ,
} ,
} ) ,
) ;
} ;
} ;
const pkiRegenerate = ( ) = > {
const pkiRegenerate = ( ) = > {
clearErrors ( ) ;
const privateKey = getX25519PrivateKey ( ) ;
const privateKey = getX25519PrivateKey ( ) ;
const publicKey = getX25519PublicKey ( privateKey ) ;
const publicKey = getX25519PublicKey ( privateKey ) ;
setPrivateKey ( fromByteArray ( privateKey ) ) ;
dispatch ( {
setPublicKey ( fromByteArray ( publicKey ) ) ;
type : "REGENERATE_PRIV_PUB_KEY" ,
payload : {
privateKey : fromByteArray ( privateKey ) ,
publicKey : fromByteArray ( publicKey ) ,
} ,
} ) ;
validateKey (
validateKey (
fromByteArray ( privateKey ) ,
fromByteArray ( privateKey ) ,
privateKeyBitCount ,
state . privateKeyBitCount ,
setPrivateKeyValidationText ,
"privateKey" ,
) ;
) ;
setPrivateKeyDialogOpen ( false ) ;
} ;
} ;
const privateKeyInputChangeEvent = (
const privateKeyInputChangeEvent = (
e : React.ChangeEvent < HTMLInputElement > ,
e : React.ChangeEvent < HTMLInputElement > ,
) = > {
) = > {
const privateKeyB64String = e . target . value ;
const privateKeyB64String = e . target . value ;
setPrivateKey ( privateKeyB64String ) ;
dispatch ( { type : "SET_PRIVATE_KEY" , payload : privateKeyB64String } ) ;
validateKey (
validateKey ( privateKeyB64String , state . privateKeyBitCount , "privateKey" ) ;
privateKeyB64String ,
privateKeyBitCount ,
setPrivateKeyValidationText ,
) ;
const publicKey = getX25519PublicKey ( toByteArray ( privateKeyB64String ) ) ;
const publicKey = getX25519PublicKey ( toByteArray ( privateKeyB64String ) ) ;
setPublicKey ( fromByteArray ( publicKey ) ) ;
dispatch ( { type : "SET_PUBLIC_KEY" , payload : fromByteArray ( publicKey ) } ) ;
} ;
} ;
const adminKeyInputChangeEvent = ( e : React.ChangeEvent < HTMLInputElement > ) = > {
const adminKeyInputChangeEvent = ( e : React.ChangeEvent < HTMLInputElement > ) = > {
const psk = e . currentTarget ? . value ;
const psk = e . currentTarget ? . value ;
setAdminKey ( psk ) ;
dispatch ( { type : "SET_ADMIN_KEY" , payload : psk } ) ;
validateKey ( psk , privateKeyBitCount , setAdminKeyValidationText ) ;
validateKey ( psk , state . privateKeyBitCount , "adminKey" ) ;
} ;
} ;
const privateKeySelectChangeEvent = ( e : string ) = > {
const privateKeySelectChangeEvent = ( e : string ) = > {
const count = Number . parseInt ( e ) ;
const count = Number . parseInt ( e ) ;
setPrivateKeyBitCount ( count ) ;
dispatch ( { type : "SET_PRIVATE_KEY_BIT_COUNT" , payload : count } ) ;
validateKey ( privateKey , count , setPrivateKeyValidationText ) ;
validateKey ( state . privateKey , count , "privateKey" ) ;
} ;
} ;
return (
return (
@ -130,9 +144,9 @@ export const Security = (): JSX.Element => {
defaultValues = { {
defaultValues = { {
. . . config . security ,
. . . config . security ,
. . . {
. . . {
adminKey : adminKey ,
adminKey : state. adminKey,
privateKey : privateKey ,
privateKey : state. privateKey,
publicKey : publicKey ,
publicKey : state. publicKey,
adminChannelEnabled : config.security?.adminChannelEnabled ? ? false ,
adminChannelEnabled : config.security?.adminChannelEnabled ? ? false ,
isManaged : config.security?.isManaged ? ? false ,
isManaged : config.security?.isManaged ? ? false ,
debugLogApiEnabled : config.security?.debugLogApiEnabled ? ? false ,
debugLogApiEnabled : config.security?.debugLogApiEnabled ? ? false ,
@ -150,28 +164,35 @@ export const Security = (): JSX.Element => {
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" ,
bits : [ { text : "256 bit" , value : "32" , key : "bit256" } ] ,
bits : [ { text : "256 bit" , value : "32" , key : "bit256" } ] ,
validationText : privateKeyValidationText ,
validationText : hasFieldError ( "privateKey" )
devicePSKBitCount : privateKeyBitCount ,
? getErrorMessage ( "privateKey" )
: "" ,
devicePSKBitCount : state.privateKeyBitCount ,
inputChange : privateKeyInputChangeEvent ,
inputChange : privateKeyInputChangeEvent ,
selectChange : privateKeySelectChangeEvent ,
selectChange : privateKeySelectChangeEvent ,
hide : ! privateKeyVisible ,
hide : ! state . privateKeyVisible ,
actionButtons : [
actionButtons : [
{
{
text : "Generate" ,
text : "Generate" ,
onClick : privateKeyClickEvent ,
onClick : ( ) = >
dispatch ( {
type : "SHOW_PRIVATE_KEY_DIALOG" ,
payload : true ,
} ) ,
variant : "success" ,
variant : "success" ,
} ,
} ,
{
{
text : "Backup Key" ,
text : "Backup Key" ,
onClick : pkiBackupClickEvent ,
onClick : ( ) = > setDialogOpen ( "pkiBackup" , true ) ,
variant : "subtle" ,
variant : "subtle" ,
} ,
} ,
] ,
] ,
properties : {
properties : {
value : privateKey ,
value : state. privateKey,
action : {
action : {
icon : privateKeyVisible ? EyeOff : Eye ,
icon : state.privateKeyVisible ? EyeOff : Eye ,
onClick : ( ) = > setPrivateKeyVisible ( ! privateKeyVisible ) ,
onClick : ( ) = >
dispatch ( { type : "TOGGLE_PRIVATE_KEY_VISIBILITY" } ) ,
} ,
} ,
} ,
} ,
} ,
} ,
@ -183,7 +204,7 @@ export const Security = (): JSX.Element => {
description :
description :
"Sent out to other nodes on the mesh to allow them to compute a shared secret key" ,
"Sent out to other nodes on the mesh to allow them to compute a shared secret key" ,
properties : {
properties : {
value : publicKey ,
value : state. publicKey,
} ,
} ,
} ,
} ,
] ,
] ,
@ -207,18 +228,47 @@ export const Security = (): JSX.Element => {
"If true, device configuration options are only able to be changed remotely by a Remote Admin node via admin messages. Do not enable this option unless a suitable Remote Admin node has been setup, and the public key stored in the field below." ,
"If true, device configuration options are only able to be changed remotely by a Remote Admin node via admin messages. Do not enable this option unless a suitable Remote Admin node has been setup, and the public key stored in the field below." ,
} ,
} ,
{
{
type : "text " ,
type : "passwordGenerator " ,
name : "adminKey" ,
name : "adminKey" ,
label : "Admin Key" ,
label : "Admin Key" ,
description :
description :
"The public key authorized to send admin messages to this node" ,
"The public key authorized to send admin messages to this node" ,
validationText : adminKeyValidationText ,
validationText : hasFieldError ( "adminKey" )
? getErrorMessage ( "adminKey" )
: "" ,
inputChange : adminKeyInputChangeEvent ,
inputChange : adminKeyInputChangeEvent ,
selectChange : ( ) = > { } ,
bits : [ { text : "256 bit" , value : "32" , key : "bit256" } ] ,
devicePSKBitCount : state.privateKeyBitCount ,
hide : ! state . adminKeyVisible ,
actionButtons : [
{
text : "Generate" ,
variant : "success" ,
onClick : ( ) = > {
const adminKey = getX25519PrivateKey ( ) ;
dispatch ( {
type : "REGENERATE_ADMIN_KEY" ,
payload : { adminKey : fromByteArray ( adminKey ) } ,
} ) ;
validateKey (
fromByteArray ( adminKey ) ,
state . adminKeyBitCount ,
"adminKey" ,
) ;
} ,
} ,
] ,
disabledBy : [
disabledBy : [
{ fieldName : "adminChannelEnabled" , invert : true } ,
{ fieldName : "adminChannelEnabled" , invert : true } ,
] ,
] ,
properties : {
properties : {
value : adminKey ,
value : state.adminKey ,
action : {
icon : state.adminKeyVisible ? EyeOff : Eye ,
onClick : ( ) = >
dispatch ( { type : "TOGGLE_ADMIN_KEY_VISIBILITY" } ) ,
} ,
} ,
} ,
} ,
} ,
] ,
] ,
@ -245,9 +295,11 @@ export const Security = (): JSX.Element => {
] }
] }
/ >
/ >
< PkiRegenerateDialog
< PkiRegenerateDialog
open = { privateKeyDialogOpen }
open = { state . privateKeyDialogOpen }
onOpenChange = { ( ) = > setPrivateKeyDialogOpen ( false ) }
onOpenChange = { ( ) = >
onSubmit = { ( ) = > pkiRegenerate ( ) }
dispatch ( { type : "SHOW_PRIVATE_KEY_DIALOG" , payload : false } )
}
onSubmit = { pkiRegenerate }
/ >
/ >
< / >
< / >
) ;
) ;