apps/mobile is a Matrix client for Android on the famedly Matrix Dart SDK, with vodozemac (compiled from Rust during the build) for end-to-end encryption. It speaks the plain Client-Server API and uses the web client's design tokens, fonts and symbol. - Sign in by server name (.well-known), homeserver address or full user ID; sign up with the dummy and registration token stages. Sessions survive restarts: SQLite in private storage, credentials also in the Keystore, both excluded from backups and device transfers. - Room list with unread and mention counts, invites, search and filters. Timeline with grouping, day separators, notices, history on scroll, links, images and files (authenticated media). Sending text, photos, camera shots and files, with Sending/Sent/Read/Not sent and Retry. Typing, receipts, deleting. New rooms and DMs, encrypted by default; joining by address; room info with members, invite, rename, topic, notification level, leave. - Encryption: one-step setup behind a recovery key, verifying a new session with the recovery key (restoring the backup) or another session, emoji verification of people and sessions, reset. Undecryptable messages say why. Session list with verify and sign out. - Notifications while running, and push through UnifiedPush (no Firebase): the distributor's endpoint becomes an event_id_only pusher, the gateway is discovered, and a push starts the app headless to fetch, decrypt and show the event. Make targets: mobile (release APKs per ABI into bin/), mobile-run, mobile-test. Kotlin incremental compilation is off (Windows, project and pub cache on different drives), and Tink's JVM artifact is excluded in favour of tink-android. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
96 lines
3.4 KiB
Dart
96 lines
3.4 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:matrix/encryption.dart';
|
|
import 'package:matrix/matrix.dart';
|
|
|
|
/// Sets up encryption for the account in one step, like the web client:
|
|
/// a new secret storage key, new cross-signing keys, and a new key backup,
|
|
/// all behind one recovery key, which is returned to be shown once.
|
|
///
|
|
/// It always starts over: whatever secret storage, cross-signing keys or
|
|
/// backup the account had are replaced. That is what "set up" means for an
|
|
/// account without cross-signing, and what "reset" means for one that lost
|
|
/// its recovery key.
|
|
///
|
|
/// Uploading cross-signing keys needs the account password; the SDK asks
|
|
/// for it through [Client.onUiaRequest], which the app answers.
|
|
Future<String> setUpEncryption(Encryption encryption) {
|
|
final done = Completer<String>();
|
|
BootstrapState? handled;
|
|
|
|
Future<void> step(Bootstrap b) async {
|
|
// onUpdate fires once per state, but guard anyway: answering a state
|
|
// twice throws.
|
|
if (b.state == handled) return;
|
|
handled = b.state;
|
|
switch (b.state) {
|
|
case BootstrapState.loading:
|
|
break;
|
|
case BootstrapState.askWipeSsss:
|
|
b.wipeSsss(true);
|
|
case BootstrapState.askUseExistingSsss:
|
|
b.useExistingSsss(false);
|
|
case BootstrapState.askBadSsss:
|
|
b.ignoreBadSecrets(true);
|
|
case BootstrapState.askUnlockSsss:
|
|
b.unlockedSsss();
|
|
case BootstrapState.askNewSsss:
|
|
await b.newSsss();
|
|
case BootstrapState.openExistingSsss:
|
|
await b.openExistingSsss();
|
|
case BootstrapState.askWipeCrossSigning:
|
|
await b.wipeCrossSigning(true);
|
|
case BootstrapState.askSetupCrossSigning:
|
|
await b.askSetupCrossSigning(
|
|
setupMasterKey: true,
|
|
setupSelfSigningKey: true,
|
|
setupUserSigningKey: true,
|
|
);
|
|
case BootstrapState.askWipeOnlineKeyBackup:
|
|
b.wipeOnlineKeyBackup(true);
|
|
case BootstrapState.askSetupOnlineKeyBackup:
|
|
await b.askSetupOnlineKeyBackup(true);
|
|
case BootstrapState.done:
|
|
final key = b.newSsssKey?.recoveryKey;
|
|
if (key == null) {
|
|
done.completeError(Exception('No recovery key was made.'));
|
|
} else {
|
|
done.complete(key);
|
|
}
|
|
case BootstrapState.error:
|
|
done.completeError(
|
|
b.errorResult?.error ?? Exception('Setting up encryption failed.'),
|
|
);
|
|
}
|
|
}
|
|
|
|
encryption.bootstrap(
|
|
onUpdate: (b) => step(b).catchError((Object e, StackTrace s) {
|
|
Logs().e('Encryption setup failed', e, s);
|
|
if (!done.isCompleted) done.completeError(e, s);
|
|
}),
|
|
);
|
|
return done.future;
|
|
}
|
|
|
|
/// Verifies this session with the recovery key (or passphrase): unlocks
|
|
/// secret storage, which caches the cross-signing and backup keys and signs
|
|
/// this session, then restores the room keys from the backup so older
|
|
/// messages can be read.
|
|
Future<void> verifyWithRecoveryKey(Encryption encryption, String input) async {
|
|
final key = encryption.ssss.open();
|
|
await key.unlock(keyOrPassphrase: input.trim());
|
|
await key.maybeCacheAll();
|
|
if (!encryption.crossSigning.isSelfSigned) {
|
|
await encryption.crossSigning.selfSign(openSsss: key);
|
|
}
|
|
if (encryption.keyManager.enabled) {
|
|
// Best effort: the session is verified whether or not this finishes.
|
|
unawaited(
|
|
encryption.keyManager.loadAllKeys().catchError(
|
|
(Object e) => Logs().w('Restoring keys from the backup failed', e),
|
|
),
|
|
);
|
|
}
|
|
}
|