@ -9,6 +9,7 @@ import { Protobuf } from "@meshtastic/core";
import { fromByteArray , toByteArray } from "base64-js" ;
import { fromByteArray , toByteArray } from "base64-js" ;
import { useReducer } from "react" ;
import { useReducer } from "react" ;
import { securityReducer } from "@components/PageComponents/Config/Security/securityReducer.tsx" ;
import { securityReducer } from "@components/PageComponents/Config/Security/securityReducer.tsx" ;
import type { SecurityConfigInit } from "./types.ts" ;
export const Security = ( ) = > {
export const Security = ( ) = > {
const { config , setWorkingConfig , setDialogOpen } = useDevice ( ) ;
const { config , setWorkingConfig , setDialogOpen } = useDevice ( ) ;
@ -24,36 +25,53 @@ export const Security = () => {
const [ state , dispatch ] = useReducer ( securityReducer , {
const [ state , dispatch ] = useReducer ( securityReducer , {
privateKey : fromByteArray ( config . security ? . privateKey ? ? new Uint8Array ( 0 ) ) ,
privateKey : fromByteArray ( config . security ? . privateKey ? ? new Uint8Array ( 0 ) ) ,
privateKeyVisible : false ,
privateKeyVisible : false ,
adminKeyVisible : false ,
adminKeyVisible : [ false , false , false ] ,
privateKeyBitCount : config.security?.privateKey?.length ? ? 32 ,
privateKeyBitCount : config.security?.privateKey?.length ? ? 32 ,
adminKeyBitCount : config.security?.adminKey?.at ( 0 ) ? . length ? ? 32 ,
publicKey : fromByteArray ( config . security ? . publicKey ? ? new Uint8Array ( 0 ) ) ,
publicKey : fromByteArray ( config . security ? . publicKey ? ? new Uint8Array ( 0 ) ) ,
adminKey : fromByteArray (
adminKey : [
config . security ? . adminKey ? . at ( 0 ) ? ? new Uint8Array ( 0 ) ,
fromByteArray ( config . security ? . adminKey ? . at ( 0 ) ? ? new Uint8Array ( 0 ) ) ,
) ,
fromByteArray ( config . security ? . adminKey ? . at ( 1 ) ? ? new Uint8Array ( 0 ) ) ,
fromByteArray ( config . security ? . adminKey ? . at ( 2 ) ? ? new Uint8Array ( 0 ) ) ,
] ,
privateKeyDialogOpen : false ,
privateKeyDialogOpen : false ,
isManaged : config.security?.isManaged ? ? false ,
adminChannelEnabled : config.security?.adminChannelEnabled ? ? false ,
debugLogApiEnabled : config.security?.debugLogApiEnabled ? ? false ,
serialEnabled : config.security?.serialEnabled ? ? false ,
} ) ;
} ) ;
const validateKey = (
const validateKey = (
input : string ,
input : string ,
count : number ,
count : number ,
fieldName : "privateKey" | "adminKey" ,
fieldName : "privateKey" | "adminKey" ,
fieldIndex? : number ,
) = > {
) = > {
const fieldNameKey = fieldName + ( fieldIndex ? ? "" ) ;
try {
try {
removeError ( fieldName ) ;
removeError ( fieldNameKey ) ;
if ( fieldName === "privateKey" && input === "" ) {
if ( fieldName === "privateKey" && input === "" ) {
addError ( fieldName , "Private Key is required" ) ;
addError ( fieldNameKey , "Private Key is required" ) ;
return ;
return ;
}
}
if ( fieldName === "adminKey" && input === "" ) {
if ( fieldName === "adminKey" && input === "" ) {
if (
state . isManaged && state . adminKey
. map ( ( v , i ) = > i === fieldIndex ? input : v )
. every ( ( s ) = > s === "" )
) {
addError (
"adminKey0" ,
"At least one admin key is requred if the node is managed." ,
) ;
}
return ;
return ;
}
}
if ( input . length % 4 !== 0 ) {
if ( input . length % 4 !== 0 ) {
addError (
addError (
fieldName ,
fieldNameKey ,
` ${
` ${
fieldName === "privateKey" ? "Private" : "Admin"
fieldName === "privateKey" ? "Private" : "Admin"
} Key is required to be a 256 bit pre - shared key ( PSK ) ` ,
} Key is required to be a 256 bit pre - shared key ( PSK ) ` ,
@ -63,13 +81,13 @@ export const Security = () => {
const decoded = toByteArray ( input ) ;
const decoded = toByteArray ( input ) ;
if ( decoded . length !== count ) {
if ( decoded . length !== count ) {
addError ( fieldName , ` Please enter a valid ${ count * 8 } bit PSK ` ) ;
addError ( fieldNameKey , ` Please enter a valid ${ count * 8 } bit PSK ` ) ;
return ;
return ;
}
}
} catch ( e ) {
} catch ( e ) {
console . error ( e ) ;
console . error ( e ) ;
addError (
addError (
fieldName ,
fieldNameKey ,
` Invalid ${
` Invalid ${
fieldName === "privateKey" ? "Private" : "Admin"
fieldName === "privateKey" ? "Private" : "Admin"
} Key format ` ,
} Key format ` ,
@ -77,24 +95,32 @@ export const Security = () => {
}
}
} ;
} ;
const onSubmit = ( data : SecurityValidation ) = > {
function setSecurityPayload (
if ( hasErrors ( ) ) {
overrides : SecurityConfigInit ,
return ;
) {
}
const base : SecurityConfigInit = {
isManaged : state.isManaged ,
adminChannelEnabled : state.adminChannelEnabled ,
debugLogApiEnabled : state.debugLogApiEnabled ,
serialEnabled : state.serialEnabled ,
privateKey : overrides?.privateKey ? ? toByteArray ( state . privateKey ) ,
publicKey : overrides?.publicKey ? ? toByteArray ( state . publicKey ) ,
adminKey : [
overrides ? . adminKey ? . [ 0 ] ? ? toByteArray ( state . adminKey [ 0 ] ) ,
overrides ? . adminKey ? . [ 0 ] ? ? toByteArray ( state . adminKey [ 0 ] ) ,
overrides ? . adminKey ? . [ 0 ] ? ? toByteArray ( state . adminKey [ 0 ] ) ,
] ,
} ;
setWorkingConfig (
setWorkingConfig (
create ( Protobuf . Config . ConfigSchema , {
create ( Protobuf . Config . ConfigSchema , {
payloadVariant : {
payloadVariant : {
case : "security" ,
case : "security" ,
value : {
value : { . . . base , . . . overrides } ,
. . . data ,
adminKey : [ new Uint8Array ( 0 ) ] ,
privateKey : toByteArray ( state . privateKey ) ,
publicKey : toByteArray ( state . publicKey ) ,
} ,
} ,
} ,
} ) ,
} ) ,
) ;
) ;
} ;
}
const pkiRegenerate = ( ) = > {
const pkiRegenerate = ( ) = > {
clearErrors ( ) ;
clearErrors ( ) ;
@ -114,6 +140,13 @@ export const Security = () => {
state . privateKeyBitCount ,
state . privateKeyBitCount ,
"privateKey" ,
"privateKey" ,
) ;
) ;
if ( ! hasErrors ( ) ) {
setSecurityPayload ( {
privateKey : privateKey ,
publicKey : publicKey ,
} ) ;
}
} ;
} ;
const privateKeyInputChangeEvent = (
const privateKeyInputChangeEvent = (
@ -125,25 +158,86 @@ export const Security = () => {
const publicKey = getX25519PublicKey ( toByteArray ( privateKeyB64String ) ) ;
const publicKey = getX25519PublicKey ( toByteArray ( privateKeyB64String ) ) ;
dispatch ( { type : "SET_PUBLIC_KEY" , payload : fromByteArray ( publicKey ) } ) ;
dispatch ( { type : "SET_PUBLIC_KEY" , payload : fromByteArray ( publicKey ) } ) ;
if ( ! hasErrors ( ) ) {
setSecurityPayload ( {
privateKey : toByteArray ( privateKeyB64String ) ,
publicKey : publicKey ,
} ) ;
}
} ;
} ;
const adminKeyInputChangeEvent = ( e : React.ChangeEvent < HTMLInputElement > ) = > {
const adminKeyInputChangeEvent = (
const psk = e . currentTarget ? . value ;
e : React.ChangeEvent < HTMLInputElement > ,
dispatch ( { type : "SET_ADMIN_KEY" , payload : psk } ) ;
fieldIndex? : number ,
validateKey ( psk , state . privateKeyBitCount , "adminKey" ) ;
) = > {
if ( fieldIndex === undefined ) return ;
const psk = e . target . value ;
const payload = [
fieldIndex === 0 ? psk : state.adminKey [ 0 ] ,
fieldIndex === 1 ? psk : state.adminKey [ 1 ] ,
fieldIndex === 2 ? psk : state.adminKey [ 2 ] ,
] satisfies [ string , string , string ] ;
dispatch ( { type : "SET_ADMIN_KEY" , payload : payload } ) ;
validateKey ( psk , state . privateKeyBitCount , "adminKey" , fieldIndex ) ;
if ( ! hasErrors ( ) ) {
setSecurityPayload ( {
adminKey : payload.map ( toByteArray ) as [
Uint8Array ,
Uint8Array ,
Uint8Array ,
] ,
} ) ;
}
} ;
} ;
const privateKeySelectChangeEvent = ( e : string ) = > {
const onToggleChange = (
const count = Number . parseInt ( e ) ;
field :
dispatch ( { type : "SET_PRIVATE_KEY_BIT_COUNT" , payload : count } ) ;
| "isManaged"
validateKey ( state . privateKey , count , "privateKey" ) ;
| "adminChannelEnabled"
| "debugLogApiEnabled"
| "serialEnabled" ,
next : boolean ,
) = > {
dispatch ( { type : "SET_TOGGLE" , field , payload : next } ) ;
if (
field === "isManaged" && state . adminKey . every ( ( s ) = > s === "" )
) {
if ( next ) {
addError (
"adminKey0" ,
"At least one admin key is requred if the node is managed." ,
) ;
} else {
removeError ( "adminKey0" ) ;
removeError ( "adminKey1" ) ;
removeError ( "adminKey2" ) ;
}
}
if ( ! hasErrors ( ) ) {
setSecurityPayload ( {
isManaged : field === "isManaged" ? next : state.isManaged ,
adminChannelEnabled : field === "adminChannelEnabled"
? next
: state . adminChannelEnabled ,
debugLogApiEnabled : field === "debugLogApiEnabled"
? next
: state . debugLogApiEnabled ,
serialEnabled : field === "serialEnabled" ? next : state.serialEnabled ,
} ) ;
}
} ;
} ;
return (
return (
< >
< >
< DynamicForm < SecurityValidation >
< DynamicForm < SecurityValidation >
onSubmit = { onSubmit }
onSubmit = { ( ) = > { } }
submitType = "onChange"
submitType = "onSubmit "
defaultValues = { {
defaultValues = { {
. . . config . security ,
. . . config . security ,
. . . {
. . . {
@ -173,7 +267,7 @@ export const Security = () => {
: "" ,
: "" ,
devicePSKBitCount : state.privateKeyBitCount ,
devicePSKBitCount : state.privateKeyBitCount ,
inputChange : privateKeyInputChangeEvent ,
inputChange : privateKeyInputChangeEvent ,
selectChange : privateKeySelectChangeEvent ,
selectChange : ( ) = > { } ,
hide : ! state . privateKeyVisible ,
hide : ! state . privateKeyVisible ,
actionButtons : [
actionButtons : [
{
{
@ -216,58 +310,107 @@ export const Security = () => {
description : "Settings for Admin" ,
description : "Settings for Admin" ,
fields : [
fields : [
{
{
type : "toggle" ,
type : "passwordGenerator" ,
name : "adminChannelEnabled" ,
name : "adminKey.0" ,
label : "Allow Legacy Admin" ,
id : "adminKey0Input" ,
label : "Primary Admin Key" ,
description :
description :
"Allow incoming device control over the insecure legacy admin channel" ,
"The primary public key authorized to send admin messages to this node" ,
validationText : hasFieldError ( "adminKey0" )
? getErrorMessage ( "adminKey0" )
: "" ,
inputChange : ( e ) = > adminKeyInputChangeEvent ( e , 0 ) ,
selectChange : ( ) = > { } ,
bits : [ { text : "256 bit" , value : "32" , key : "bit256" } ] ,
devicePSKBitCount : state.privateKeyBitCount ,
hide : ! state . adminKeyVisible [ 0 ] ,
actionButtons : [ ] ,
disabledBy : [
{ fieldName : "adminChannelEnabled" , invert : true } ,
] ,
properties : {
value : state.adminKey [ 0 ] ,
showCopyButton : true ,
showPasswordToggle : true ,
} ,
} ,
} ,
{
{
type : "toggle" ,
type : "passwordGenerator" ,
name : "isManaged" ,
name : "adminKey.1" ,
label : "Managed" ,
id : "adminKey1Input" ,
label : "Secondary Admin Key" ,
description :
description :
"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." ,
"The secondary public key authorized to send admin messages to this node" ,
validationText : hasFieldError ( "adminKey1" )
? getErrorMessage ( "adminKey1" )
: "" ,
inputChange : ( e ) = > adminKeyInputChangeEvent ( e , 1 ) ,
selectChange : ( ) = > { } ,
bits : [ { text : "256 bit" , value : "32" , key : "bit256" } ] ,
devicePSKBitCount : state.privateKeyBitCount ,
hide : ! state . adminKeyVisible [ 1 ] ,
actionButtons : [ ] ,
disabledBy : [
{ fieldName : "adminChannelEnabled" , invert : true } ,
] ,
properties : {
value : state.adminKey [ 1 ] ,
showCopyButton : true ,
showPasswordToggle : true ,
} ,
} ,
} ,
{
{
type : "passwordGenerator" ,
type : "passwordGenerator" ,
name : "adminKey" ,
name : "adminKey.2 " ,
id : "adminKeyInput" ,
id : "adminKey2 Input" ,
label : "Admin Key" ,
label : "Tertiary Admin Key" ,
description :
description :
"The public key authorized to send admin messages to this node" ,
"The tertiary public key authorized to send admin messages to this node" ,
validationText : hasFieldError ( "adminKey" )
validationText : hasFieldError ( "adminKey2 " )
? getErrorMessage ( "adminKey" )
? getErrorMessage ( "adminKey2 " )
: "" ,
: "" ,
inputChange : adminKeyInputChangeEvent ,
inputChange : ( e ) = > adminKeyInputChangeEvent ( e , 2 ) ,
selectChange : ( ) = > { } ,
selectChange : ( ) = > { } ,
bits : [ { text : "256 bit" , value : "32" , key : "bit256" } ] ,
bits : [ { text : "256 bit" , value : "32" , key : "bit256" } ] ,
devicePSKBitCount : state.privateKeyBitCount ,
devicePSKBitCount : state.privateKeyBitCount ,
hide : ! state . adminKeyVisible ,
hide : ! state . adminKeyVisible [ 2 ] ,
actionButtons : [
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 : state.adminKey ,
value : state.adminKey [ 2 ] ,
showCopyButton : true ,
showCopyButton : true ,
showPasswordToggle : true ,
} ,
} ,
{
type : "toggle" ,
name : "isManaged" ,
label : "Managed" ,
description :
"If enabled, device configuration options are only able to be changed remotely by a Remote Admin node via admin messages. Do not enable this option unless at least one suitable Remote Admin node has been setup, and the public key is stored in one of the fields above." ,
inputChange : ( e : boolean ) = > onToggleChange ( "isManaged" , e ) ,
properties : {
checked : state.isManaged ,
} ,
disabled : (
( hasFieldError ( "adminKey0" ) ||
hasFieldError ( "adminKey1" ) ||
hasFieldError ( "adminKey2" ) ) &&
! state . adminKey . every ( ( s ) = > s === "" )
) ,
} ,
{
type : "toggle" ,
name : "adminChannelEnabled" ,
label : "Allow Legacy Admin" ,
description :
"Allow incoming device control over the insecure legacy admin channel" ,
inputChange : ( e : boolean ) = >
onToggleChange ( "adminChannelEnabled" , e ) ,
properties : {
checked : state.adminChannelEnabled ,
} ,
} ,
} ,
} ,
] ,
] ,
@ -282,12 +425,21 @@ export const Security = () => {
label : "Enable Debug Log API" ,
label : "Enable Debug Log API" ,
description :
description :
"Output live debug logging over serial, view and export position-redacted device logs over Bluetooth" ,
"Output live debug logging over serial, view and export position-redacted device logs over Bluetooth" ,
inputChange : ( e : boolean ) = >
onToggleChange ( "debugLogApiEnabled" , e ) ,
properties : {
checked : state.debugLogApiEnabled ,
} ,
} ,
} ,
{
{
type : "toggle" ,
type : "toggle" ,
name : "serialEnabled" ,
name : "serialEnabled" ,
label : "Serial Output Enabled" ,
label : "Serial Output Enabled" ,
description : "Serial Console over the Stream API" ,
description : "Serial Console over the Stream API" ,
inputChange : ( e : boolean ) = > onToggleChange ( "serialEnabled" , e ) ,
properties : {
checked : state.serialEnabled ,
} ,
} ,
} ,
] ,
] ,
} ,
} ,