Š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
+74
View File
@@ -0,0 +1,74 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:sepot/core/crypto/safety_number.dart';
import 'package:sepot/core/crypto/sepot_crypto.dart';
void main() {
setUpAll(() async {
await SepotCrypto.init();
});
Uint8List fixedKey(int seed) {
// deterministický „privátní klíč" → veřejný přes libsignal derivaci
final priv = Uint8List.fromList(List.generate(32, (i) => (i * 7 + seed) & 0xFF));
return SepotCrypto.derivePublicKey(priv);
}
test('obě strany vidí stejné číslo (symetrie)', () {
final a = fixedKey(1);
final b = fixedKey(2);
final fromA = SafetyNumber.compute(localIdentityKey: a, remoteIdentityKey: b);
final fromB = SafetyNumber.compute(localIdentityKey: b, remoteIdentityKey: a);
expect(fromA.display, fromB.display);
});
test('60 číslic, 12 bloků po 5', () {
final n = SafetyNumber.compute(
localIdentityKey: fixedKey(1),
remoteIdentityKey: fixedKey(2),
);
expect(n.display, matches(RegExp(r'^\d{60}$')));
expect(n.groups, hasLength(12));
expect(n.groups.every((g) => g.length == 5), isTrue);
});
test('jiný pár klíčů = jiné číslo', () {
final ab = SafetyNumber.compute(
localIdentityKey: fixedKey(1), remoteIdentityKey: fixedKey(2));
final ac = SafetyNumber.compute(
localIdentityKey: fixedKey(1), remoteIdentityKey: fixedKey(3));
expect(ab.display, isNot(ac.display));
});
test('QR compare: protistrany si odpovídají, cizí ne', () {
final a = fixedKey(1);
final b = fixedKey(2);
final mine = SafetyNumber.compute(localIdentityKey: a, remoteIdentityKey: b);
final theirs = SafetyNumber.compute(localIdentityKey: b, remoteIdentityKey: a);
final stranger = SafetyNumber.compute(
localIdentityKey: a, remoteIdentityKey: fixedKey(9));
expect(SafetyNumber.compare(mine.scannable, theirs.scannable), isTrue);
expect(SafetyNumber.compare(mine.scannable, stranger.scannable), isFalse);
// vadný vstup nesmí spadnout
expect(
SafetyNumber.compare(mine.scannable, Uint8List.fromList([1, 2, 3])),
isFalse,
);
});
test('KAT: zafixovaný výstup — změna = breaking change pro uživatele', () {
final n = SafetyNumber.compute(
localIdentityKey: fixedKey(1),
remoteIdentityKey: fixedKey(2),
);
// Vygenerováno libsignal 5.0.9 (iterations=5200, version=2,
// identifier=BLAKE2b-256(pubkey)). Pokud tenhle test spadne po
// upgradu, VŠEM uživatelům se změní safety numbers → stop ship.
expect(
n.display,
'289143504470406742912626343937385761080203728630218051783783',
);
});
}