Files
sepot/app/test/storage/decoy_isolation_test.dart
T
Julius Joska e3a2378c34 fix(app): accept incoming calls, restore from recovery phrase, duress session isolation, message loss
Calls: `call` was missing from the known content types, so every offer was
acknowledged and dropped; the call screen column now fills the width (the
avatar jumped left after hang-up); a ring started from a push ends after
12 s when no offer arrives; STUN fallback when no TURN credentials.
Registration: X-Sepot-Identity-Proof lets a new device reclaim an existing
identity; retry backs off from 30 s to 10 min and exposes a conflict flag.
Prekeys: PrekeysLow triggers a batch of 50 one-time prekeys via POST
/v1/keys; the signed prekey rotates weekly, the Kyber prekey monthly.
Messaging: crypto operations run serially (concurrent encrypts derived the
same message key); stop() waits for in-flight deliveries (auto-lock between
decrypt and persist lost messages); disappearing-message cleanup no longer
overwrites fresh messages; a push wake-up reconnects stale connections; the
outbox carries polls and mentions; length limits on wire input.
Storage: a duress session never exposes the real identity or phrase, does
not start networking and cannot touch the keystore; deleting data in a
duress session leaves the real account alone; PIN guessing is throttled
(5 misses, then 30 s doubling up to 15 min); the duress blob shares the salt
so unlock time does not reveal which PIN was entered; iOS keychain items are
this-device only; biometrics are biometric-only; screen security defaults
to on and is applied after unlock; tapjacking filter in MainActivity.
Tests: content types, shared-salt unwrap, duress isolation.
2026-10-09 14:02:33 +02:00

87 lines
3.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Argon2id (64 MiB) ×6 pod zátěží celé sady: víc než výchozích 30 s.
@Timeout(Duration(minutes: 3))
library;
import 'dart:io';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sepot/core/crypto/dek_wrap.dart';
import 'package:sepot/core/crypto/sepot_crypto.dart';
import 'package:sepot/core/storage/secure_storage.dart';
import 'package:sepot/models/identity.dart';
/// Nátlaková (decoy) relace musí být izolovaná od skutečného účtu:
/// jiná identita, žádná fráze, „smazat" nesáhne na skutečná data,
/// a zámek režim resetuje. Keystore je nahrazený mockem, DB jde do temp.
/// Potřebuje nativní libsignal (LocalIdentity) a sqlcipher.
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late Directory tmp;
late SecureStorageService storage;
setUpAll(() async => SepotCrypto.init());
setUp(() async {
FlutterSecureStorage.setMockInitialValues({});
tmp = Directory.systemTemp.createTempSync('sepot_decoy');
storage = SecureStorageService(dbPath: '${tmp.path}/app.db');
await storage.unlockWithoutPin();
await storage.saveIdentity(await LocalIdentity.create('Alice'));
await storage.saveSeedPhrase(List.filled(24, 'abandon'));
await storage.enablePin('111111');
await storage.enableDecoyPin('222222');
storage.lock();
});
tearDown(() {
storage.lock();
tmp.deleteSync(recursive: true);
});
test('decoy PIN otevře izolovanou relaci', () async {
await storage.unlockWithPin('222222');
expect(storage.isDecoyMode, isTrue);
expect(await storage.loadSeedPhrase(), isNull);
final shown = await storage.loadIdentity();
expect(shown, isNotNull);
storage.lock();
expect(storage.isDecoyMode, isFalse);
await storage.unlockWithPin('111111');
expect(storage.isDecoyMode, isFalse);
final real = await storage.loadIdentity();
expect(real!.id, isNot(shown!.id), reason: 'decoy ukazuje jinou identitu');
expect(await storage.loadSeedPhrase(), hasLength(24));
});
test('smazání v decoy relaci nechá skutečná data', () async {
await storage.unlockWithPin('222222');
await storage.deleteAll();
expect(File('${tmp.path}/app.db').existsSync(), isTrue);
expect(await storage.hasIdentity(), isTrue);
expect(await storage.hasPin(), isTrue);
await storage.unlockWithPin('111111');
expect(storage.isDecoyMode, isFalse);
});
test('změny keystore jsou v decoy relaci zakázané', () async {
await storage.unlockWithPin('222222');
expect(() => storage.enableBiometrics(), throwsStateError);
expect(() => storage.disablePin(), throwsStateError);
expect(
() => storage.changePin(oldPin: '222222', newPin: '333333'),
throwsStateError);
});
test('po pěti špatných PINech se zámek na čas zavře', () async {
for (var i = 0; i < 5; i++) {
await expectLater(storage.unlockWithPin('000000'),
throwsA(isA<WrongPinException>()));
}
expect(await storage.pinLockRemaining(), isNotNull);
await expectLater(storage.unlockWithPin('111111'),
throwsA(isA<PinLockedException>()));
});
}