Šepot — end-to-end encrypted messenger

Initial public release. Anonymous on-device identity (no phone number,
e-mail, or password), Signal-protocol cryptography (X3DH + PQXDH, Double
Ratchet, sealed sender) via libsignal, a metadata-minimizing relay,
group chats, disappearing messages, encrypted local storage, optional
Tor transport, and a UI localized into Czech, English, German, Russian,
and Italian.

Monorepo: Flutter client (app/), Rust relay + wire types (rust/),
protobuf definitions (proto/), self-host deploy (deploy/).

Licensed under AGPL-3.0 with an app-store distribution exception (§7).
This commit is contained in:
Julius Joska
2026-07-11 17:06:39 +02:00
commit 8081d98c67
309 changed files with 44102 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
/// Obnova identity přes mnemonic — BIP39 wordlist jako REVERZIBILNÍ
/// ENKÓDOVÁNÍ 32B privátního identity klíče.
///
/// Žádné KDF, žádný seed: 24 slov je přímý zápis bajtů klíče + checksum.
/// `decode(encode(klic)) == klic` platí bit po bitu — hlídají to KAT testy.
///
/// DŮLEŽITÉ: enkóduje se SERIALIZOVANÝ privátní klíč (výstup fasády
/// SepotCrypto, už clampnutý pro Curve25519). Díky tomu je
/// deserialize→serialize stabilní a mnemonic vždy obnoví identický klíč.
library;
import 'dart:typed_data';
import 'package:bip39/bip39.dart' as bip39;
import 'package:convert/convert.dart' show hex;
import 'package:sepot/core/errors.dart';
class SepotMnemonic {
SepotMnemonic._();
/// 32 B entropie = 24 slov BIP39 (256 bitů + 8bit checksum).
static const int wordCount = 24;
static const int keyLength = 32;
/// Serializovaný privátní klíč (32 B) → 24 slov.
static List<String> encode(Uint8List privateKey) {
if (privateKey.length != keyLength) {
throw ArgumentError.value(
privateKey.length,
'privateKey',
'Expected $keyLength bytes of serialized private key',
);
}
return bip39.entropyToMnemonic(hex.encode(privateKey)).split(' ');
}
/// 24 slov → 32 B privátního klíče.
///
/// Vyhodí [MnemonicWordCountError] při špatném počtu slov a
/// [MnemonicInvalidError] při neznámém slovu nebo nesedícím checksumu
/// (překlep uživatele) — UI je překládá přes describeError.
static Uint8List decode(List<String> words) {
final normalized = _normalize(words);
if (normalized.length != wordCount) {
throw MnemonicWordCountError(normalized.length, wordCount);
}
final String entropyHex;
try {
entropyHex = bip39.mnemonicToEntropy(normalized.join(' '));
} on ArgumentError catch (e) {
// bip39 hlásí neznámé slovo / špatný checksum přes ArgumentError a
// StateError — pro UI sjednocujeme na typovanou chybu
throw MnemonicInvalidError('${e.message}');
} on StateError catch (e) {
throw MnemonicInvalidError(e.message);
}
return Uint8List.fromList(hex.decode(entropyHex));
}
/// Rychlá validace pro UI (živá kontrola při zadávání).
static bool isValid(List<String> words) {
final normalized = _normalize(words);
if (normalized.length != wordCount) return false;
return bip39.validateMnemonic(normalized.join(' '));
}
static List<String> _normalize(List<String> words) => words
.expand((w) => w.trim().toLowerCase().split(RegExp(r'\s+')))
.where((w) => w.isNotEmpty)
.toList();
}