Files
sepot/app/lib/core/errors.dart
T
Julius Joska 9a638545dd feat(app): admin-signed group membership
Until now any member could change a group's roster or name. Groups now
have admins, and every change to membership, roles or the name is an
operation signed by an admin's identity key (XEdDSA). Recipients verify
the signature against the admin's key from their own contacts, never a
key carried in the same message; the relay plays no part.

- Operations form a hash chain with an epoch; gaps or forks are held back
  and resolved by a signed state from an admin.
- Groups created by older versions migrate with the creator as admin; if
  the creator has left, the smallest fingerprint takes over
  deterministically on every device.
- Mixed groups with older clients keep working in a compatibility mode
  until every member advertises admin support; that capability can only
  be added, never withdrawn.
- The last admin cannot step down or be removed.

The verification rules live in a network-free policy module with its own
tests.
2026-10-03 22:44:11 +02:00

221 lines
6.0 KiB
Dart

/// Typované chyby, které se zobrazují uživateli.
///
/// Service vrstva nemá přístup k lokalizaci (žádný BuildContext), proto
/// nesmí formulovat texty pro uživatele — vyhazuje tyhle typy a UI je
/// překládá přes `describeError` (core/l10n/error_messages.dart).
/// `toString()` je anglický technický popis pro logy, ne pro UI.
library;
sealed class SepotUserError implements Exception {
const SepotUserError();
}
/// Trust root relaye nesedí s pinovaným — server nelze ověřit.
class TrustRootChangedError extends SepotUserError {
const TrustRootChangedError();
@override
String toString() => 'relay trust root changed — refusing to communicate';
}
/// Relay nedostupný (typicky fetch sender certificate).
class RelayUnavailableError extends SepotUserError {
const RelayUnavailableError();
@override
String toString() => 'relay unavailable';
}
/// Vlastní registrace na relay ještě nedoběhla — nemáme sender adresu.
/// Přechodný stav po čerstvé instalaci; registrace běží na pozadí.
class NotYetRegisteredError extends SepotUserError {
const NotYetRegisteredError();
@override
String toString() => 'not registered on relay yet';
}
/// Odkaz/QR kontaktu je neúplný (chybí routing aid+da) — vygenerovaný
/// dřív, než měla protistrana dokončenou registraci na relay.
class ContactLinkIncompleteError extends SepotUserError {
const ContactLinkIncompleteError();
@override
String toString() => 'contact link is missing routing (incomplete)';
}
/// Příloha přesahuje serverový limit.
class AttachmentTooLargeError extends SepotUserError {
const AttachmentTooLargeError(this.maxBytes);
final int maxBytes;
int get maxMb => (maxBytes / (1024 * 1024)).round();
@override
String toString() => 'attachment too large (max $maxBytes bytes)';
}
/// Příloha pro přeposlání už v telefonu není (smazaná, mizící, nestažená).
class AttachmentUnavailableError extends SepotUserError {
const AttachmentUnavailableError();
@override
String toString() => 'attachment not available locally';
}
/// Změnu skupiny smí udělat jen správce.
class NotGroupAdminError extends SepotUserError {
const NotGroupAdminError();
@override
String toString() => 'only a group admin can do this';
}
/// Poslední správce se nesmí odebrat ani zbavit role.
class LastGroupAdminError extends SepotUserError {
const LastGroupAdminError();
@override
String toString() => 'the last group admin cannot step down';
}
/// Skupina bez jediného dalšího člena nedává smysl.
class GroupNeedsMemberError extends SepotUserError {
const GroupNeedsMemberError();
@override
String toString() => 'group needs at least one member';
}
/// Překročen strop počtu členů skupiny.
class GroupTooLargeError extends SepotUserError {
const GroupTooLargeError(this.maxMembers);
final int maxMembers;
@override
String toString() => 'group exceeds $maxMembers members';
}
/// Člen skupiny nemá doručovací adresu (starý odkaz bez routingu).
class MemberNoDeliveryAddressError extends SepotUserError {
const MemberNoDeliveryAddressError(this.displayName);
final String displayName;
@override
String toString() => 'member "$displayName" has no delivery address';
}
/// Kontakt nemá doručovací adresu (starý odkaz bez routingu).
class ContactNoDeliveryAddressError extends SepotUserError {
const ContactNoDeliveryAddressError();
@override
String toString() => 'contact has no delivery address';
}
/// Identity klíč z relaye nesedí s uloženým kontaktem — možný MITM.
class IdentityKeyMismatchError extends SepotUserError {
const IdentityKeyMismatchError();
@override
String toString() => 'identity key mismatch — possible MITM, message not sent';
}
// === Obnova účtu (mnemonic) ===
/// Fráze má špatný počet slov.
class MnemonicWordCountError extends SepotUserError {
const MnemonicWordCountError(this.count, this.expected);
final int count;
final int expected;
@override
String toString() => 'mnemonic has $count words, expected $expected';
}
/// Fráze neprošla validací BIP39 (neznámé slovo, checksum…).
class MnemonicInvalidError extends SepotUserError {
const MnemonicInvalidError(this.detail);
final String detail;
@override
String toString() => 'invalid mnemonic: $detail';
}
// === Zálohy historie (backup_service) ===
/// Soubor zálohy je poškozený (nejde přečíst/dešifrovat strukturu).
class BackupCorruptedError extends SepotUserError {
const BackupCorruptedError();
@override
String toString() => 'corrupted backup file';
}
/// Soubor není záloha Šepotu.
class BackupUnknownFormatError extends SepotUserError {
const BackupUnknownFormatError();
@override
String toString() => 'unknown backup format';
}
/// Záloha je šifrovaná klíčem jiné identity.
class BackupWrongIdentityError extends SepotUserError {
const BackupWrongIdentityError();
@override
String toString() => 'backup belongs to a different identity';
}
// === Kontakty ===
/// Kontakt s tímto fingerprintem už existuje.
class ContactAlreadyExistsError extends SepotUserError {
const ContactAlreadyExistsError();
@override
String toString() => 'contact already exists';
}
// === Nahlášení zneužití (report_service) ===
/// Hlášení bez popisu nemá co řešit.
class ReportEmptyDescriptionError extends SepotUserError {
const ReportEmptyDescriptionError();
@override
String toString() => 'report has empty description';
}
/// Server hlášení je nedosažitelný.
class ReportConnectionError extends SepotUserError {
const ReportConnectionError();
@override
String toString() => 'report endpoint unreachable';
}
/// Rate limit hlášení (429).
class ReportRateLimitedError extends SepotUserError {
const ReportRateLimitedError();
@override
String toString() => 'report rate limited';
}
/// Server hlášení vrátil chybový kód.
class ReportServerError extends SepotUserError {
const ReportServerError(this.statusCode);
final int statusCode;
@override
String toString() => 'report failed with status $statusCode';
}