Files
sharedinbox/lib/ui/router.dart
T
Thomas SharedInboxandClaude Sonnet 4.6 04e65d2fba feat: secure account sharing via public-key encryption (#107)
Replace the insecure plaintext QR export/import flow with an
end-to-end-encrypted account-transfer mechanism:

- Receiver generates an ephemeral X25519 key pair (20-minute lifetime,
  stored in the new share_keys DB table at schema v31) and displays it
  as a QR code (sharedinbox.de:pubkey:v1:…).
- Sender scans the public-key QR, selects accounts (or auto-selects
  when only one exists), encrypts them with ECIES (X25519-ECDH +
  HKDF-SHA256 + AES-256-GCM) and displays an encrypted QR
  (sharedinbox.de:encrypted-accounts:v1:…).
- Receiver scans the encrypted QR, decrypts, verifies the 20-minute
  expiry and MAC authentication tag, then imports the accounts.

New screens: AccountReceiveScreen (/accounts/receive) and
AccountSendScreen (/accounts/send), accessible from the account-list
drawer and per-account popup menu respectively.

Remove the old insecure AccountExportScreen and AccountImportScreen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 01:19:01 +02:00

166 lines
6.3 KiB
Dart

import 'package:go_router/go_router.dart';
import 'package:sharedinbox/core/models/sieve_script.dart';
import 'package:sharedinbox/ui/screens/about_screen.dart';
import 'package:sharedinbox/ui/screens/account_list_screen.dart';
import 'package:sharedinbox/ui/screens/account_receive_screen.dart';
import 'package:sharedinbox/ui/screens/account_send_screen.dart';
import 'package:sharedinbox/ui/screens/add_account_screen.dart';
import 'package:sharedinbox/ui/screens/address_emails_screen.dart';
import 'package:sharedinbox/ui/screens/changelog_screen.dart';
import 'package:sharedinbox/ui/screens/compose_screen.dart';
import 'package:sharedinbox/ui/screens/edit_account_screen.dart';
import 'package:sharedinbox/ui/screens/email_detail_screen.dart';
import 'package:sharedinbox/ui/screens/email_list_screen.dart';
import 'package:sharedinbox/ui/screens/mailbox_list_screen.dart';
import 'package:sharedinbox/ui/screens/search_screen.dart';
import 'package:sharedinbox/ui/screens/sieve_script_edit_screen.dart';
import 'package:sharedinbox/ui/screens/sieve_scripts_screen.dart';
import 'package:sharedinbox/ui/screens/sync_log_screen.dart';
import 'package:sharedinbox/ui/screens/thread_detail_screen.dart';
import 'package:sharedinbox/ui/screens/undo_log_screen.dart';
import 'package:sharedinbox/ui/widgets/undo_shell.dart';
final router = GoRouter(
initialLocation: '/accounts',
routes: [
ShellRoute(
builder: (ctx, state, child) => UndoShell(child: child),
routes: [
GoRoute(
path: '/accounts',
builder: (ctx, state) => const AccountListScreen(),
routes: [
GoRoute(
path: 'add',
builder: (ctx, state) => const AddAccountScreen(),
),
GoRoute(
path: 'receive',
builder: (ctx, state) => const AccountReceiveScreen(),
),
GoRoute(
path: 'send',
builder: (ctx, state) => const AccountSendScreen(),
),
GoRoute(
path: 'undo-log',
builder: (ctx, state) => const UndoLogScreen(),
),
GoRoute(
path: 'changelog',
builder: (ctx, state) => const ChangeLogScreen(),
),
GoRoute(
path: 'about',
builder: (ctx, state) => const AboutScreen(),
),
GoRoute(
path: ':accountId/edit',
builder: (ctx, state) => EditAccountScreen(
accountId: state.pathParameters['accountId']!,
),
),
GoRoute(
path: ':accountId/sync-log',
builder: (ctx, state) =>
SyncLogScreen(accountId: state.pathParameters['accountId']!),
),
GoRoute(
path: ':accountId/sieve',
builder: (ctx, state) => SieveScriptsScreen(
accountId: state.pathParameters['accountId']!,
),
),
GoRoute(
path: ':accountId/sieve/edit',
builder: (ctx, state) => SieveScriptEditScreen(
accountId: state.pathParameters['accountId']!,
script: state.extra as SieveScript?,
),
),
GoRoute(
path: ':accountId/sieve/local',
builder: (ctx, state) => SieveScriptsScreen(
accountId: state.pathParameters['accountId']!,
isLocal: true,
),
),
GoRoute(
path: ':accountId/sieve/local/edit',
builder: (ctx, state) => SieveScriptEditScreen(
accountId: state.pathParameters['accountId']!,
script: state.extra as SieveScript?,
isLocal: true,
),
),
GoRoute(
path: ':accountId/search',
builder: (ctx, state) =>
SearchScreen(accountId: state.pathParameters['accountId']!),
),
GoRoute(
path: ':accountId/emails/by-address/:address',
builder: (ctx, state) => AddressEmailsScreen(
accountId: state.pathParameters['accountId']!,
address: state.pathParameters['address']!,
),
),
GoRoute(
path: ':accountId/mailboxes',
builder: (ctx, state) => MailboxListScreen(
accountId: state.pathParameters['accountId']!,
),
routes: [
GoRoute(
path: ':mailboxPath/emails',
builder: (ctx, state) => EmailListScreen(
accountId: state.pathParameters['accountId']!,
mailboxPath: state.pathParameters['mailboxPath']!,
),
routes: [
GoRoute(
path: ':emailId',
builder: (ctx, state) => EmailDetailScreen(
emailId: state.pathParameters['emailId']!,
),
),
],
),
GoRoute(
path: ':mailboxPath/threads/:threadId',
builder: (ctx, state) => ThreadDetailScreen(
accountId: state.pathParameters['accountId']!,
mailboxPath: Uri.decodeComponent(
state.pathParameters['mailboxPath']!,
),
threadId: Uri.decodeComponent(
state.pathParameters['threadId']!,
),
),
),
],
),
],
),
GoRoute(path: '/search', builder: (ctx, state) => const SearchScreen()),
GoRoute(
path: '/compose',
builder: (ctx, state) {
final extra = state.extra as Map<String, dynamic>?;
return ComposeScreen(
accountId: extra?['accountId'] as String?,
replyToEmailId: extra?['replyToEmailId'] as String?,
prefillTo: extra?['prefillTo'] as String?,
prefillCc: extra?['prefillCc'] as String?,
prefillSubject: extra?['prefillSubject'] as String?,
prefillBody: extra?['prefillBody'] as String?,
);
},
),
],
),
],
);