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>
68 lines
2.0 KiB
Dart
68 lines
2.0 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:drift/drift.dart';
|
|
|
|
import 'package:sharedinbox/core/repositories/share_key_repository.dart';
|
|
import 'package:sharedinbox/core/services/share_encryption_service.dart';
|
|
import 'package:sharedinbox/data/db/database.dart';
|
|
|
|
/// Drift-backed implementation of [ShareKeyRepository].
|
|
///
|
|
/// Each key pair lives for 20 minutes. Expired rows are pruned whenever a
|
|
/// new key pair is created or looked up.
|
|
class ShareKeyRepositoryImpl implements ShareKeyRepository {
|
|
ShareKeyRepositoryImpl(this._db);
|
|
|
|
final AppDatabase _db;
|
|
|
|
@override
|
|
Future<ShareKeyMaterial> createKeyPair() async {
|
|
await _pruneExpired();
|
|
|
|
final material = await ShareEncryptionService.generateKeyPair();
|
|
final keyIdHex = _hex(material.keyId);
|
|
final expiresAt = DateTime.now().toUtc().add(const Duration(minutes: 20));
|
|
|
|
await _db.into(_db.shareKeys).insert(
|
|
ShareKeysCompanion.insert(
|
|
id: keyIdHex,
|
|
publicKey: base64.encode(material.publicKeyBytes),
|
|
privateKey: base64.encode(material.privateKeyBytes),
|
|
expiresAt: expiresAt,
|
|
),
|
|
);
|
|
|
|
return material;
|
|
}
|
|
|
|
@override
|
|
Future<ShareKeyMaterial?> findByKeyId(Uint8List keyId) async {
|
|
await _pruneExpired();
|
|
|
|
final keyIdHex = _hex(keyId);
|
|
final row = await (_db.select(_db.shareKeys)
|
|
..where((t) => t.id.equals(keyIdHex)))
|
|
.getSingleOrNull();
|
|
|
|
if (row == null) return null;
|
|
if (row.expiresAt.isBefore(DateTime.now().toUtc())) return null;
|
|
|
|
return ShareKeyMaterial(
|
|
keyId: keyId,
|
|
publicKeyBytes: Uint8List.fromList(base64.decode(row.publicKey)),
|
|
privateKeyBytes: Uint8List.fromList(base64.decode(row.privateKey)),
|
|
);
|
|
}
|
|
|
|
Future<void> _pruneExpired() async {
|
|
await (_db.delete(_db.shareKeys)
|
|
..where(
|
|
(t) => t.expiresAt.isSmallerThanValue(DateTime.now().toUtc()),
|
|
))
|
|
.go();
|
|
}
|
|
|
|
static String _hex(Uint8List bytes) =>
|
|
bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
|
}
|