From e1a4900dc0912ec423fdb54535339cb048427068 Mon Sep 17 00:00:00 2001 From: Seth Golub Date: Thu, 28 May 2026 10:57:50 -0700 Subject: [PATCH] Add UI for setting repeater identity keys This adds a simple UI in repeater management settings for remotely setting a repeater's private key. A key can be pasted from an external key generator, or a random one can be generated here, optionally with a specified prefix. If a prefix is specified, searching is done in 30ms batches in order to keep the UI responsive and show the progress indicator animation smoothly. Searches can be interrupted. An estimation of search length is given as expected number of generated key pairs. (A future improvement would be to observe keys per second performance and give time estimates based on that.) When it generates a new private key on your behalf, it can tell you what the corresponding public key will be. Unfortunately, if you paste one in from another source, it can't compute the corresponding public key. This is because the standard Dart crypto library doesn't provide that capability, nor does it give access to the elliptic curve functions it implements so we could do it ourselves. Our only recourse would be to copy the source for all those things, and I didn't think it was worth it. This will not check the validity of a pasted key, but the remote repeater will. It does limit it to the correct length and hexadecimal charset. Prefixes are limited to 4 characters. There's no technical reason for this, but users are unlikely to want more, and I think it would just be a bad experience to allow more and spin for a very long time. If someone really wants a longer one, they can use an external generator like they do now. Possible TODO: rewrite background processing using isolates. Closes #142. --- lib/l10n/app_en.arb | 22 +- lib/l10n/app_localizations.dart | 62 +++++- lib/l10n/app_localizations_bg.dart | 35 +++ lib/l10n/app_localizations_de.dart | 35 +++ lib/l10n/app_localizations_en.dart | 37 +++- lib/l10n/app_localizations_es.dart | 35 +++ lib/l10n/app_localizations_fr.dart | 35 +++ lib/l10n/app_localizations_hu.dart | 35 +++ lib/l10n/app_localizations_it.dart | 35 +++ lib/l10n/app_localizations_ja.dart | 35 +++ lib/l10n/app_localizations_ko.dart | 35 +++ lib/l10n/app_localizations_nl.dart | 35 +++ lib/l10n/app_localizations_pl.dart | 35 +++ lib/l10n/app_localizations_pt.dart | 35 +++ lib/l10n/app_localizations_ru.dart | 35 +++ lib/l10n/app_localizations_sk.dart | 35 +++ lib/l10n/app_localizations_sl.dart | 35 +++ lib/l10n/app_localizations_sv.dart | 35 +++ lib/l10n/app_localizations_uk.dart | 35 +++ lib/l10n/app_localizations_zh.dart | 35 +++ lib/models/community.dart | 15 +- lib/screens/channels_screen.dart | 14 +- lib/screens/repeater_settings_screen.dart | 170 ++++++++++++++- lib/utils/keys.dart | 87 ++++++++ linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.yaml | 3 + untranslated.json | 204 ++++++++++++++++-- windows/flutter/generated_plugins.cmake | 1 + 29 files changed, 1169 insertions(+), 44 deletions(-) create mode 100644 lib/utils/keys.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index bcbe3005..ef174827 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1409,6 +1409,26 @@ "repeater_advancedSettingsSubtitle": "Tuning knobs for experienced operators", "repeater_pathHashMode": "Path hash mode", "repeater_pathHashModeHelper": "Bytes used to encode this repeater's ID in flood path/loop-detect tags. 0=1 byte (256 IDs, up to 64 hops), 1=2 bytes (65K IDs, up to 32 hops), 2=3 bytes (16M IDs, up to 21 hops). v1.13 and older firmware drops multi-byte paths — only raise once your network is on v1.14+.", + "repeater_keySettings": "Change Identity Keys", + "repeater_keySettingsSubtitle": "Change the public/private keypair", + "repeater_prvKey": "Private key", + "repeater_prvKeyHelper": "A new private key for the repeater, a 128-character hex string.", + "repeater_generatePrvKey": "Generate a random keypair", + "repeater_stopGeneratingPrvKey": "Interrupt search for keypair", + "repeater_pubKey": "Public key", + "repeater_pubKeyHelper": "This is the public key that goes with the generated private key. You can't set this directly.", + "repeater_pubKeyPrefix": "Desired prefix", + "repeater_pubKeyPrefixHelper": "Find a public key that starts with these hex digits. Expected tries needed: {tries}.", + "@repeater_pubKeyPrefixHelper": { + "placeholders": { + "tries": { + "type": "int" + } + } + }, + + + "repeater_txDelay": "Flood TX delay", "repeater_txDelayHelper": "Retransmit spacing for flood traffic, as a multiplier of the packet's airtime (0-2, default 0.5). Higher = fewer collisions but slower delivery.", "repeater_directTxDelay": "Direct TX delay", @@ -1589,7 +1609,7 @@ "repeater_cliHelpPowersavingOnOff": "Enables or disables powersaving mode (where supported).", "repeater_cliHelpErase": "(Serial only) Formats the device file system. Wipes all settings and contacts.", "repeater_cliHelpSetDutyCycle": "Sets the maximum allowed transmit duty cycle as a percentage (1-100). Internally adjusts the airtime factor.", - "repeater_cliHelpSetPrvKey": "(Serial only) Replaces the device identity private key. Reboot required to apply. Generates a new public key.", + "repeater_cliHelpSetPrvKey": "Replaces the device identity private key. Reboot required to apply. Generates a new public key.", "repeater_cliHelpSetRadioRxGain": "(SX126x only) Toggles boosted RX gain for improved sensitivity at higher current draw.", "repeater_cliHelpSetOwnerInfo": "Sets the owner contact info string included in adverts. Use '|' for newlines.", "repeater_cliHelpSetPathHashMode": "Sets the path-hash mode. 0 = legacy, 1 = standard, 2 = strict. Affects how routing paths are matched.", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 08d7a0b6..92bf8c66 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4748,6 +4748,66 @@ abstract class AppLocalizations { /// **'Bytes used to encode this repeater\'s ID in flood path/loop-detect tags. 0=1 byte (256 IDs, up to 64 hops), 1=2 bytes (65K IDs, up to 32 hops), 2=3 bytes (16M IDs, up to 21 hops). v1.13 and older firmware drops multi-byte paths — only raise once your network is on v1.14+.'** String get repeater_pathHashModeHelper; + /// No description provided for @repeater_keySettings. + /// + /// In en, this message translates to: + /// **'Change Identity Keys'** + String get repeater_keySettings; + + /// No description provided for @repeater_keySettingsSubtitle. + /// + /// In en, this message translates to: + /// **'Change the public/private keypair'** + String get repeater_keySettingsSubtitle; + + /// No description provided for @repeater_prvKey. + /// + /// In en, this message translates to: + /// **'Private key'** + String get repeater_prvKey; + + /// No description provided for @repeater_prvKeyHelper. + /// + /// In en, this message translates to: + /// **'A new private key for the repeater, a 128-character hex string.'** + String get repeater_prvKeyHelper; + + /// No description provided for @repeater_generatePrvKey. + /// + /// In en, this message translates to: + /// **'Generate a random keypair'** + String get repeater_generatePrvKey; + + /// No description provided for @repeater_stopGeneratingPrvKey. + /// + /// In en, this message translates to: + /// **'Interrupt search for keypair'** + String get repeater_stopGeneratingPrvKey; + + /// No description provided for @repeater_pubKey. + /// + /// In en, this message translates to: + /// **'Public key'** + String get repeater_pubKey; + + /// No description provided for @repeater_pubKeyHelper. + /// + /// In en, this message translates to: + /// **'This is the public key that goes with the generated private key. You can\'t set this directly.'** + String get repeater_pubKeyHelper; + + /// No description provided for @repeater_pubKeyPrefix. + /// + /// In en, this message translates to: + /// **'Desired prefix'** + String get repeater_pubKeyPrefix; + + /// No description provided for @repeater_pubKeyPrefixHelper. + /// + /// In en, this message translates to: + /// **'Find a public key that starts with these hex digits. Expected tries needed: {tries}.'** + String repeater_pubKeyPrefixHelper(int tries); + /// No description provided for @repeater_txDelay. /// /// In en, this message translates to: @@ -5519,7 +5579,7 @@ abstract class AppLocalizations { /// No description provided for @repeater_cliHelpSetPrvKey. /// /// In en, this message translates to: - /// **'(Serial only) Replaces the device identity private key. Reboot required to apply. Generates a new public key.'** + /// **'Replaces the device identity private key. Reboot required to apply. Generates a new public key.'** String get repeater_cliHelpSetPrvKey; /// No description provided for @repeater_cliHelpSetRadioRxGain. diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 1404277c..e678b89f 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2641,6 +2641,41 @@ class AppLocalizationsBg extends AppLocalizations { @override String get repeater_txDelay => 'Забавяне на Flood TX'; + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + + @override @override String get repeater_txDelayHelper => diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 9e061241..c897d64e 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2635,6 +2635,41 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_pathHashModeHelper => 'Bytes, die zur Kodierung der ID dieses Repeaters in Flood-Pfad-/Schleifen-Erkennung-Tags verwendet werden. 0 = 1 Byte (256 IDs, bis zu 64 Hops), 1 = 2 Bytes (65.000 IDs, bis zu 32 Hops), 2 = 3 Bytes (16 Millionen IDs, bis zu 21 Hops). Firmware-Versionen 1.13 und älter verwenden mehrstellige Pfade – ab Version 1.14+ wird nur ein Pfad erstellt, sobald das Netzwerk aktiv ist.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Flood-TX-Verzögerung'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 08f45442..db221c95 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2589,6 +2589,41 @@ class AppLocalizationsEn extends AppLocalizations { String get repeater_pathHashModeHelper => 'Bytes used to encode this repeater\'s ID in flood path/loop-detect tags. 0=1 byte (256 IDs, up to 64 hops), 1=2 bytes (65K IDs, up to 32 hops), 2=3 bytes (16M IDs, up to 21 hops). v1.13 and older firmware drops multi-byte paths — only raise once your network is on v1.14+.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Flood TX delay'; @@ -3059,7 +3094,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get repeater_cliHelpSetPrvKey => - '(Serial only) Replaces the device identity private key. Reboot required to apply. Generates a new public key.'; + 'Replaces the device identity private key. Reboot required to apply. Generates a new public key.'; @override String get repeater_cliHelpSetRadioRxGain => diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index da9decb1..fd7851aa 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2634,6 +2634,41 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_pathHashModeHelper => 'Bytes utilizados para codificar el ID de este repetidor en las etiquetas de ruta/detección de bucles. 0=1 byte (256 IDs, hasta 64 saltos), 1=2 bytes (65.000 IDs, hasta 32 saltos), 2=3 bytes (16 millones de IDs, hasta 21 saltos). Las versiones 1.13 y anteriores de firmware eliminan rutas de múltiples bytes; solo se detectan una vez que la red está activa en la versión 1.14 o posterior.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Retraso TX por inundación'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 2f9205cf..3094ecdd 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2645,6 +2645,41 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_pathHashModeHelper => 'Octets utilisés pour encoder l\'ID de ce routeur dans les balises de détection de flux/boucles. 0 = 1 octet (256 ID, jusqu\'à 64 sauts), 1 = 2 octets (65 000 ID, jusqu\'à 32 sauts), 2 = 3 octets (16 millions d\'ID, jusqu\'à 21 sauts). Les versions 1.13 et antérieures utilisent des chemins multi-octets ; à partir de la version 1.14, cela n\'est plus nécessaire.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Délai de transmission en flood'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index d64268ed..20681a99 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -2628,6 +2628,41 @@ class AppLocalizationsHu extends AppLocalizations { String get repeater_pathHashModeHelper => 'Az átjátszó azonosítójának elárasztási útvonal/hurokészlelési címkékbe való kódolására használt bájtok. 0 = 1 bájt (256 azonosító, legfeljebb 64 ugrás), 1 = 2 bájt (65 000 azonosító, legfeljebb 32 ugrás), 2 = 3 bájt (16 millió azonosító, legfeljebb 21 ugrás). A v1.13 és régebbi firmware eldobja a többbájtos elérési utat – csak akkor emelje meg, ha a hálózat a v1.14+ verziót használja.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Áradási TX késleltetés'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 350e7b78..5bf4aae5 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2636,6 +2636,41 @@ class AppLocalizationsIt extends AppLocalizations { String get repeater_pathHashModeHelper => 'Byte utilizzati per codificare l\'ID di questo ripetitore nei tag per il rilevamento del percorso/loop. 0=1 byte (256 ID, fino a 64 salti), 1=2 byte (65.000 ID, fino a 32 salti), 2=3 byte (16 milioni di ID, fino a 21 salti). Le versioni 1.13 e precedenti utilizzano percorsi multi-byte: è necessario attivare la rete prima di utilizzare questa funzionalità (a partire dalla versione 1.14).'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Ritardo a Flood, TX'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 53851ba3..4a576363 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -2519,6 +2519,41 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_pathHashModeHelper => 'このリピータのIDをフローパス/ループ検出タグにエンコードするために使用されるバイト数。 0=1バイト (256個のID、最大64ホップ)、1=2バイト (65,000個のID、最大32ホップ)、2=3バイト (160万個のID、最大21ホップ)。 v1.13およびそれ以前のファームウェアでは、マルチバイトパスがサポートされていません。 v1.14以降のバージョンでは、一度ネットワークが起動されると、パスが一度だけ検出されます。'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'フロイド・TXでの遅延'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 3ddff59a..e20e048b 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -2524,6 +2524,41 @@ class AppLocalizationsKo extends AppLocalizations { String get repeater_pathHashModeHelper => '이 리피터의 ID를 플러드 경로/루프 감지 태그에 인코딩하는 데 사용되는 바이트 수: 0=1 바이트 (256개의 ID, 최대 64개의 홉), 1=2 바이트 (65,000개의 ID, 최대 32개의 홉), 2=3 바이트 (16백만 개의 ID, 최대 21개의 홉). v1.13 및 이전 버전의 펌웨어는 다중 바이트 경로를 지원하지 않으며, 네트워크가 v1.14 이상으로 업그레이드되면 한 번만 감지합니다.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => '플러드 TX 지연'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index ade730a2..0e30d205 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2617,6 +2617,41 @@ class AppLocalizationsNl extends AppLocalizations { String get repeater_pathHashModeHelper => 'Bytes die gebruikt worden om de ID van deze repeater te coderen in flood-pad/lusdetectietags. 0=1 byte (256 ID\'s, tot 64 hops), 1=2 bytes (65.000 ID\'s, tot 32 hops), 2=3 bytes (16 miljoen ID\'s, tot 21 hops). Versies 1.13 en ouder gebruiken multi-byte paden – alleen na het activeren van het netwerk.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Vertraging bij Flood TX'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 627b332b..8a80e003 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2650,6 +2650,41 @@ class AppLocalizationsPl extends AppLocalizations { String get repeater_pathHashModeHelper => 'Bity wykorzystywane do kodowania identyfikatora tego urządzenia w tagach ścieżek/detekcji pętli. 0=1 bity (256 identyfikatorów, do 64 skoków), 1=2 bity (65 000 identyfikatorów, do 32 skoków), 2=3 bity (1 600 000 identyfikatorów, do 21 skoków). Wersje 1.13 i wcześniejsze nie obsługują ścieżek wielobitowych – wykrywają tylko jedną, gdy sieć jest w wersji 1.14 lub nowszej.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Opóźnienie w Flood, TX'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 0c9f67e2..d19939bc 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2635,6 +2635,41 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_pathHashModeHelper => 'Bytes utilizados para codificar o ID deste repetidor nas tags de caminho/detecção de loop. 0=1 byte (256 IDs, até 64 saltos), 1=2 bytes (65.000 IDs, até 32 saltos), 2=3 bytes (16 milhões de IDs, até 21 saltos). As versões 1.13 e anteriores do firmware não suportam caminhos multi-byte — apenas funcionam uma vez após a ativação da rede (a partir da versão 1.14+).'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Atraso na entrega em Flood, TX'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 1ddc9c79..2d78dd70 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2640,6 +2640,41 @@ class AppLocalizationsRu extends AppLocalizations { String get repeater_pathHashModeHelper => 'Байты, используемые для кодирования идентификатора этого ретранслятора в тегах для обнаружения потоков/циклов. 0 = 1 байт (256 идентификаторов, до 64 переходов), 1 = 2 байта (65 000 идентификаторов, до 32 переходов), 2 = 3 байта (1 600 000 идентификаторов, до 21 перехода). Версии прошивки v1.13 и более ранние версии не поддерживают многобайтовые пути — они поднимаются только после того, как ваша сеть будет обновлена до версии v1.14 и выше.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Задержка в работе системы Flood TX'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index bd16dc0f..4c32ade5 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2621,6 +2621,41 @@ class AppLocalizationsSk extends AppLocalizations { String get repeater_pathHashModeHelper => 'Byty použité na zakódovanie ID tohto opakovača v tagoch pre trasu/detekciu slučky. 0 = 1 bytu (256 ID, až 64 skokov), 1 = 2 byty (65 000 ID, až 32 skokov), 2 = 3 byty (16 miliónov ID, až 21 skokov). Verzie 1.13 a staršie nepodporujú viacbytové trasy – fungujú len, keď je sieť aktivovaná.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Zpoždanie v Flood, TX'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 064391b7..c248fea9 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -2617,6 +2617,41 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_pathHashModeHelper => 'Biti, ki so bila uporabljena za kodiranje ID-ja tega releja v oznakah za zaznavanje pot/kroga, imajo naslednje velikosti: 0=1 bit (256 ID-jev, do 64 skokov), 1=2 biti (65.000 ID-jev, do 32 skokov), 2=3 biti (16 milijonov ID-jev, do 21 skokov). V različicah 1.13 in starejših se ustvarjajo večbitne poti – vendar se to zgodi šele, ko je omrežje vklopljeno v različicah 1.14 in kasnejših.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Zatemnitevanje zaradi poplav v Texasu'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 1d1ec527..faee2719 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -2605,6 +2605,41 @@ class AppLocalizationsSv extends AppLocalizations { String get repeater_pathHashModeHelper => 'Byte används för att koda denna repeaters ID i taggar för att upptäcka loopar/flödesvägar. 0=1 byte (256 ID:n, upp till 64 hopp), 1=2 byte (65 000 ID:n, upp till 32 hopp), 2=3 byte (16 miljoner ID:n, upp till 21 hopp). Versioner 1.13 och äldre har stöd för multi-byte-vägar – endast en gång när nätverket är aktiverat (från och med version 1.14).'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Försening i Flood TX'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index ab02382b..88619ead 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -2637,6 +2637,41 @@ class AppLocalizationsUk extends AppLocalizations { String get repeater_pathHashModeHelper => 'Байти, що використовуються для кодування ідентифікатора цього ретранслятора в тегах для виявлення потоків/петлі. 0=1 байт (256 ідентифікаторів, до 64 перехідів), 1=2 байти (65 000 ідентифікаторів, до 32 перехідів), 2=3 байти (16 мільйонів ідентифікаторів, до 21 переходу). Версії 1.13 та старіші не підтримують багатобайтні шляхи — вони активуються лише після того, як мережа буде оновлена до версії 1.14+.'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => 'Затримка у Flood, штат Техас'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 6e0704bb..8dffde15 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2494,6 +2494,41 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_pathHashModeHelper => '用于编码此复用器的 ID 的字节数,在“洪流路径/环检测”标签中使用。 0=1 字节(256 个 ID,最多 64 个跳跃),1=2 字节(65K 个 ID,最多 32 个跳跃),2=3 字节(16M 个 ID,最多 21 个跳跃)。 v1.13 及更早版本的固件会使用多字节路径——只有在您的网络升级到 v1.14 或更高版本后才会生效。'; + @override + String get repeater_keySettings => 'Change Identity Keys'; + + @override + String get repeater_keySettingsSubtitle => + 'Change the public/private keypair'; + + @override + String get repeater_prvKey => 'Private key'; + + @override + String get repeater_prvKeyHelper => + 'A new private key for the repeater, a 128-character hex string.'; + + @override + String get repeater_generatePrvKey => 'Generate a random keypair'; + + @override + String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair'; + + @override + String get repeater_pubKey => 'Public key'; + + @override + String get repeater_pubKeyHelper => + 'This is the public key that goes with the generated private key. You can\'t set this directly.'; + + @override + String get repeater_pubKeyPrefix => 'Desired prefix'; + + @override + String repeater_pubKeyPrefixHelper(int tries) { + return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.'; + } + @override String get repeater_txDelay => '洪水(德克萨斯州)延误'; diff --git a/lib/models/community.dart b/lib/models/community.dart index 7261ddf9..c0abf7f0 100644 --- a/lib/models/community.dart +++ b/lib/models/community.dart @@ -1,8 +1,8 @@ import 'dart:convert'; -import 'dart:math'; import 'dart:typed_data'; import 'package:crypto/crypto.dart' as crypto; +import 'package:meshcore_open/utils/keys.dart'; import 'channel.dart'; @@ -37,11 +37,7 @@ class Community { /// Generate a new community with a random 32-byte secret factory Community.create({required String id, required String name}) { - final random = Random.secure(); - final secret = Uint8List(32); - for (int i = 0; i < 32; i++) { - secret[i] = random.nextInt(256); - } + final secret = randomBytes(32); return Community( id: id, name: name, @@ -210,12 +206,7 @@ class Community { /// Create a copy of this community with a regenerated random secret Community withRegeneratedSecret() { - final random = Random.secure(); - final newSecret = Uint8List(32); - for (int i = 0; i < 32; i++) { - newSecret[i] = random.nextInt(256); - } - return withNewSecret(newSecret); + return withNewSecret(randomBytes(32)); } /// Extract secret from QR data (for updating existing community) diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 2de232c6..e6bd0885 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:meshcore_open/storage/channel_message_store.dart'; +import 'package:meshcore_open/utils/keys.dart'; import 'package:meshcore_open/utils/platform_info.dart'; import 'package:meshcore_open/widgets/app_bar.dart'; import 'package:provider/provider.dart'; @@ -938,11 +938,7 @@ class _ChannelsScreenState extends State ); return; } - final random = Random.secure(); - final psk = Uint8List(16); - for (int i = 0; i < 16; i++) { - psk[i] = random.nextInt(256); - } + final psk = randomBytes(16); Navigator.pop(sheetContext); await connector.setChannel( nextIndex, @@ -1571,11 +1567,7 @@ class _ChannelsScreenState extends State icon: const Icon(Icons.casino), tooltip: sheetContext.l10n.channels_generateRandomPsk, onPressed: () { - final random = Random.secure(); - final bytes = Uint8List(16); - for (int i = 0; i < 16; i++) { - bytes[i] = random.nextInt(256); - } + final bytes = randomBytes(16); pskController.text = Channel.formatPskHex(bytes); }, ), diff --git a/lib/screens/repeater_settings_screen.dart b/lib/screens/repeater_settings_screen.dart index dca0d7f4..fb95d0ea 100644 --- a/lib/screens/repeater_settings_screen.dart +++ b/lib/screens/repeater_settings_screen.dart @@ -1,6 +1,7 @@ import 'dart:async'; -import 'dart:typed_data'; +import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../l10n/l10n.dart'; import '../models/contact.dart'; @@ -11,6 +12,7 @@ import '../services/storage_service.dart'; import '../theme/mesh_theme.dart'; import '../widgets/mesh_ui.dart'; import '../widgets/routing_sheet.dart'; +import '../utils/keys.dart'; import '../helpers/snack_bar_builder.dart'; class RepeaterSettingsScreen extends StatefulWidget { @@ -44,6 +46,7 @@ enum _SettingField { advertInterval, floodAdvertInterval, pathHashMode, + prvKey, txDelay, directTxDelay, intThresh, @@ -109,6 +112,8 @@ class _RepeaterSettingsScreenState extends State { bool _refreshingIntThresh = false; bool _refreshingAgcResetInterval = false; bool _runningAction = false; + bool _searchingForKeyPair = false; + bool _stopSearchingForKeyPair = false; StreamSubscription? _frameSubscription; RepeaterCommandService? _commandService; @@ -154,6 +159,11 @@ class _RepeaterSettingsScreenState extends State { // Advanced int _pathHashMode = 0; // 0-2 + final TextEditingController _prvKeyController = TextEditingController(); + final TextEditingController _pubKeyController = TextEditingController(); + final TextEditingController _prvKeyPrefixController = TextEditingController(); + final KeyPairSearcher _keyPairSearcher = KeyPairSearcher(); + final int _searchChunkTime = 30; // time in ms to search before yielding final TextEditingController _txDelayController = TextEditingController(); final TextEditingController _directTxDelayController = TextEditingController(); @@ -860,6 +870,12 @@ class _RepeaterSettingsScreenState extends State { command: 'set path.hash.mode $_pathHashMode', )); } + if (_dirtyFields.contains(_SettingField.prvKey)) { + final v = _prvKeyController.text.trim(); + if (v != "") { + pending.add((field: _SettingField.prvKey, command: 'set prv.key $v')); + } + } if (_dirtyFields.contains(_SettingField.txDelay) && _txDelayController.text.isNotEmpty) { final v = double.tryParse(_txDelayController.text.trim()); @@ -1005,6 +1021,36 @@ class _RepeaterSettingsScreenState extends State { } } + void _startSearchingForKeyPair() async { + if (_searchingForKeyPair) return; + setState(() { + _searchingForKeyPair = true; + _stopSearchingForKeyPair = false; + }); + _searchForKeyPair(); + } + + void _searchForKeyPair() async { + if (_stopSearchingForKeyPair) { + setState(() => _searchingForKeyPair = false); + return; + } + final String prefix = _prvKeyPrefixController.text.trim(); + MCKeyPair? keys = await _keyPairSearcher.findMatchingKeyPair( + prefix, + _searchChunkTime, + ); + if (keys == null) { + // Ran out of time; schedule another attempt. + Timer.run(_searchForKeyPair); + } else { + setState(() => _searchingForKeyPair = false); + _prvKeyController.text = pubKeyToHex(keys.private); + _pubKeyController.text = pubKeyToHex(keys.public); + _markChanged(_SettingField.prvKey); + } + } + Widget _buildInlineRefreshButton({ required bool isRefreshing, required VoidCallback onRefresh, @@ -1069,6 +1115,7 @@ class _RepeaterSettingsScreenState extends State { _buildOwnerInfoCard(), _buildActionsCard(), _buildAdvancedCard(), + _buildKeysCard(), const SizedBox(height: 16), _buildDangerZoneCard(), ], @@ -1984,6 +2031,127 @@ class _RepeaterSettingsScreenState extends State { ); } + Widget _buildKeysCard() { + final l10n = context.l10n; + return MeshCard( + child: ExpansionTile( + leading: const Icon(Icons.vpn_key), + title: Text( + l10n.repeater_keySettings, + style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600), + ), + subtitle: Text(l10n.repeater_keySettingsSubtitle), + childrenPadding: const EdgeInsets.fromLTRB(0, 8, 0, 4), + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: TextField( + controller: _prvKeyController, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp("[0-9a-fA-F]")), + ], + decoration: InputDecoration( + labelText: l10n.repeater_prvKey, + helperText: l10n.repeater_prvKeyHelper, + helperMaxLines: 3, + border: const OutlineInputBorder(), + ), + onChanged: (_) { + _pubKeyController.text = ""; + if (_prvKeyController.text.isNotEmpty) { + _markChanged(_SettingField.prvKey); + } + }, + ), + ), + const SizedBox(width: 8), + Padding( + padding: const EdgeInsets.only(top: 8), + child: _searchingForKeyPair + ? IconButton( + icon: const Icon(Icons.cancel, size: 24), + onPressed: (() => _stopSearchingForKeyPair = true), + tooltip: l10n.repeater_stopGeneratingPrvKey, + visualDensity: VisualDensity.compact, + ) + : IconButton( + icon: const Icon(Icons.casino_rounded, size: 24), + onPressed: () { + _startSearchingForKeyPair(); + }, + tooltip: l10n.repeater_generatePrvKey, + visualDensity: VisualDensity.compact, + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: TextField( + readOnly: true, + controller: _pubKeyController, + style: TextStyle( + color: Theme.of( + context, + ).textTheme.headlineSmall?.color?.withValues(alpha: 0.6), + ), + decoration: InputDecoration( + labelText: l10n.repeater_pubKey, + helperText: l10n.repeater_pubKeyHelper, + helperMaxLines: 3, + border: const OutlineInputBorder(), + ), + ), + ), + const SizedBox(width: 16), + Padding( + padding: const EdgeInsets.only(top: 12), + child: SizedBox( + width: 24, + height: 24, + child: _searchingForKeyPair + ? CircularProgressIndicator(strokeWidth: 2) + : null, + ), + ), + const SizedBox(width: 8), + ], + ), + const SizedBox(height: 16), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: TextField( + controller: _prvKeyPrefixController, + maxLength: 4, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp("[0-9a-fA-F]")), + ], + onChanged: (_) => setState(() => {}), // updates helper text + decoration: InputDecoration( + labelText: l10n.repeater_pubKeyPrefix, + helperText: l10n.repeater_pubKeyPrefixHelper( + pow(16, _prvKeyPrefixController.text.length).toInt(), + ), + helperMaxLines: 3, + border: const OutlineInputBorder(), + ), + ), + ), + const SizedBox(width: 48), + ], + ), + ], + ), + ); + } + Widget _buildDangerZoneCard() { final l10n = context.l10n; return MeshCard( diff --git a/lib/utils/keys.dart b/lib/utils/keys.dart new file mode 100644 index 00000000..a89a5d6c --- /dev/null +++ b/lib/utils/keys.dart @@ -0,0 +1,87 @@ +import 'dart:async'; +import 'dart:math'; +import 'dart:typed_data'; +import 'package:convert/convert.dart'; +import 'package:cryptography/cryptography.dart'; + +Uint8List randomBytes(int length) { + final random = Random.secure(); + final bytes = Uint8List(32); + for (int i = 0; i < length; i++) { + bytes[i] = random.nextInt(256); + } + return bytes; +} + +class MCKeyPair { + final Uint8List public; + final Uint8List private; + MCKeyPair(this.public, this.private); +} + +Future generateKeyPair() async { + final algo = Ed25519(); + final seed = randomBytes(32); + final SimpleKeyPair keys = await algo.newKeyPairFromSeed(seed); + final Uint8List pubKeyBytes = Uint8List.fromList( + (await keys.extractPublicKey()).bytes, + ); + // Ed25519 gives 32-byte public and private keys, but MeshCore + // uses an expanded 64-byte private key, so we have to make it + // ourselves. Luckily, we can do it such that it has the same public + // key, which we'd otherwise be unable to compute, since although the + // crypto library implements the elliptic curve computations we need, + // it doesn't make them available. + final Uint8List hash = Uint8List.fromList((await Sha512().hash(seed)).bytes); + hash[0] &= 248; // Clamp the scalar. This keeps the chosen point in the + hash[31] &= 63; // large elliptic curve subgroup, and is demanded both by + hash[31] |= 64; // Ed25519 and by the MeshCore repeater key validation code. + return MCKeyPair(pubKeyBytes, hash); +} + +class _KeyHashPrefix { + Uint8List _bytes = Uint8List(0); + Uint8List _mask = Uint8List(0); + _KeyHashPrefix(String prefix) { + final bool lengthIsOdd = (prefix.length % 2 == 1) ? true : false; + final String decodableString = lengthIsOdd ? "${prefix}0" : prefix; + _bytes = hex.decoder.convert(decodableString); + _mask = Uint8List(_bytes.length); + for (var i = 0; i < _mask.length; i++) { + _mask[i] = 0xff; + } + if (lengthIsOdd) { + _mask[_mask.length - 1] = 0xf0; + } + } + + bool matches(List keyBytes) { + if (keyBytes.length < _bytes.length) { + return false; + } + for (var i = 0; i < _bytes.length; i++) { + if (keyBytes[i] & _mask[i] != _bytes[i]) { + return false; + } + } + return true; + } +} + +class KeyPairSearcher { + Future findMatchingKeyPair( + String prefixStr, + int maxMilliseconds, + ) async { + final int now = DateTime.now().millisecondsSinceEpoch; + final int endTime = now + maxMilliseconds; + final _KeyHashPrefix keyHashPrefix = _KeyHashPrefix(prefixStr); + while (DateTime.now().millisecondsSinceEpoch < endTime) { + final MCKeyPair keys = await generateKeyPair(); + if (keyHashPrefix.matches(keys.public)) { + return keys; + } + } + return null; + } +} diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 379e36fa..93e46829 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flserial + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index ffc8c590..3ba5e9d1 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import cryptography_flutter import flutter_blue_plus_darwin import flutter_local_notifications import mobile_scanner @@ -15,6 +16,7 @@ import sqflite_darwin import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + CryptographyFlutterPlugin.register(with: registry.registrar(forPlugin: "CryptographyFlutterPlugin")) FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) diff --git a/pubspec.yaml b/pubspec.yaml index dcefc016..b9864106 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -73,6 +73,9 @@ dependencies: ml_dataframe: ^1.0.0 llamadart: ^0.8.0 flutter_langdetect: ^0.0.1 + cryptography: ^2.9.0 + cryptography_flutter: ^2.3.4 + convert: ^3.1.2 hooks: user_defines: diff --git a/untranslated.json b/untranslated.json index f42fd2c3..e63eed61 100644 --- a/untranslated.json +++ b/untranslated.json @@ -15,13 +15,33 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "de": [ "settings_regionFetchRegions", "settings_regionFetchRegionsFail", - "settings_regionFetchRegionsAlreadyExists" + "settings_regionFetchRegionsAlreadyExists", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "es": [ @@ -40,7 +60,17 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "fr": [ @@ -59,7 +89,17 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "hu": [ @@ -78,7 +118,17 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "it": [ @@ -97,7 +147,17 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "ja": [ @@ -116,7 +176,17 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "ko": [ @@ -135,7 +205,17 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "nl": [ @@ -154,7 +234,17 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "pl": [ @@ -173,7 +263,17 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "pt": [ @@ -192,7 +292,17 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "ru": [ @@ -211,7 +321,17 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "sk": [ @@ -230,7 +350,17 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "sl": [ @@ -249,7 +379,17 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "sv": [ @@ -268,7 +408,17 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "uk": [ @@ -287,7 +437,17 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ], "zh": [ @@ -306,6 +466,16 @@ "channels_regionSetTo", "channels_regionNotSet", "channels_regionSelect_Title", - "channels_clearRegion" + "channels_clearRegion", + "repeater_keySettings", + "repeater_keySettingsSubtitle", + "repeater_prvKey", + "repeater_prvKeyHelper", + "repeater_generatePrvKey", + "repeater_stopGeneratingPrvKey", + "repeater_pubKey", + "repeater_pubKeyHelper", + "repeater_pubKeyPrefix", + "repeater_pubKeyPrefixHelper" ] } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index f02857f4..533a1712 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flserial flutter_local_notifications_windows + jni ) set(PLUGIN_BUNDLED_LIBRARIES)