90a1390716
Fixes reported by testers and found in an app review: - First message right after install no longer fails. Before the app finished registering with the relay, the outgoing message could crash on a null delivery address. The profile field is now optional and the send path throws a clear "still connecting, try again shortly" error while registration completes in the background. - The QR code and share link now appear only once the delivery address is ready. Previously a link generated before the first connection lacked routing, so the other side could not send the first message. The screen now shows a connecting state and reactively swaps in the QR when ready. - Incomplete contact links are rejected on import instead of failing at first send, with a clear prompt to ask for a fresh link. - Auto-lock no longer silences the app: after locking and unlocking, messaging is restarted (it previously stayed dead until app restart). - Group messages are marked failed on a non-per-member send error instead of hanging in the "sending" state forever. - Hardened the relay client against a rare duplicate connection when toggling Tor or locking mid-connect, and fromShareString now rejects a delivery address of the wrong length. - Pin unifiedpush below 6.2.0 to drop the webcrypto dependency (its native framework lacks LC_ENCRYPTION_INFO and blocked the iOS upload). - Skip Android release lint (network dependency check hangs on the build host and adds no value for a Flutter app).
161 lines
6.1 KiB
Dart
161 lines
6.1 KiB
Dart
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<MnemonicInvalidError>()),
|
|
);
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
}
|