import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:sepot/core/crypto/mnemonic.dart'; import 'package:sepot/core/errors.dart'; import 'package:sepot/core/crypto/sepot_crypto.dart'; import 'package:sepot/models/identity.dart'; /// S1/S2 testy nad reálným libsignal backendem (nativní knihovna). void main() { setUpAll(() async { await SepotCrypto.init(); }); group('fingerprint a ID z klíče', () { test('fingerprint má 52 znaků Crockford base32, SEP- zkratka 14', () async { final identity = await LocalIdentity.create('Alice'); expect(identity.fingerprint.display, hasLength(52)); expect(identity.sepotId, startsWith('SEP-')); expect(identity.sepotId, hasLength(14)); expect(identity.id, identity.fingerprint.display); }); test('fingerprint je deterministický: stejný klíč → stejné ID', () async { final identity = await LocalIdentity.create('A'); final again = SepotFingerprint.ofPublicKey(identity.identityPublicKey); expect(again, identity.fingerprint); }); test('KAT: známý pubkey → známý fingerprint (BLAKE2b-256 + Crockford)', () { // Pevný "pubkey" (33 B, type byte 0x05 + 32 B vzestupně) — nezávislý // na libsignal, fixuje hash i base32 vrstvu proti tiché změně final pubkey = Uint8List.fromList( [0x05, ...List.generate(32, (i) => i)], ); final fp = SepotFingerprint.ofPublicKey(pubkey); expect(fp.display, hasLength(52)); // KAT hodnota zafixovaná při zavedení fingerprintů — pokud se // změní, změnil se hash vstup/algoritmus a VŠECHNA ID uživatelů expect( fp.display, '2TFMWYYW4MRZH3BR8QCWQZVVM20EPFEP40N91S6J9HXW1GGHATRG', ); }); test('klíče: privátní 32 B, veřejný 33 B, derivace sedí', () { final keys = SepotCrypto.generateIdentityKeys(); expect(keys.privateKey, hasLength(32)); expect(keys.publicKey, hasLength(33)); expect(SepotCrypto.derivePublicKey(keys.privateKey), keys.publicKey); }); test('serializovaný privátní klíč je clamping-stabilní', () { // deserialize→serialize musí být identita na už serializovaných // bajtech — mnemonic kóduje serializovaný (clampnutý) privátní klíč, // takže nestabilní serializace by rozbila obnovu účtu ze slov for (var i = 0; i < 20; i++) { final keys = SepotCrypto.generateIdentityKeys(); final roundtrip = SepotCrypto.derivePublicKey(keys.privateKey); expect(roundtrip, keys.publicKey); final mnemonicBack = SepotMnemonic.decode( SepotMnemonic.encode(keys.privateKey), ); expect(mnemonicBack, keys.privateKey); expect(SepotCrypto.derivePublicKey(mnemonicBack), keys.publicKey); } }); }); group('mnemonic recovery', () { test('recover z mnemonic obnoví identický klíč i fingerprint', () async { final original = await LocalIdentity.create('Alice'); final words = original.mnemonic; expect(words, hasLength(24)); final recovered = await LocalIdentity.recover( mnemonicWords: words, displayName: 'Alice (obnoveno)', ); expect(recovered.identityPrivateKey, original.identityPrivateKey); expect(recovered.identityPublicKey, original.identityPublicKey); expect(recovered.fingerprint, original.fingerprint); }); test('překlep v mnemonic → MnemonicInvalidError, ne tichá jiná identita', () async { final original = await LocalIdentity.create('Alice'); final words = [...original.mnemonic]..[3] = 'abandon'; expect( () => LocalIdentity.recover( mnemonicWords: words, displayName: 'x', ), throwsA(isA()), ); }); }); group('Serializace', () { test('LocalIdentity JSON roundtrip (v2)', () async { final identity = await LocalIdentity.create('Alice'); final back = LocalIdentity.fromJson(identity.toJson()); expect(back.identityPrivateKey, identity.identityPrivateKey); expect(back.fingerprint, identity.fingerprint); expect(back.displayName, 'Alice'); }); test('starý (před-libsignal) JSON → FormatException', () { expect( () => LocalIdentity.fromJson({ 'id': 'uuid-v4-neco', 'displayName': 'Starý formát', }), throwsFormatException, ); }); test('sepot://add/ share link roundtrip', () async { final identity = await LocalIdentity.create('Čeněk Šťastný'); final link = identity.publicIdentity.toShareString(); expect(link, startsWith('sepot://add/')); final parsed = PublicIdentity.fromShareString(link); expect(parsed, isNotNull); expect(parsed!.displayName, 'Čeněk Šťastný'); expect(parsed.fingerprint, identity.fingerprint); expect(parsed.identityKey, identity.identityPublicKey); }); test('fromShareString: odpad → null, ne výjimka', () { expect(PublicIdentity.fromShareString('nesmysl'), isNull); expect(PublicIdentity.fromShareString('sepot://add/%%%'), isNull); expect(PublicIdentity.fromShareString(''), isNull); }); test('share link s routingem: aid+da přežijí roundtrip', () async { final base = await LocalIdentity.create('Routovaná'); final withRouting = base.publicIdentity.withRouting( accountId: 'deadbeef', deliveryAddress: Uint8List.fromList(List.generate(32, (i) => i)), ); final parsed = PublicIdentity.fromShareString(withRouting.toShareString()); expect(parsed, isNotNull); expect(parsed!.accountId, 'deadbeef'); expect(parsed.deliveryAddress, Uint8List.fromList(List.generate(32, (i) => i))); }); test('fromShareString: da špatné délky → null (ne vadný kontakt)', () async { final base = await LocalIdentity.create('Krátká'); final bad = base.publicIdentity.withRouting( accountId: 'deadbeef', deliveryAddress: Uint8List.fromList(List.generate(8, (i) => i)), ); expect(PublicIdentity.fromShareString(bad.toShareString()), isNull); }); }); }