Š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,119 @@
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:sepot/core/crypto/mnemonic.dart';
|
||||
import 'package:sepot/core/errors.dart';
|
||||
|
||||
/// KAT vektory z oficiální BIP39 test suite (Trezor) pro 256bit entropii.
|
||||
/// Review-crypto F-3: mnemonic je REVERZIBILNÍ ENKÓDOVÁNÍ klíče — known-answer
|
||||
/// testy jsou povinné v CI napříč Dart i Rust.
|
||||
const _katVectors = <String, String>{
|
||||
'00000000000000000000000000000000'
|
||||
'00000000000000000000000000000000':
|
||||
'abandon abandon abandon abandon abandon abandon abandon abandon '
|
||||
'abandon abandon abandon abandon abandon abandon abandon abandon '
|
||||
'abandon abandon abandon abandon abandon abandon abandon art',
|
||||
'7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f'
|
||||
'7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f':
|
||||
'legal winner thank year wave sausage worth useful legal winner '
|
||||
'thank year wave sausage worth useful legal winner thank year '
|
||||
'wave sausage worth title',
|
||||
'80808080808080808080808080808080'
|
||||
'80808080808080808080808080808080':
|
||||
'letter advice cage absurd amount doctor acoustic avoid letter '
|
||||
'advice cage absurd amount doctor acoustic avoid letter advice '
|
||||
'cage absurd amount doctor acoustic bless',
|
||||
'ffffffffffffffffffffffffffffffff'
|
||||
'ffffffffffffffffffffffffffffffff':
|
||||
'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo '
|
||||
'zoo zoo zoo zoo zoo zoo zoo vote',
|
||||
};
|
||||
|
||||
Uint8List _hexToBytes(String hex) {
|
||||
final out = Uint8List(hex.length ~/ 2);
|
||||
for (var i = 0; i < out.length; i++) {
|
||||
out[i] = int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('SepotMnemonic KAT (oficiální BIP39 vektory)', () {
|
||||
_katVectors.forEach((entropyHex, expectedMnemonic) {
|
||||
test('entropy ${entropyHex.substring(0, 8)}… → známých 24 slov', () {
|
||||
final words = SepotMnemonic.encode(_hexToBytes(entropyHex));
|
||||
expect(words.join(' '), expectedMnemonic);
|
||||
expect(words, hasLength(SepotMnemonic.wordCount));
|
||||
});
|
||||
|
||||
test('mnemonic ${expectedMnemonic.substring(0, 12)}… → přesná entropie',
|
||||
() {
|
||||
final decoded = SepotMnemonic.decode(expectedMnemonic.split(' '));
|
||||
expect(decoded, _hexToBytes(entropyHex));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('SepotMnemonic roundtrip', () {
|
||||
test('decode(encode(k)) == k pro 100 náhodných klíčů', () {
|
||||
final rng = Random.secure();
|
||||
for (var i = 0; i < 100; i++) {
|
||||
final key = Uint8List.fromList(
|
||||
List.generate(32, (_) => rng.nextInt(256)),
|
||||
);
|
||||
expect(SepotMnemonic.decode(SepotMnemonic.encode(key)), key);
|
||||
}
|
||||
});
|
||||
|
||||
test('normalizace: velká písmena a mezery navíc nevadí', () {
|
||||
final key = Uint8List(32);
|
||||
final words = SepotMnemonic.encode(key);
|
||||
final messy = words
|
||||
.map((w) => ' ${w.toUpperCase()} ')
|
||||
.toList();
|
||||
expect(SepotMnemonic.decode(messy), key);
|
||||
});
|
||||
});
|
||||
|
||||
group('SepotMnemonic chyby', () {
|
||||
final valid = SepotMnemonic.encode(Uint8List(32));
|
||||
|
||||
test('špatný počet slov → MnemonicWordCountError', () {
|
||||
expect(
|
||||
() => SepotMnemonic.decode(valid.sublist(0, 23)),
|
||||
throwsA(isA<MnemonicWordCountError>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('neznámé slovo → MnemonicInvalidError', () {
|
||||
final broken = [...valid]..[0] = 'xylofon';
|
||||
expect(() => SepotMnemonic.decode(broken),
|
||||
throwsA(isA<MnemonicInvalidError>()));
|
||||
});
|
||||
|
||||
test('rozbitý checksum (prohozená slova) → MnemonicInvalidError', () {
|
||||
// poslední slovo nese checksum — výměna prvních dvou RŮZNÝCH slov
|
||||
// checksum rozbije
|
||||
final words = SepotMnemonic.encode(
|
||||
Uint8List.fromList(List.generate(32, (i) => i * 7 % 256)),
|
||||
);
|
||||
final swapped = [...words];
|
||||
final j = swapped.indexWhere((w) => w != swapped[0]);
|
||||
swapped[0] = words[j];
|
||||
swapped[j] = words[0];
|
||||
expect(() => SepotMnemonic.decode(swapped),
|
||||
throwsA(isA<MnemonicInvalidError>()));
|
||||
});
|
||||
|
||||
test('isValid odmítne nevalidní a přijme validní', () {
|
||||
expect(SepotMnemonic.isValid(valid), isTrue);
|
||||
expect(SepotMnemonic.isValid(valid.sublist(0, 12)), isFalse);
|
||||
});
|
||||
|
||||
test('encode odmítne jinou délku než 32 B', () {
|
||||
expect(() => SepotMnemonic.encode(Uint8List(16)), throwsArgumentError);
|
||||
expect(() => SepotMnemonic.encode(Uint8List(64)), throwsArgumentError);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user