Šepot — end-to-end encrypted messenger
Initial public release. Anonymous on-device identity (no phone number, e-mail, or password), Signal-protocol cryptography (X3DH + PQXDH, Double Ratchet, sealed sender) via libsignal, a metadata-minimizing relay, group chats, disappearing messages, encrypted local storage, optional Tor transport, and a UI localized into Czech, English, German, Russian, and Italian. Monorepo: Flutter client (app/), Rust relay + wire types (rust/), protobuf definitions (proto/), self-host deploy (deploy/). Licensed under AGPL-3.0 with an app-store distribution exception (§7).
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
// Integrační testy na REÁLNÉM Android runtime (skutečný keychain +
|
||||
// SQLCipher, ne mocky). Ověřují běh funkcí, které nejde otestovat v čistém
|
||||
// Dart VM. Spouštět na zařízení/emulátoru:
|
||||
// flutter test integration_test/features_test.dart -d emulator-5554
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sepot/core/backup/backup_service.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/conversation.dart';
|
||||
import 'package:sepot/models/identity.dart';
|
||||
import 'package:sepot/models/message.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
const keystore = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||
);
|
||||
|
||||
setUpAll(() async {
|
||||
await SepotCrypto.init(); // libsignal (RustLib) — jinak LocalIdentity.create padá
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
await keystore.deleteAll();
|
||||
});
|
||||
|
||||
testWidgets('DECOY PIN: skutečný PIN → reálná data, decoy PIN → prázdná DB '
|
||||
'(oddělené šifrované databáze)', (tester) async {
|
||||
final dir = await getApplicationSupportDirectory();
|
||||
final s = SecureStorageService(dbPath: '${dir.path}/it_decoy_${DateTime.now().millisecondsSinceEpoch}.db');
|
||||
|
||||
// Reálný prostor: odemkni + zapiš marker
|
||||
await s.unlockWithoutPin();
|
||||
await s.setSetting('marker', 'REAL');
|
||||
await s.enablePin('111111');
|
||||
await s.enableDecoyPin('222222');
|
||||
expect(await s.hasDecoyPin(), isTrue);
|
||||
|
||||
// Odemčení decoy PINem → prázdná decoy DB
|
||||
s.lock();
|
||||
await s.unlockWithPin('222222');
|
||||
expect(s.isDecoyMode, isTrue, reason: 'decoy PIN má otevřít decoy DB');
|
||||
expect(await s.getSetting<String>('marker'), isNull,
|
||||
reason: 'decoy DB nesmí vidět reálná data');
|
||||
await s.setSetting('marker', 'DECOY');
|
||||
|
||||
// Odemčení skutečným PINem → reálná data nedotčená
|
||||
s.lock();
|
||||
await s.unlockWithPin('111111');
|
||||
expect(s.isDecoyMode, isFalse);
|
||||
expect(await s.getSetting<String>('marker'), 'REAL',
|
||||
reason: 'reálná data přežila zápis v decoy módu');
|
||||
|
||||
// Zpět decoy → jeho zápis přetrval odděleně
|
||||
s.lock();
|
||||
await s.unlockWithPin('222222');
|
||||
expect(await s.getSetting<String>('marker'), 'DECOY');
|
||||
|
||||
// Špatný PIN (ani real ani decoy) → WrongPinException
|
||||
s.lock();
|
||||
await expectLater(
|
||||
s.unlockWithPin('999999'), throwsA(isA<WrongPinException>()));
|
||||
});
|
||||
|
||||
testWidgets('DECOY PIN se musí lišit od skutečného', (tester) async {
|
||||
final dir = await getApplicationSupportDirectory();
|
||||
final s = SecureStorageService(dbPath: '${dir.path}/it_conf_${DateTime.now().millisecondsSinceEpoch}.db');
|
||||
await s.unlockWithoutPin();
|
||||
await s.enablePin('123456');
|
||||
await expectLater(
|
||||
s.enableDecoyPin('123456'), throwsA(isA<DecoyPinConflict>()));
|
||||
});
|
||||
|
||||
testWidgets('ZMĚNA PINU po nastavení decoy: nový PIN odemkne reálná data, '
|
||||
'starý už ne, decoy zůstává funkční', (tester) async {
|
||||
final dir = await getApplicationSupportDirectory();
|
||||
final s = SecureStorageService(dbPath: '${dir.path}/it_chg_${DateTime.now().millisecondsSinceEpoch}.db');
|
||||
|
||||
await s.unlockWithoutPin();
|
||||
await s.setSetting('marker', 'REAL');
|
||||
await s.enablePin('111222');
|
||||
await s.enableDecoyPin('999888');
|
||||
|
||||
// Změna PINu (přesně pořadí z on-device testu: PIN → decoy → změna PINu)
|
||||
await s.changePin(oldPin: '111222', newPin: '333444');
|
||||
|
||||
// Nový PIN odemkne reálná data
|
||||
s.lock();
|
||||
await s.unlockWithPin('333444');
|
||||
expect(s.isDecoyMode, isFalse);
|
||||
expect(await s.getSetting<String>('marker'), 'REAL');
|
||||
|
||||
// Starý PIN už neplatí (a není to ani decoy)
|
||||
s.lock();
|
||||
await expectLater(
|
||||
s.unlockWithPin('111222'), throwsA(isA<WrongPinException>()));
|
||||
|
||||
// Decoy PIN pořád otevírá prázdnou decoy DB
|
||||
s.lock();
|
||||
await s.unlockWithPin('999888');
|
||||
expect(s.isDecoyMode, isTrue);
|
||||
expect(await s.getSetting<String>('marker'), isNull);
|
||||
|
||||
// Nový PIN se nesmí trefit do decoy
|
||||
s.lock();
|
||||
await s.unlockWithPin('333444');
|
||||
await expectLater(s.changePin(oldPin: '333444', newPin: '999888'),
|
||||
throwsA(isA<DecoyPinConflict>()));
|
||||
});
|
||||
|
||||
testWidgets('AUTO-ZÁMEK: lock() za běhu zavře DB, unlock stejným PINem '
|
||||
'data vrátí (SQLCipher reopen)', (tester) async {
|
||||
final dir = await getApplicationSupportDirectory();
|
||||
final s = SecureStorageService(dbPath: '${dir.path}/it_lock_${DateTime.now().millisecondsSinceEpoch}.db');
|
||||
|
||||
await s.unlockWithoutPin();
|
||||
await s.setSetting('marker', 'PRED-ZAMKEM');
|
||||
await s.enablePin('111222');
|
||||
|
||||
// Simulace auto-zámku: zamkni za běhu (DB se zavře)
|
||||
s.lock();
|
||||
expect(s.isUnlocked, isFalse);
|
||||
|
||||
// Odemčení TÝMŽ PINem musí projít a vrátit data
|
||||
await s.unlockWithPin('111222');
|
||||
expect(s.isUnlocked, isTrue);
|
||||
expect(await s.getSetting<String>('marker'), 'PRED-ZAMKEM',
|
||||
reason: 'auto-zámek nesmí poškodit wrapped DEK ani DB');
|
||||
|
||||
// Opakovaný cyklus zámek → odemčení (stabilita)
|
||||
s.lock();
|
||||
await s.unlockWithPin('111222');
|
||||
expect(await s.getSetting<String>('marker'), 'PRED-ZAMKEM');
|
||||
});
|
||||
|
||||
testWidgets('ZÁLOHA: export → import obnoví konverzaci (a cizí identita ji '
|
||||
'neotevře)', (tester) async {
|
||||
final dir = await getApplicationSupportDirectory();
|
||||
final ts = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
// Zdroj: konverzace se zprávou
|
||||
final src = SecureStorageService(dbPath: '${dir.path}/it_bk_src_$ts.db');
|
||||
await src.unlockWithoutPin();
|
||||
final conv = Conversation(contactId: 'PEER1', messages: [
|
||||
Message.text(senderId: 'PEER1', recipientId: 'me', content: 'ahoj ze zálohy'),
|
||||
]);
|
||||
await src.saveConversation(conv);
|
||||
|
||||
final me = await LocalIdentity.create('Já');
|
||||
final bytes = await BackupService(src).export(me);
|
||||
expect(bytes.length, greaterThan(20));
|
||||
|
||||
// Cíl: čerstvá DB, import STEJNOU identitou → konverzace zpět
|
||||
final dst = SecureStorageService(dbPath: '${dir.path}/it_bk_dst_$ts.db');
|
||||
await dst.unlockWithoutPin();
|
||||
expect((await dst.loadConversations()).length, 0);
|
||||
final n = await BackupService(dst).import(me, bytes);
|
||||
expect(n, 1);
|
||||
final restored = await dst.loadConversations();
|
||||
expect(restored.length, 1);
|
||||
expect(restored.first.messages.first.content, 'ahoj ze zálohy');
|
||||
|
||||
// Cizí identita zálohu neotevře
|
||||
final other = await LocalIdentity.create('Nikdo');
|
||||
final dst2 = SecureStorageService(dbPath: '${dir.path}/it_bk_x_$ts.db');
|
||||
await dst2.unlockWithoutPin();
|
||||
await expectLater(
|
||||
BackupService(dst2).import(other, bytes), throwsA(isA<FormatException>()));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user