Files
sepot/app/lib/core/crypto/safety_number.dart
T
Julius Joska 8081d98c67 Š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).
2026-07-11 17:21:47 +02:00

71 lines
2.5 KiB
Dart

/// Safety number — vizuální ověření kontaktu.
///
/// Používá libsignal `Fingerprint` (NumericFingerprintGenerator), žádný
/// vlastní výpočet: iterovaný SHA-512 (5200 iterací jako Signal) nad
/// (verze ‖ identifikátor ‖ identity klíč) pro obě strany, zobrazení
/// 60 dekadických číslic (12 bloků po 5). Menší polovina jde první,
/// takže OBĚ strany vidí stejné číslo.
///
/// Stabilní identifikátor strany = BLAKE2b-256 fingerprint jejího
/// identity klíče (deterministický z klíče — žádný telefon, e-mail ani
/// server handle, přesně v duchu „identita = klíč").
///
/// QR ověření: [scannable] bajty do QR; sken protistrany se porovná přes
/// [SafetyNumber.compare] (libsignal kontroluje křížem local↔remote).
library;
import 'dart:typed_data';
import 'package:libsignal/libsignal.dart' as signal;
import 'package:sepot/models/identity.dart';
class SafetyNumber {
SafetyNumber._(this.display, this.scannable);
/// Iterace hashe jako Signal (bezpečnostní parametr — NEměnit,
/// změna = všem se změní safety numbers).
static const int iterations = 5200;
/// Verze scannable formátu (libsignal proto).
static const int version = 2;
/// 60 číslic bez mezer.
final String display;
/// Binární encoding pro QR + [compare].
final Uint8List scannable;
/// Spočítá safety number páru identity klíčů (33 B serializované).
factory SafetyNumber.compute({
required Uint8List localIdentityKey,
required Uint8List remoteIdentityKey,
}) {
final fp = signal.Fingerprint(
iterations: iterations,
version: version,
localIdentifier: SepotFingerprint.ofPublicKey(localIdentityKey).bytes,
localPublicKey: localIdentityKey,
remoteIdentifier: SepotFingerprint.ofPublicKey(remoteIdentityKey).bytes,
remotePublicKey: remoteIdentityKey,
);
return SafetyNumber._(fp.displayString(), fp.scannableEncoding());
}
/// 12 bloků po 5 číslicích pro zobrazení.
List<String> get groups => [
for (var i = 0; i < display.length; i += 5)
display.substring(i, i + 5),
];
/// Porovná můj scannable s naskenovaným od protistrany. True = klíče
/// si křížem odpovídají (žádný MITM), false = NESHODA.
static bool compare(Uint8List mine, Uint8List scanned) {
try {
return signal.fingerprintCompare(fingerprint1: mine, fingerprint2: scanned);
} on Object {
// vadný/cizí QR formát = neshoda, ne pád
return false;
}
}
}