fix: read back sent copies after the sec 5.6 upstream fix, pinned at 2d65d66
This commit is contained in:
parent
52947e153d
commit
d6f4c434fa
36 changed files with 4223 additions and 3118 deletions
|
|
@ -1,26 +1,32 @@
|
|||
// End-to-end against a live reference server: register an address on a
|
||||
// locally running smolmaild, send sealed messages to ourselves, fetch them
|
||||
// back, exercise the accept-token round trip (§5.8) between the requests and
|
||||
// main tiers, and check the server is drained afterwards. Skips when nothing
|
||||
// listens on 127.0.0.1:1961, so `devbox run test` does not depend on a server.
|
||||
// End-to-end against a live server: register an address, send sealed mail to
|
||||
// ourselves, fetch it back, exercise the accept-token round trip (§5.8)
|
||||
// between the requests and main tiers, restore onto a second store, and
|
||||
// check the server is drained afterwards. Skips when nothing listens on
|
||||
// 127.0.0.1:1961, so `devbox run test` does not depend on a server.
|
||||
//
|
||||
// To run it: (cd ../smolmail && uv run smolmaild.py keygen --key server.key &&
|
||||
// uv run smolmaild.py serve --key server.key --db mail.db)
|
||||
// then `devbox run test`.
|
||||
// The server key is pinned directly: on this machine the bunshin.service
|
||||
// static key is a documented, operator-supplied value — exactly the trusted
|
||||
// channel SPEC.md §4 asks a pin to come from.
|
||||
|
||||
import "dart:io";
|
||||
import "dart:math";
|
||||
import "dart:typed_data";
|
||||
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:hive_flutter/hive_flutter.dart";
|
||||
|
||||
import "package:smol_mail/smol/address.dart";
|
||||
import "package:smol_mail/smol/client.dart";
|
||||
import "package:smol_mail/smol/crypto.dart";
|
||||
import "package:smol_mail/smol/errors.dart";
|
||||
import "package:smol_mail/smol/proto.dart";
|
||||
import "package:smol_mail/smol/store.dart";
|
||||
import "package:smol_mail/smol/ui.dart";
|
||||
|
||||
const host = "127.0.0.1";
|
||||
const port = 1961;
|
||||
const serverKey = "lm2gqd7e5q67xq3isc5hx6jfj2q7a7xvq4l7trctxipudujovmgq";
|
||||
|
||||
Uint8List randomBytes(int n) =>
|
||||
Uint8List.fromList(List.generate(n, (_) => Random.secure().nextInt(256)));
|
||||
|
||||
Future<bool> serverUp() async {
|
||||
try {
|
||||
|
|
@ -33,35 +39,31 @@ Future<bool> serverUp() async {
|
|||
}
|
||||
}
|
||||
|
||||
Future<SmolStore> freshStore(String tag, String dir) => SmolStore.open(
|
||||
dbPath: "$dir/$tag.db", stateBox: "$tag-state", readBox: "$tag-read");
|
||||
|
||||
void main() {
|
||||
test("register, send to self, fetch, unseal, drain", () async {
|
||||
if (!await serverUp()) {
|
||||
markTestSkipped("no smolmaild on $host:$port");
|
||||
markTestSkipped("no server on $host:$port");
|
||||
return;
|
||||
}
|
||||
final dir = await Directory.systemTemp.createTemp("smol-e2e");
|
||||
Hive.init(dir.path);
|
||||
final store = await SmolStore.open();
|
||||
final store = await freshStore("main", dir.path);
|
||||
final client = SmolClient(store);
|
||||
|
||||
// First contact is trust-on-first-use: learn the key the handshake reveals,
|
||||
// then pin it — the flow a user with an operator-supplied key skips.
|
||||
final warnings = <String>[];
|
||||
client.onWarning = warnings.add;
|
||||
|
||||
final master = client.createIdentity();
|
||||
await client.createIdentity();
|
||||
final me = client.identity!;
|
||||
final user = "e2e${hex(randomBytes(4))}";
|
||||
final address = parseAddress("$user@$host");
|
||||
final learned = await client.connect(address, requirePin: false);
|
||||
// §8: the first, unpinned session must be announced as unverified.
|
||||
expect(warnings, isNotEmpty);
|
||||
expect(warnings.single, contains("not pinned"));
|
||||
store.pinServer(host, learned.serverStatic);
|
||||
learned.session.wire.close();
|
||||
store.pinServer(host, serverKey);
|
||||
|
||||
await client.registerAccount(address.short);
|
||||
expect(store.account()?.user, user);
|
||||
expect(client.accountAddress()!.short, address.short);
|
||||
|
||||
await client.send(address.short, "hello e2e", "sealed and signed");
|
||||
await client.send(address.short, "second", "another sealed envelope");
|
||||
|
|
@ -75,33 +77,32 @@ void main() {
|
|||
expect(store.listMessages("inbox"), isEmpty);
|
||||
final requests = store.listMessages("requests");
|
||||
expect(requests.length, 2);
|
||||
// receivedAt has second granularity, so the order of the two is not
|
||||
// guaranteed; assert on the pair, then open the one we care about.
|
||||
final subjects = requests.map((m) => client.describe(m).subject).toSet();
|
||||
expect(subjects, {"hello e2e", "second"});
|
||||
final hello = requests.firstWhere(
|
||||
(m) => client.describe(m).subject == "hello e2e");
|
||||
final hello = requests
|
||||
.firstWhere((m) => client.describe(m).subject == "hello e2e");
|
||||
final opened = client.describe(hello);
|
||||
expect(opened.error, isNull);
|
||||
expect(opened.body, "sealed and signed\n");
|
||||
expect(hex(opened.sender!), hex(me.publicKey));
|
||||
// fumi's describe splits the trailing newline into the frontmatter
|
||||
// parse, so the body arrives trimmed.
|
||||
expect(opened.body, "sealed and signed");
|
||||
expect(opened.sender, me.publicKey);
|
||||
|
||||
// The server must be drained: everything that verified was acknowledged.
|
||||
final again = await client.fetch();
|
||||
expect(again.stored, 0);
|
||||
|
||||
// Accept ourselves as a correspondent (§5.8): the change is pushed to the
|
||||
// server right away. This next message carries our own Accept field, but
|
||||
// no MAC yet — we cannot know our own token before receiving and parsing
|
||||
// a message that carries it — so it still lands in requests.
|
||||
// Accept ourselves as a correspondent (§5.8): the change is pushed to
|
||||
// the server right away. This next message carries our own Accept field,
|
||||
// but no MAC yet, so it still lands in requests.
|
||||
await client.acceptContact(address.short);
|
||||
await client.send(address.short, "third", "still unsolicited");
|
||||
expect((await client.fetch()).stored, 1);
|
||||
expect(store.listMessages("requests").length, 3);
|
||||
expect(store.listMessages("inbox"), isEmpty);
|
||||
|
||||
// Having now learned our own token from that message's Accept field, the
|
||||
// next one carries a matching MAC and reaches the main tier.
|
||||
// Having now learned our own token, the next one carries a matching MAC
|
||||
// and reaches the main tier.
|
||||
await client.send(address.short, "fourth", "now accepted");
|
||||
expect((await client.fetch()).stored, 1);
|
||||
final mainTier = store.listMessages("inbox");
|
||||
|
|
@ -123,18 +124,17 @@ void main() {
|
|||
// Restore on a second device: same master, fresh store, no pin. Unpinned
|
||||
// recall is refused; re-registering a taken name is refused; recall with
|
||||
// the operator-supplied key then binds the account without REGISTER.
|
||||
final restored = await SmolStore.open(
|
||||
stateBox: "e2e-restore-state", mailBox: "e2e-restore-mail");
|
||||
final secondDevice = SmolClient(restored);
|
||||
restored.setMaster(master);
|
||||
expect(
|
||||
final secondStore = await freshStore("second", dir.path);
|
||||
final secondDevice = SmolClient(secondStore);
|
||||
secondStore.restoreMaster(store.master()!, 0);
|
||||
await expectLater(
|
||||
secondDevice.recallAccount(address.short), throwsA(isA<SmolError>()));
|
||||
restored.pinServer(host, learned.serverStatic);
|
||||
secondStore.pinServer(host, serverKey);
|
||||
await expectLater(
|
||||
secondDevice.registerAccount(address.short), throwsA(isA<SmolError>()));
|
||||
final bound = await secondDevice.recallAccount(address.short);
|
||||
expect(bound.short, address.short);
|
||||
expect(restored.account()!.user, user);
|
||||
expect(secondDevice.accountAddress()!.user, user);
|
||||
|
||||
// Re-resolving our own address finds the same key and says so quietly.
|
||||
final outcome = await client.refreshContact(address.short);
|
||||
|
|
@ -146,24 +146,21 @@ void main() {
|
|||
test("leave mail on server keeps mail until deleted, with dedupe on refetch",
|
||||
() async {
|
||||
if (!await serverUp()) {
|
||||
markTestSkipped("no smolmaild on $host:$port");
|
||||
markTestSkipped("no server on $host:$port");
|
||||
return;
|
||||
}
|
||||
final dir = await Directory.systemTemp.createTemp("smol-e2e-keep");
|
||||
Hive.init(dir.path);
|
||||
final store =
|
||||
await SmolStore.open(stateBox: "e2e-keep-state", mailBox: "e2e-keep-mail");
|
||||
final store = await freshStore("keep", dir.path);
|
||||
final client = SmolClient(store);
|
||||
|
||||
client.createIdentity();
|
||||
await client.createIdentity();
|
||||
final user = "e2ekeep${hex(randomBytes(4))}";
|
||||
final address = parseAddress("$user@$host");
|
||||
final learned = await client.connect(address, requirePin: false);
|
||||
store.pinServer(host, learned.serverStatic);
|
||||
learned.session.wire.close();
|
||||
store.pinServer(host, serverKey);
|
||||
await client.registerAccount(address.short);
|
||||
|
||||
store.setLeaveOnServer(true);
|
||||
await store.setLeaveOnServer(true);
|
||||
await client.send(address.short, "kept", "stays on the server until deleted");
|
||||
|
||||
final first = await client.fetch();
|
||||
|
|
@ -172,19 +169,17 @@ void main() {
|
|||
expect(record.keptOnServer, isTrue);
|
||||
expect(client.describe(record).subject, "kept");
|
||||
|
||||
// Re-fetching from scratch must not duplicate it locally (storeIfNew's
|
||||
// Re-paging from scratch must not duplicate it locally (the seen-id
|
||||
// dedupe), even though the server still has it (nothing was deleted).
|
||||
store.setCursor(0, Uint8List(idLen));
|
||||
final second = await client.fetch();
|
||||
final second = await client.fetch(reset: true);
|
||||
expect(second.stored, 0);
|
||||
expect(store.listMessages("requests").length, 1);
|
||||
|
||||
// Deleting removes it from the server too: a further full re-page after
|
||||
// deletion must come back empty rather than resurrecting it.
|
||||
// Deleting removes it locally and from the server too: a further full
|
||||
// re-page after deletion comes back empty rather than resurrecting it.
|
||||
await client.deleteMessage("requests", record);
|
||||
expect(store.listMessages("requests"), isEmpty);
|
||||
store.setCursor(0, Uint8List(idLen));
|
||||
final third = await client.fetch();
|
||||
final third = await client.fetch(reset: true);
|
||||
expect(third.stored, 0);
|
||||
expect(store.listMessages("requests"), isEmpty);
|
||||
}, timeout: const Timeout(Duration(minutes: 2)));
|
||||
|
|
|
|||
19
test/ffi_smoke_test.dart
Normal file
19
test/ffi_smoke_test.dart
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Spike: the first Dart-to-Rust link. Opens the built cdylib and calls the
|
||||
// smoke entry point, which round-trips an address through fumi-core's
|
||||
// in-memory store. Proves the dynamic-library bridge works; the real binding
|
||||
// replaces this once the surface settles.
|
||||
|
||||
import "dart:ffi";
|
||||
import "dart:io";
|
||||
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
void main() {
|
||||
test("native library links and round-trips", () {
|
||||
final lib = DynamicLibrary.open(
|
||||
"${Directory.current.path}/native/target/release/libsmol_mail_native.so");
|
||||
final smolSmoke =
|
||||
lib.lookupFunction<Int32 Function(), int Function()>("smol_smoke");
|
||||
expect(smolSmoke(), 0);
|
||||
});
|
||||
}
|
||||
77
test/interop_fumi_test.dart
Normal file
77
test/interop_fumi_test.dart
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// Cross-client interop: kirakira's gsmol export imports into fumi, and
|
||||
// fumi's export imports back into kirakira — the interchange the export
|
||||
// patch upstream exists for. The kirakira side is driven through the real
|
||||
// store API now: pins and contacts (with rotation history) land in the
|
||||
// native store, and the sealed mail round-trip is covered by fumi-core's
|
||||
// own export tests, so this proves the container compatibility both ways.
|
||||
// Runs the fumi CLI as a subprocess, so the test skips when no built fumi
|
||||
// sits in the sibling checkout.
|
||||
|
||||
import "dart:io";
|
||||
import "dart:typed_data";
|
||||
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:hive_flutter/hive_flutter.dart";
|
||||
|
||||
import "package:smol_mail/smol/store.dart";
|
||||
|
||||
const fumiBinary = "../fumi/target/release/fumi";
|
||||
|
||||
void main() {
|
||||
final fumiBuilt = File(fumiBinary).existsSync();
|
||||
|
||||
setUpAll(() async {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final dir = await Directory.systemTemp.createTemp("smol-interop");
|
||||
Hive.init(dir.path);
|
||||
});
|
||||
|
||||
test("exports cross-import in both directions",
|
||||
skip: fumiBuilt ? false : "no built fumi in ../fumi", () async {
|
||||
final dir = await Directory.systemTemp.createTemp("fumi-interop");
|
||||
final master = Uint8List.fromList(List.generate(32, (i) => i + 3));
|
||||
// Real, distinct, valid key forms: the system server's and fumi's.
|
||||
const pinKey = "lm2gqd7e5q67xq3isc5hx6jfj2q7a7xvq4l7trctxipudujovmgq";
|
||||
|
||||
final mine = await SmolStore.open(
|
||||
dbPath: "${dir.path}/kirakira.db",
|
||||
stateBox: "interop-state",
|
||||
readBox: "interop-read");
|
||||
mine.restoreMaster(master, 0);
|
||||
mine.pinServer("example.org", pinKey);
|
||||
// One contact, bound to the current key; rotation history is written by
|
||||
// the trust engine on a validated rotation, not by a plain re-save.
|
||||
await mine.saveContact("alice@example.org", pinKey, verified: true);
|
||||
|
||||
final file = File("${dir.path}/kirakira.json");
|
||||
await file.writeAsString(await mine.exportData());
|
||||
|
||||
// fumi imports it.
|
||||
final keyFile = File("${dir.path}/identity.key");
|
||||
await keyFile.writeAsBytes(master);
|
||||
Future<ProcessResult> fumi(List<String> args) => Process.run(
|
||||
fumiBinary,
|
||||
["--key", keyFile.path, "--db", "${dir.path}/fumi.db", ...args]);
|
||||
final into = await fumi(["import-backup", file.path]);
|
||||
expect(into.exitCode, 0, reason: into.stderr.toString());
|
||||
expect(into.stdout.toString(), contains("1 contacts"));
|
||||
|
||||
// fumi exports its store, and kirakira imports that back.
|
||||
final fumiFile = File("${dir.path}/fumi.json");
|
||||
final out = await fumi(["export", fumiFile.path]);
|
||||
expect(out.exitCode, 0, reason: out.stderr.toString());
|
||||
final restored = await SmolStore.open(
|
||||
dbPath: "${dir.path}/restored.db",
|
||||
stateBox: "interop2-state",
|
||||
readBox: "interop2-read");
|
||||
restored.restoreMaster(master, 0);
|
||||
final summary = await restored.importData(await fumiFile.readAsString());
|
||||
expect(summary.contactsAdded, 1);
|
||||
expect(summary.pinsAdded, 1);
|
||||
expect(summary.malformed, 0);
|
||||
// The binding survived both directions of the round trip.
|
||||
final contact = restored.contact("alice@example.org")!;
|
||||
expect(contact.key, pinKey);
|
||||
expect(restored.serverPin("example.org"), pinKey);
|
||||
});
|
||||
}
|
||||
127
test/native_binding_test.dart
Normal file
127
test/native_binding_test.dart
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
// The binding layer's proof: the Dart client drives the C ABI end to end —
|
||||
// handles, error codes, async isolates, and the backup round-trip. The
|
||||
// server-backed part runs only when the spike bunshin is up on 19619.
|
||||
|
||||
import "dart:convert";
|
||||
import "dart:io";
|
||||
import "dart:typed_data";
|
||||
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import "package:smol_mail/native/client.dart";
|
||||
import "package:smol_mail/native/ffi.dart";
|
||||
|
||||
const spikeServerKey = "g5ttsssqjq6j37vso3h7otn33zrk6o6r7c6jjm7guxbbijoa5nqq";
|
||||
const spikeServer = "127.0.0.1";
|
||||
const spikePort = 19619;
|
||||
|
||||
Future<bool> spikeUp() async {
|
||||
try {
|
||||
final socket = await Socket.connect(spikeServer, spikePort,
|
||||
timeout: const Duration(milliseconds: 300));
|
||||
socket.destroy();
|
||||
return true;
|
||||
} on SocketException {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
test("handles, error codes and async isolates work end to end", () async {
|
||||
final dir = await Directory.systemTemp.createTemp("native-binding");
|
||||
final client = FumiNative("${dir.path}/a.db");
|
||||
await client.open();
|
||||
client.setMaster(Uint8List.fromList(List.filled(32, 9)));
|
||||
|
||||
// The account public key crosses as base32, driven from a worker isolate.
|
||||
expect((await client.accountPk()).length, 52);
|
||||
expect(await client.accountAddress(), isNull);
|
||||
|
||||
// A pinned but dead host fails by code, never by prose: Unreachable (7).
|
||||
await client.pinServer("127.0.0.1", spikeServerKey);
|
||||
client.setMaster(Uint8List.fromList(List.filled(32, 9)));
|
||||
try {
|
||||
await client.register("nobody@127.0.0.1:19629");
|
||||
fail("register against a dead port must fail");
|
||||
} on NativeSmolException catch (err) {
|
||||
expect(err.code, smolUnreachable);
|
||||
expect(err.details["host"], "127.0.0.1");
|
||||
}
|
||||
|
||||
// Empty folders and contact lookups render, not crash.
|
||||
expect(await client.mail("inbox"), isEmpty);
|
||||
expect((await client.contact("ghost@example.org"))["key"], "");
|
||||
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("the backup round-trip crosses the ABI in both directions", () async {
|
||||
final dir = await Directory.systemTemp.createTemp("native-backup");
|
||||
final master = Uint8List.fromList(List.generate(32, (i) => i + 3));
|
||||
|
||||
final mine = FumiNative("${dir.path}/mine.db");
|
||||
await mine.open();
|
||||
mine.setMaster(master);
|
||||
await mine.pinServer("example.org", spikeServerKey);
|
||||
await mine.importContact(
|
||||
"smol://alice@example.org/ayb6y3mwr3cfkcmcaqbn3cgfi6xsrsoqkalykw5s7oqmwqcpnmsq");
|
||||
|
||||
final file = await mine.exportBackup();
|
||||
|
||||
final restored = FumiNative("${dir.path}/restored.db");
|
||||
await restored.open();
|
||||
restored.setMaster(master);
|
||||
final summary = jsonDecode(await restored.importBackup(file))
|
||||
as Map<String, dynamic>;
|
||||
expect(summary["contactsAdded"], 1);
|
||||
expect(summary["pinsAdded"], 1);
|
||||
expect(await restored.serverPin("example.org"), spikeServerKey);
|
||||
|
||||
await mine.close();
|
||||
await restored.close();
|
||||
});
|
||||
|
||||
test("register, send, fetch and describe against the spike bunshin",
|
||||
skip: await spikeUp()
|
||||
? false
|
||||
: "no bunshin on 127.0.0.1:19619", () async {
|
||||
final dir = await Directory.systemTemp.createTemp("native-e2e");
|
||||
// Random masters: bunshin refuses a key already bound under another
|
||||
// username, so identities must be fresh per run.
|
||||
final aliceMaster =
|
||||
Uint8List.fromList(List.generate(32, (i) => i * 7 + DateTime.now().microsecondsSinceEpoch % 251));
|
||||
final bobMaster = Uint8List.fromList(List.generate(32, (i) => i * 13 + 5));
|
||||
final run = DateTime.now().millisecondsSinceEpoch.toRadixString(36);
|
||||
|
||||
final alice = FumiNative("${dir.path}/alice.db");
|
||||
await alice.open();
|
||||
alice.setMaster(aliceMaster);
|
||||
await alice.pinServer(spikeServer, spikeServerKey);
|
||||
await alice.register("a$run@$spikeServer:$spikePort");
|
||||
expect(await alice.accountAddress(), "a$run@$spikeServer:$spikePort");
|
||||
|
||||
final bob = FumiNative("${dir.path}/bob.db");
|
||||
await bob.open();
|
||||
bob.setMaster(bobMaster);
|
||||
await bob.pinServer(spikeServer, spikeServerKey);
|
||||
await bob.register("b$run@$spikeServer:$spikePort");
|
||||
|
||||
final sent = await alice.send("b$run@$spikeServer:$spikePort",
|
||||
"binding: hello over the ABI",
|
||||
subject: "binding");
|
||||
expect(sent["change"], "new");
|
||||
|
||||
final summary = await bob.fetch();
|
||||
expect(summary["stored"], 1);
|
||||
// First contact lands in the requests tier (sec 5.8).
|
||||
final requests = await bob.mail("requests");
|
||||
expect(requests, hasLength(1));
|
||||
final described =
|
||||
await bob.describe(requests[0]["id"] as String);
|
||||
expect(described["text"], "binding: hello over the ABI");
|
||||
expect(described["subject"], "binding");
|
||||
|
||||
await alice.close();
|
||||
await bob.close();
|
||||
});
|
||||
}
|
||||
|
|
@ -4,6 +4,8 @@
|
|||
// router flow itself.
|
||||
|
||||
import "dart:io";
|
||||
import "dart:math";
|
||||
import "dart:typed_data";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
|
|
@ -12,11 +14,17 @@ import "package:hive_flutter/hive_flutter.dart";
|
|||
|
||||
import "package:smol_mail/data/providers/providers.dart";
|
||||
import "package:smol_mail/presentation/app_widget.dart";
|
||||
import "package:smol_mail/smol/address.dart";
|
||||
import "package:smol_mail/smol/client.dart";
|
||||
import "package:smol_mail/smol/crypto.dart";
|
||||
import "package:smol_mail/smol/errors.dart";
|
||||
import "package:smol_mail/smol/proto.dart";
|
||||
import "package:smol_mail/smol/store.dart";
|
||||
import "package:smol_mail/smol/ui.dart";
|
||||
|
||||
Uint8List randomBytes(int n) => Uint8List.fromList(List.generate(n, (_) => Random.secure().nextInt(256)));
|
||||
|
||||
// A syntactically valid server key for the stubbed pin step: the flow under
|
||||
// test is the UI routing, not the handshake.
|
||||
const fakePin = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
||||
|
||||
/// A [SmolClient] whose network operations complete instantly, so the test
|
||||
/// exercises the flow rather than the network. [restoreAndRecall] still
|
||||
|
|
@ -32,14 +40,14 @@ class StubClient extends SmolClient {
|
|||
throw SmolError("no pinned key for ${addr.host}");
|
||||
}
|
||||
store.restoreMaster(unhex(masterHex.trim()), 0);
|
||||
store.setAccount(addr);
|
||||
store.native.setAccount(addr.short);
|
||||
return addr;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SmolAddress> recallAccount(String addressText) async {
|
||||
final addr = parseAddress(addressText);
|
||||
store.setAccount(addr);
|
||||
store.native.setAccount(addr.short);
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
|
|
@ -53,11 +61,11 @@ void main() {
|
|||
final dir = await Directory.systemTemp.createTemp("smol-recall-flow");
|
||||
Hive.init(dir.path);
|
||||
storeA = await SmolStore.open(
|
||||
stateBox: "recall-a-state", mailBox: "recall-a-mail");
|
||||
dbPath: "${dir.path}/a.db", stateBox: "recall-a-state", readBox: "recall-a-read");
|
||||
storeB = await SmolStore.open(
|
||||
stateBox: "recall-b-state", mailBox: "recall-b-mail");
|
||||
dbPath: "${dir.path}/b.db", stateBox: "recall-b-state", readBox: "recall-b-read");
|
||||
storeC = await SmolStore.open(
|
||||
stateBox: "recall-c-state", mailBox: "recall-c-mail");
|
||||
dbPath: "${dir.path}/c.db", stateBox: "recall-c-state", readBox: "recall-c-read");
|
||||
});
|
||||
|
||||
Widget app(SmolStore store) => ProviderScope(
|
||||
|
|
@ -99,13 +107,14 @@ void main() {
|
|||
expect(find.text("Register a new address instead"), findsNothing);
|
||||
fields = find.byType(TextField);
|
||||
expect(fields, findsNWidgets(2)); // address (carried over), server key
|
||||
await tester.enterText(fields.at(1), b32encode(randomBytes(32)));
|
||||
await tester.enterText(fields.at(1), fakePin);
|
||||
await tester.tap(find.text("Pin and Recall"));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text("Inbox"), findsOneWidget);
|
||||
expect(store.master(), master);
|
||||
expect(store.account()!.user, "randogoth");
|
||||
expect(parseAddress(SmolClient(store).accountAddress()!.short).user,
|
||||
"randogoth");
|
||||
});
|
||||
|
||||
testWidgets("restore requires an address before it will submit",
|
||||
|
|
@ -141,7 +150,7 @@ void main() {
|
|||
await tester.tap(find.text("Create Identity"));
|
||||
await tester.pumpAndSettle();
|
||||
expect(store.master(), isNotNull);
|
||||
expect(store.account(), isNull);
|
||||
expect(SmolClient(store).accountAddress(), isNull);
|
||||
|
||||
// Simulate returning to onboarding later (e.g. a cold restart). Pumping
|
||||
// app(store) directly would just rebuild the existing OnboardingScreen
|
||||
|
|
@ -164,7 +173,7 @@ void main() {
|
|||
|
||||
// Lands on the recall-framed register step (no pin yet); pin it and finish.
|
||||
expect(find.byType(TextField), findsNWidgets(2));
|
||||
await tester.enterText(find.byType(TextField).at(1), b32encode(randomBytes(32)));
|
||||
await tester.enterText(find.byType(TextField).at(1), fakePin);
|
||||
await tester.tap(find.text("Pin and Recall"));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,317 +0,0 @@
|
|||
// Unit tests: lib/smol must reproduce test/vectors.json byte for byte (the
|
||||
// vectors are generated from the reference stack — PyNaCl, noiseprotocol — by
|
||||
// ../gsmol/test/gen_vectors.py), plus protocol-level checks for frontmatter,
|
||||
// addresses, rotation chains and response framing.
|
||||
// Run: devbox run test
|
||||
|
||||
import "dart:convert";
|
||||
import "dart:io";
|
||||
import "dart:typed_data";
|
||||
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import "package:smol_mail/smol/crypto.dart";
|
||||
import "package:smol_mail/smol/errors.dart";
|
||||
import "package:smol_mail/smol/noise.dart";
|
||||
import "package:smol_mail/smol/proto.dart";
|
||||
|
||||
final vectors =
|
||||
jsonDecode(File("test/vectors.json").readAsStringSync()) as Map<String, dynamic>;
|
||||
|
||||
Uint8List vhex(String text) => unhex(text);
|
||||
String vstring(dynamic value) => value as String;
|
||||
|
||||
void main() {
|
||||
test("hashes, HMAC/HKDF and AEAD match the reference vectors", () {
|
||||
for (final v in vectors["sha256"] as List) {
|
||||
final m = v as Map;
|
||||
expect(hex(sha256(vhex(vstring(m["in"])))), vstring(m["out"]));
|
||||
}
|
||||
for (final v in vectors["sha512"] as List) {
|
||||
final m = v as Map;
|
||||
expect(hex(sha512(vhex(vstring(m["in"])))), vstring(m["out"]));
|
||||
}
|
||||
for (final v in vectors["hkdf"] as List) {
|
||||
final m = v as Map;
|
||||
expect(
|
||||
hex(hkdfSha256(vhex(vstring(m["ikm"])), vhex(vstring(m["salt"])),
|
||||
vhex(vstring(m["info"])), m["len"] as int)),
|
||||
vstring(m["out"]));
|
||||
}
|
||||
for (final v in vectors["aead"] as List) {
|
||||
final m = v as Map;
|
||||
final key = vhex(vstring(m["key"]));
|
||||
final nonce = vhex(vstring(m["nonce"]));
|
||||
final aad = vhex(vstring(m["aad"]));
|
||||
final sealed = aeadEncrypt(
|
||||
key, nonce, vhex(vstring(m["plaintext"])), aad);
|
||||
expect(hex(sealed), vstring(m["sealed"]), reason: "aead-seal ${m["name"]}");
|
||||
expect(
|
||||
hex(aeadDecrypt(key, nonce, vhex(vstring(m["sealed"])), aad)),
|
||||
vstring(m["plaintext"]));
|
||||
expect(
|
||||
() => aeadDecrypt(
|
||||
key, nonce, Uint8List.fromList(vhex(vstring(m["sealed"])).sublist(0, vhex(vstring(m["sealed"])).length - 1)), aad),
|
||||
throwsA(isA<SmolError>()));
|
||||
}
|
||||
});
|
||||
|
||||
test("X25519 matches and rejects low-order points", () {
|
||||
final byName = <String, Map>{};
|
||||
for (final v in vectors["x25519"] as List) {
|
||||
final m = v as Map;
|
||||
byName[m["name"] as String] = m;
|
||||
}
|
||||
expect(hex(x25519Base(vhex(vstring(byName["alice"]!["priv"])))), byName["alice"]!["pub"]);
|
||||
expect(hex(x25519Base(vhex(vstring(byName["bob"]!["priv"])))), byName["bob"]!["pub"]);
|
||||
expect(
|
||||
hex(x25519(vhex(vstring(byName["alice"]!["priv"])), vhex(vstring(byName["bob"]!["pub"])))),
|
||||
byName["agree"]!["shared"]);
|
||||
for (final v in vectors["x25519"] as List) {
|
||||
final m = v as Map;
|
||||
if ((m["name"] as String).startsWith("low-order")) {
|
||||
expect(
|
||||
() => x25519(vhex(vstring(byName["alice"]!["priv"])), vhex(vstring(m["peer"]))),
|
||||
throwsA(isA<SmolError>()));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("Ed25519 signs and verifies like the reference stack", () {
|
||||
for (final v in vectors["ed25519"] as List) {
|
||||
final m = v as Map;
|
||||
final seed = vhex(vstring(m["seed"]));
|
||||
expect(hex(ed25519PublicKey(seed)), vstring(m["pub"]), reason: "ed25519-pub ${m["name"]}");
|
||||
final sig = ed25519Sign(seed, vhex(vstring(m["message"])));
|
||||
if (m["valid"] as bool) {
|
||||
expect(hex(sig), vstring(m["signature"]), reason: "ed25519-sign ${m["name"]}");
|
||||
expect(ed25519Verify(vhex(vstring(m["pub"])), vhex(vstring(m["message"])), sig), isTrue);
|
||||
expect(
|
||||
ed25519Verify(vhex(vstring(m["pub"])), vhex(vstring(m["message"])),
|
||||
vhex(vstring(m["signature"]))),
|
||||
isTrue,
|
||||
reason: "ed25519-verify-pynacl ${m["name"]}");
|
||||
} else {
|
||||
expect(
|
||||
ed25519Verify(vhex(vstring(m["pub"])), vhex(vstring(m["message"])),
|
||||
vhex(vstring(m["signature"]))),
|
||||
isFalse,
|
||||
reason: "ed25519-reject ${m["name"]}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("§2 conversions between the identity key and X25519", () {
|
||||
for (final v in vectors["ed_to_x25519"] as List) {
|
||||
final m = v as Map;
|
||||
expect(hex(ed25519SeedToX25519(vhex(vstring(m["seed"])))), vstring(m["x_priv"]));
|
||||
expect(hex(ed25519ToX25519(ed25519PublicKey(vhex(vstring(m["seed"]))))), vstring(m["x_pub"]));
|
||||
}
|
||||
});
|
||||
|
||||
test("§5 envelope seals, ids and unseals byte for byte", () {
|
||||
final v = vectors["envelope"] as Map;
|
||||
final sender = identityFromSeed(vhex(vstring(v["sender_seed"])));
|
||||
final recipient = identityFromSeed(vhex(vstring(v["recipient_seed"])));
|
||||
Uint8List sealWith(bool pad) => seal(sender, recipient.publicKey,
|
||||
vhex(vstring(v["body"])), v["time"] as int,
|
||||
SealOptions(esk: vhex(vstring(v["esk"])), pad: pad));
|
||||
|
||||
expect(hex(sealWith(false)), vstring(v["envelope"]));
|
||||
expect(hex(messageId(vhex(vstring(v["envelope"])))), vstring(v["id"]));
|
||||
final opened = unseal([recipient], vhex(vstring(v["envelope"])));
|
||||
expect(hex(opened.sender), hex(sender.publicKey));
|
||||
expect(opened.time, v["time"] as int);
|
||||
expect(hex(opened.body), vstring(v["body"]));
|
||||
expect(
|
||||
() => unseal([identityFromSeed(vhex(vstring(v["esk"])))],
|
||||
vhex(vstring(v["envelope"]))),
|
||||
throwsA(isA<SmolError>()));
|
||||
// padding round-trips and is ignored by the receiver (§5.3)
|
||||
final padded = sealWith(true);
|
||||
expect((padded.length - envelopeHeader - 16) % padTo, 0);
|
||||
expect(padded.length > vstring(v["envelope"]).length ~/ 2, isTrue);
|
||||
expect(hex(unseal([recipient], padded).body), vstring(v["body"]));
|
||||
});
|
||||
|
||||
test("Noise NX transcript matches the reference", () {
|
||||
final v = vectors["noise"] as Map;
|
||||
final nx = NxInitiator();
|
||||
expect(hex(nx.writeMessage1(vhex(vstring(v["initiator_eph_priv"])))), vstring(v["message1"]));
|
||||
final result = nx.readMessage2(vhex(vstring(v["message2"])));
|
||||
expect(hex(result.serverStatic), vstring(v["server_static_pub"]));
|
||||
expect(hex(result.handshakeHash), vstring(v["handshake_hash"]));
|
||||
final initiatorFrames = (v["initiator_frames"] as List).cast<Map>();
|
||||
final responderFrames = (v["responder_frames"] as List).cast<Map>();
|
||||
for (var i = 0; i < initiatorFrames.length; i++) {
|
||||
expect(hex(result.send.encrypt(vhex(vstring(initiatorFrames[i]["plaintext"])))),
|
||||
vstring(initiatorFrames[i]["sealed"]),
|
||||
reason: "noise-frame-i$i");
|
||||
}
|
||||
for (var i = 0; i < responderFrames.length; i++) {
|
||||
expect(hex(result.recv.decrypt(vhex(vstring(responderFrames[i]["sealed"])))),
|
||||
vstring(responderFrames[i]["plaintext"]),
|
||||
reason: "noise-frame-r$i");
|
||||
}
|
||||
// The responder direction must also produce identical ciphertexts (AEAD is
|
||||
// deterministic), so the recv cipher can be checked in both directions.
|
||||
final mirrored = NxInitiator();
|
||||
mirrored.writeMessage1(vhex(vstring(v["initiator_eph_priv"])));
|
||||
final mirror = mirrored.readMessage2(vhex(vstring(v["message2"])));
|
||||
expect(hex(mirror.recv.encrypt(vhex(vstring(responderFrames[0]["plaintext"])))),
|
||||
vstring(responderFrames[0]["sealed"]));
|
||||
});
|
||||
|
||||
test("frontmatter parses and fails closed (§5.5)", () {
|
||||
const inReplyTo =
|
||||
"4f2a1c9e8b7d6a5f3e2d1c0b9a8f7e6d5c4b3a291807f6e5d4c3b2a1908f7e6d5";
|
||||
final spec =
|
||||
"---\nSubject: Re: the thing\nIn-Reply-To: $inReplyTo\nX-Mood: cautiously optimistic\n---\nBody text starts here.";
|
||||
final parsed = parseFrontmatter(spec);
|
||||
expect(parsed.fields["subject"], "Re: the thing");
|
||||
expect(parsed.fields["in-reply-to"], inReplyTo);
|
||||
expect(parsed.body, "Body text starts here.");
|
||||
// a malformed line invalidates the whole block, which fails closed toward display
|
||||
expect(parseFrontmatter("---\nno colon here\n---\nrest").body,
|
||||
"---\nno colon here\n---\nrest");
|
||||
expect(parseFrontmatter("---\nSubject: x\nno end").body,
|
||||
"---\nSubject: x\nno end");
|
||||
expect(parseFrontmatter("---\nA: 1\nA: 2\n---\ntext").fields["a"], "1");
|
||||
// keys compare case-insensitively; the first occurrence wins (§5.5)
|
||||
expect(parseFrontmatter("---\nSubject: x\nsubject: y\n---\ntext").fields["subject"], "x");
|
||||
expect(buildFrontmatter(const {}, "---\nactual body"),
|
||||
"---\n---\n---\nactual body");
|
||||
expect(buildFrontmatter({"Subject": "hi"}, "there"), "---\nSubject: hi\n---\nthere");
|
||||
expect(buildFrontmatter(const {}, "plain"), "plain");
|
||||
expect(
|
||||
parseFrontmatter("---\n${"X: y\n" * 65}---\nbody").fields["subject"],
|
||||
isNull);
|
||||
});
|
||||
|
||||
test("addresses parse per §3 and round-trip through smol://", () {
|
||||
final a = parseAddress("Alice@Example.ORG:1961");
|
||||
expect(a.user, "alice");
|
||||
expect(a.port, 1961);
|
||||
expect(a.short, "alice@example.org"); // a default port is dropped
|
||||
expect(parseAddress("bob@host").port, defaultPort);
|
||||
final key = vhex(vstring((vectors["ed25519"] as List).cast<Map>().first["pub"]));
|
||||
final parsed = parseAddress(parseAddress("bob@h").uri(key));
|
||||
expect(parsed.identity, key);
|
||||
expect(parsed.user, "bob");
|
||||
expect(() => parseAddress("-bob@h"), throwsA(isA<SmolError>()));
|
||||
// §3: never two separators in a row
|
||||
expect(() => parseAddress("a..b@h"), throwsA(isA<SmolError>()));
|
||||
expect(parseAddress("a.b_c@h").user, "a.b_c");
|
||||
// §3: fingerprints are the first 20 base32 characters in groups of four
|
||||
final b32 = b32encode(key);
|
||||
expect(
|
||||
fingerprint(key),
|
||||
[
|
||||
b32.substring(0, 4), b32.substring(4, 8), b32.substring(8, 12),
|
||||
b32.substring(12, 16), b32.substring(16, 20)
|
||||
].join(" "));
|
||||
for (final n in [1, 2, 5, 32, 52, 64]) {
|
||||
final raw = randomBytes(n);
|
||||
expect(b32decode(b32encode(raw)), raw, reason: "b32[$n]");
|
||||
}
|
||||
});
|
||||
|
||||
test("rotation chains validate, break and oversize per §7", () {
|
||||
const username = "alice";
|
||||
final old = identityFromSeed(randomBytes(32));
|
||||
final mid = randomBytes(32);
|
||||
final fresh = randomBytes(32);
|
||||
final when = nowSeconds();
|
||||
final chain = [
|
||||
makeCert(username, old, mid, when),
|
||||
makeCert(username, identityFromSeed(mid), fresh, when),
|
||||
];
|
||||
expect(walkChain(username, old.publicKey, ed25519PublicKey(fresh), chain), isTrue);
|
||||
expect(walkChain(username, old.publicKey, old.publicKey, []), isTrue);
|
||||
expect(
|
||||
walkChain(username, old.publicKey, ed25519PublicKey(fresh), chain.sublist(1)),
|
||||
isFalse);
|
||||
final forged = List<Uint8List>.from(chain);
|
||||
forged[1] = makeCert(username, identityFromSeed(mid), randomBytes(32), when);
|
||||
expect(
|
||||
walkChain(username, old.publicKey, ed25519PublicKey(fresh), forged), isFalse);
|
||||
expect(
|
||||
walkChain(username, old.publicKey, ed25519PublicKey(fresh),
|
||||
List.filled(17, chain[0])),
|
||||
isFalse);
|
||||
// a chain signed for a different username must not validate (§7)
|
||||
expect(
|
||||
walkChain("bob", old.publicKey, ed25519PublicKey(fresh), chain), isFalse);
|
||||
expect(chain[0].length, certLen);
|
||||
});
|
||||
|
||||
test("response framing guards (§6.1)", () async {
|
||||
Session scripted(Uint8List frame) {
|
||||
// readNoise wants a u16 length prefix and at least 16 bytes of Noise
|
||||
// message; the frame is padded up to that floor.
|
||||
final message = Uint8List.fromList([
|
||||
...frame,
|
||||
...List.filled(frame.length < 16 ? 16 - frame.length : 0, 0),
|
||||
]);
|
||||
final session = Session(_ScriptedWire(concat([u16be(message.length), message])),
|
||||
_PassthroughCipher(), _PassthroughCipher());
|
||||
return session;
|
||||
}
|
||||
|
||||
Future<void> refuses(String name, Uint8List frame, int op) async {
|
||||
try {
|
||||
await scripted(frame).call(op);
|
||||
fail("$name: call resolved instead of throwing");
|
||||
} on TestFailure {
|
||||
rethrow;
|
||||
} catch (_) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
// The shortest legal response is a type byte and a status byte.
|
||||
await refuses("frame-too-short", concat([u32be(1), Uint8List.fromList([opFetch])]), opFetch);
|
||||
// A response reuses the request's type byte; a mismatch means the session
|
||||
// desynchronised, which must not be read as a status.
|
||||
await refuses("frame-op-mismatch",
|
||||
concat([u32be(2), Uint8List.fromList([opResolve, 0])]), opFetch);
|
||||
// The same frame with the right echo still passes, so the guard is not
|
||||
// simply rejecting everything.
|
||||
final okSession =
|
||||
scripted(concat([u32be(2), Uint8List.fromList([opFetch, 0])]));
|
||||
expect((await okSession.call(opFetch)).status, 0);
|
||||
});
|
||||
}
|
||||
|
||||
/// Serves a scripted response and ignores sends, so framing can be tested
|
||||
/// without a server.
|
||||
class _ScriptedWire implements Wire {
|
||||
final Uint8List _queue;
|
||||
int _pos = 0;
|
||||
|
||||
_ScriptedWire(this._queue);
|
||||
|
||||
@override
|
||||
void send(Uint8List bytes) {}
|
||||
|
||||
@override
|
||||
void close() {}
|
||||
|
||||
@override
|
||||
Future<Uint8List> readExact(int n) async {
|
||||
if (_pos + n > _queue.length) {
|
||||
throw const SmolError("script exhausted");
|
||||
}
|
||||
final out = Uint8List.fromList(_queue.sublist(_pos, _pos + n));
|
||||
_pos += n;
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
class _PassthroughCipher implements SessionCipher {
|
||||
@override
|
||||
Uint8List encrypt(Uint8List plaintext) => Uint8List.fromList(plaintext);
|
||||
|
||||
@override
|
||||
Uint8List decrypt(Uint8List sealed) => Uint8List.fromList(sealed);
|
||||
}
|
||||
|
|
@ -1,22 +1,22 @@
|
|||
// Store-level behavior: contact key history (§8), import binding, and the
|
||||
// export/import backup file.
|
||||
// Store-level behavior against the real native store: master lifecycle, read
|
||||
// marks, pins and the settings the app owns in Hive.
|
||||
|
||||
import "dart:convert";
|
||||
import "dart:io";
|
||||
import "dart:math";
|
||||
import "dart:typed_data";
|
||||
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:hive_flutter/hive_flutter.dart";
|
||||
|
||||
import "package:smol_mail/smol/client.dart";
|
||||
import "package:smol_mail/smol/crypto.dart";
|
||||
import "package:smol_mail/smol/errors.dart";
|
||||
import "package:smol_mail/smol/proto.dart";
|
||||
import "package:smol_mail/smol/store.dart";
|
||||
|
||||
// Each store gets its own boxes; Hive is a per-process singleton, so without
|
||||
// this the "exporting" and "importing" stores would be the same store.
|
||||
Future<SmolStore> freshStore(String tag) =>
|
||||
SmolStore.open(stateBox: "test-$tag-state", mailBox: "test-$tag-mail");
|
||||
Uint8List randomBytes(int n) =>
|
||||
Uint8List.fromList(List.generate(n, (_) => Random.secure().nextInt(256)));
|
||||
|
||||
Future<SmolStore> freshStore(String tag) => SmolStore.open(
|
||||
dbPath: "${Directory.systemTemp.createTempSync("smol-store-$tag").path}/store.db",
|
||||
stateBox: "$tag-state",
|
||||
readBox: "$tag-read");
|
||||
|
||||
void main() {
|
||||
setUpAll(() async {
|
||||
|
|
@ -25,182 +25,69 @@ void main() {
|
|||
Hive.init(dir.path);
|
||||
});
|
||||
|
||||
test("contact history keeps displaced keys, not re-saves", () async {
|
||||
final store = await freshStore("history");
|
||||
final a = randomBytes(32), b = randomBytes(32), c = randomBytes(32);
|
||||
test("master set, restore and rotations", () async {
|
||||
final store = await freshStore("master");
|
||||
expect(store.master(), isNull);
|
||||
expect(store.identity(), isNull);
|
||||
|
||||
store.saveContact("alice@example.org", a, true);
|
||||
expect(store.contact("alice@example.org")!.history, isEmpty);
|
||||
|
||||
store.saveContact("alice@example.org", b, false);
|
||||
final rotated = store.contact("alice@example.org")!;
|
||||
expect(rotated.key, b);
|
||||
expect(rotated.history.length, 1);
|
||||
expect(rotated.history.first.key, a);
|
||||
expect(rotated.history.first.until, greaterThan(0));
|
||||
|
||||
// Re-saving the same key is not a rotation and must not add an entry.
|
||||
store.saveContact("alice@example.org", b, true);
|
||||
expect(store.contact("alice@example.org")!.history.length, 1);
|
||||
|
||||
// A second displacement appends, oldest first.
|
||||
store.saveContact("alice@example.org", c, false);
|
||||
final history = store.contact("alice@example.org")!.history;
|
||||
expect(history.length, 2);
|
||||
expect(history[0].key, a);
|
||||
expect(history[1].key, b);
|
||||
expect(store.allContacts().single.$2.history.length, 2);
|
||||
});
|
||||
|
||||
test("importContact binds a smol:// address to the key it carries", () async {
|
||||
final store = await freshStore("import-contact");
|
||||
final client = SmolClient(store);
|
||||
final identity = identityFromSeed(randomBytes(32));
|
||||
client.importContact(
|
||||
"smol://bob@example.org/${b32encode(identity.publicKey)}");
|
||||
final saved = store.contact("bob@example.org")!;
|
||||
expect(saved.verified, isTrue);
|
||||
expect(saved.key, identity.publicKey);
|
||||
});
|
||||
|
||||
test("export never contains the master secret and round-trips through import",
|
||||
() async {
|
||||
final a = await freshStore("export");
|
||||
final b = await freshStore("round-trip");
|
||||
a.setMaster(randomBytes(32));
|
||||
a.pinServer("example.org", randomBytes(32));
|
||||
a.saveContact("alice@example.org", randomBytes(32), true);
|
||||
a.saveContact("alice@example.org", randomBytes(32), false); // history grows
|
||||
await a.storeMessage(
|
||||
"inbox", MailRecord("aa", randomBytes(64), receivedAt: 5));
|
||||
await a.storeMessage("sent",
|
||||
MailRecord("bb", randomBytes(64), recipient: "bob@example.org", sentAt: 6));
|
||||
|
||||
final data = a.exportData();
|
||||
expect(data["gsmolExport"], 2); // sealed to the identity's master, like gsmol
|
||||
expect(jsonEncode(data).contains(hex(a.master()!)), isFalse);
|
||||
|
||||
// v2 is sealed to the exporting identity's master — a restore-on-new-device
|
||||
// scenario, not a transfer to someone else's identity (see the test below).
|
||||
b.setMaster(a.master()!);
|
||||
final summary = await b.importData(data);
|
||||
expect(summary.mailAdded, 2);
|
||||
expect(summary.pinsAdded, 1);
|
||||
expect(summary.contactsAdded, 1);
|
||||
expect(b.serverPin("example.org"), a.serverPin("example.org"));
|
||||
expect(b.contact("alice@example.org")!.history.length, 1);
|
||||
expect(b.getMessage("inbox", "aa"), isNotNull);
|
||||
expect(b.getMessage("sent", "bb"), isNotNull);
|
||||
});
|
||||
|
||||
test("v2 import refuses a different identity's export", () async {
|
||||
final a = await freshStore("export-wrong-identity");
|
||||
final c = await freshStore("round-trip-wrong-identity");
|
||||
a.setMaster(randomBytes(32));
|
||||
a.pinServer("example.org", randomBytes(32));
|
||||
final data = a.exportData();
|
||||
|
||||
c.setMaster(randomBytes(32)); // a different master than a's
|
||||
expect(() => c.importData(data), throwsA(isA<SmolError>()));
|
||||
});
|
||||
|
||||
test("accept tokens: accept/block gate the sync set, tiers split the inbox view",
|
||||
() async {
|
||||
final store = await freshStore("accept-tokens");
|
||||
final master = randomBytes(32);
|
||||
store.setMaster(master);
|
||||
final alice = randomBytes(32);
|
||||
store.saveContact("alice@example.org", alice, true);
|
||||
expect(store.master(), master);
|
||||
// A fresh identity's public key is the native-derived one.
|
||||
expect(store.identity()!.publicKey.length, 52);
|
||||
expect(store.rotations(), 0);
|
||||
|
||||
// No one accepted yet: an empty set is already complete, so it may sync.
|
||||
var (sync, tokens) = store.tokenSet(master);
|
||||
expect(sync, 1);
|
||||
expect(tokens, isEmpty);
|
||||
|
||||
store.accept("alice@example.org", alice);
|
||||
(sync, tokens) = store.tokenSet(master);
|
||||
expect(sync, 1);
|
||||
expect(tokens, [tokenFor(master, alice)]);
|
||||
expect(store.accepted("alice@example.org")!.active, isTrue);
|
||||
|
||||
store.block("alice@example.org");
|
||||
expect(store.accepted("alice@example.org")!.active, isFalse);
|
||||
expect(store.tokenSet(master).$2, isEmpty);
|
||||
// Re-accepting keeps the identity frozen at the original acceptance,
|
||||
// so the token a correspondent already holds keeps working.
|
||||
store.accept("alice@example.org", randomBytes(32));
|
||||
expect(store.accepted("alice@example.org")!.identity, alice);
|
||||
|
||||
expect(() => store.block("bob@example.org"), throwsA(isA<SmolError>()));
|
||||
|
||||
// A restored master must not silently replace the server's set.
|
||||
store.setSyncOk(false);
|
||||
(sync, tokens) = store.tokenSet(master);
|
||||
expect(sync, 0);
|
||||
expect(tokens, isEmpty);
|
||||
|
||||
await store.storeIfNew(
|
||||
"inbox", MailRecord("aa", randomBytes(64), receivedAt: 1, tier: tierMain));
|
||||
await store.storeIfNew("inbox",
|
||||
MailRecord("bb", randomBytes(64), receivedAt: 2, tier: tierRequests));
|
||||
expect(store.listMessages("inbox").map((r) => r.id), ["aa"]);
|
||||
expect(store.listMessages("requests").map((r) => r.id), ["bb"]);
|
||||
expect(store.getMessage("requests", "bb"), isNotNull);
|
||||
final other = randomBytes(32);
|
||||
store.restoreMaster(other, 3);
|
||||
expect(store.master(), other);
|
||||
// The account handle was rebuilt at the restored rotation index.
|
||||
expect(store.identity()!.publicKey.length, 52);
|
||||
});
|
||||
|
||||
test("v1 legacy export still imports without an identity", () async {
|
||||
final store = await freshStore("import-legacy-v1");
|
||||
final summary = await store.importData({
|
||||
"gsmolExport": 1,
|
||||
"servers": {"example.org": b32encode(randomBytes(32))},
|
||||
});
|
||||
expect(summary.pinsAdded, 1);
|
||||
test("leave-on-server is a setting, not protocol state", () async {
|
||||
final store = await freshStore("leave");
|
||||
expect(store.leaveOnServer(), isFalse);
|
||||
await store.setLeaveOnServer(true);
|
||||
expect(store.leaveOnServer(), isTrue);
|
||||
await store.setLeaveOnServer(false);
|
||||
expect(store.leaveOnServer(), isFalse);
|
||||
});
|
||||
|
||||
test("import never overwrites a differing trust binding", () async {
|
||||
final store = await freshStore("conflict");
|
||||
final mine = randomBytes(32), other = randomBytes(32);
|
||||
store.pinServer("example.org", mine);
|
||||
store.saveContact("alice@example.org", mine, true);
|
||||
final summary = await store.importData({
|
||||
"gsmolExport": 1,
|
||||
"servers": {"example.org": b32encode(other)},
|
||||
"contacts": {
|
||||
"alice@example.org": {"key": b32encode(other), "verified": true},
|
||||
"bob@example.org": {"key": b32encode(randomBytes(32)), "verified": false},
|
||||
},
|
||||
});
|
||||
expect(summary.pinsConflicted, 1);
|
||||
expect(summary.pinsAdded, 0);
|
||||
expect(summary.contactsConflicted, 1);
|
||||
expect(summary.contactsAdded, 1);
|
||||
expect(store.serverPin("example.org"), mine);
|
||||
expect(store.contact("alice@example.org")!.key, mine);
|
||||
expect(store.contact("bob@example.org"), isNotNull);
|
||||
test("read marks drive the unread counts", () async {
|
||||
final store = await freshStore("read");
|
||||
expect(store.unreadCount(), 0);
|
||||
// Nothing is stored yet, so marking an id is harmless bookkeeping.
|
||||
store.markRead("ab" * 32);
|
||||
expect(store.isRead("ab" * 32), isTrue);
|
||||
});
|
||||
|
||||
test("import skips malformed entries and rejects wrong files", () async {
|
||||
final store = await freshStore("malformed");
|
||||
final summary = await store.importData({
|
||||
"gsmolExport": 1,
|
||||
"servers": {"bad": "notbase32!", "short": b32encode(randomBytes(8))},
|
||||
"contacts": {
|
||||
"x": {"key": "nope"},
|
||||
"y": "not a record",
|
||||
},
|
||||
"inbox": [
|
||||
{"id": "ok", "envelope": base64Encode(randomBytes(64)), "receivedAt": 1},
|
||||
{"id": "bad", "envelope": "!!!not base64!!!"},
|
||||
"not a record",
|
||||
],
|
||||
"sent": "not a list",
|
||||
});
|
||||
expect(summary.malformed, 7); // 2 pins, 2 contacts, 2 mail, 1 sent
|
||||
expect(summary.pinsAdded, 0);
|
||||
expect(summary.mailAdded, 1);
|
||||
expect(store.getMessage("inbox", "ok"), isNotNull);
|
||||
expect(store.getMessage("inbox", "bad"), isNull);
|
||||
test("pins save, list and unpin", () async {
|
||||
final store = await freshStore("pins");
|
||||
expect(store.allPins(), isEmpty);
|
||||
// A syntactically valid key: the system server's, a real 52-char form.
|
||||
const key = "lm2gqd7e5q67xq3isc5hx6jfj2q7a7xvq4l7trctxipudujovmgq";
|
||||
store.pinServer("example.org", key);
|
||||
expect(store.serverPin("example.org"), key);
|
||||
expect(store.allPins().length, 1);
|
||||
await store.unpinServer("example.org");
|
||||
expect(store.serverPin("example.org"), isNull);
|
||||
});
|
||||
|
||||
expect(() => store.importData({"nope": 1}), throwsA(isA<SmolError>()));
|
||||
test("wipe clears every secret and mark, overwriting the master", () async {
|
||||
final store = await freshStore("wipe");
|
||||
final master = randomBytes(32);
|
||||
store.setMaster(master);
|
||||
store.pinServer("example.org",
|
||||
"lm2gqd7e5q67xq3isc5hx6jfj2q7a7xvq4l7trctxipudujovmgq");
|
||||
store.markRead("cd" * 32);
|
||||
|
||||
await store.wipe();
|
||||
// The bytes the store owned are overwritten, not just dereferenced —
|
||||
// the caller's reference sees the zeros too.
|
||||
expect(master.every((b) => b == 0), isTrue);
|
||||
expect(store.master(), isNull);
|
||||
expect(store.identity(), isNull);
|
||||
expect(store.serverPin("example.org"), isNull);
|
||||
expect(store.isRead("cd" * 32), isFalse);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,226 +0,0 @@
|
|||
{
|
||||
"sha256": [
|
||||
{
|
||||
"in": "",
|
||||
"out": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"in": "616263",
|
||||
"out": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
|
||||
},
|
||||
{
|
||||
"in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
|
||||
"out": "fdeab9acf3710362bd2658cdc9a29e8f9c757fcf9811603a8c447cd1d9151108"
|
||||
},
|
||||
{
|
||||
"in": "736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c736d6f6c6d61696c",
|
||||
"out": "58a0adce483c517ae1b37025dbe3b534880972be4d9d10b78959a13fb8b13462"
|
||||
}
|
||||
],
|
||||
"sha512": [
|
||||
{
|
||||
"in": "",
|
||||
"out": "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"
|
||||
},
|
||||
{
|
||||
"in": "616263",
|
||||
"out": "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f"
|
||||
},
|
||||
{
|
||||
"in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
|
||||
"out": "ee4320ebaf3fdb4f2c832b137200c08e235e0fa7bbd0eb1740c7063ba8a0d151da77e003398e1714a955d475b05e3e950b639503b452ec185de4229bc4873949"
|
||||
}
|
||||
],
|
||||
"hkdf": [
|
||||
{
|
||||
"name": "rfc5869-1",
|
||||
"ikm": "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
|
||||
"salt": "000102030405060708090a0b0c",
|
||||
"info": "f0f1f2f3f4f5f6f7f8f9",
|
||||
"len": 42,
|
||||
"out": "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865"
|
||||
},
|
||||
{
|
||||
"name": "seal-shaped",
|
||||
"ikm": "61677265656d656e7420736861726564",
|
||||
"salt": "65706b726563697069656e74",
|
||||
"info": "736d6f6c6d61696c2f31207365616c",
|
||||
"len": 32,
|
||||
"out": "b9f729e45d7bab89598edbce35f3f5bb91d6f403a5ff85943a838defbfcd7a0c"
|
||||
}
|
||||
],
|
||||
"aead": [
|
||||
{
|
||||
"name": "empty",
|
||||
"key": "831386140c054287c460b9f6092d43a5bb6eb2d2c549b00fc3fce0e7cca1a19d",
|
||||
"nonce": "3edd69829394f0807df7b01d",
|
||||
"aad": "",
|
||||
"plaintext": "",
|
||||
"sealed": "941b286ea0ee86bb66236fc3c16b2e48"
|
||||
},
|
||||
{
|
||||
"name": "short",
|
||||
"key": "831386140c054287c460b9f6092d43a5bb6eb2d2c549b00fc3fce0e7cca1a19d",
|
||||
"nonce": "3edd69829394f0807df7b01d",
|
||||
"aad": "50515253c0c1c2c3c4c5c6c7",
|
||||
"plaintext": "68656c6c6f",
|
||||
"sealed": "7dbede6dd11ffa046f102dedea535912be561e3c29"
|
||||
},
|
||||
{
|
||||
"name": "multiline",
|
||||
"key": "831386140c054287c460b9f6092d43a5bb6eb2d2c549b00fc3fce0e7cca1a19d",
|
||||
"nonce": "3edd69829394f0807df7b01d",
|
||||
"aad": "50515253c0c1c2c3c4c5c6c7",
|
||||
"plaintext": "4c616469657320616e642047656e746c656d656e206f662074686520636c617373206f66202739393a206966204920636f756c64206f6666657220796f75206f6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73637265656e20776f756c642062652069742e",
|
||||
"sealed": "59bad668dbf00561dd86add05afe35b269f9997d57e46cee0e42fd69693c3df16b681f3905bbea4135cb379f4a0c730b5dbb3ba06d1231ce396660a16f8cadb8b422d745b932df3c1eed2df9636750551a0333b3d06877582f172f3ec2d437061143699bfc7258aa98e319f23a1f31f88fd15a24a97b46fd2e05c266d31ee96f5e24"
|
||||
}
|
||||
],
|
||||
"x25519": [
|
||||
{
|
||||
"name": "alice",
|
||||
"priv": "c63095403fea13cb2c43380599e669ebfb41ab184b04df28a143472e4283aa33",
|
||||
"pub": "712e68cefc13d0e1778226b7f7aaeb59042225e7a4def3816344da256e031664"
|
||||
},
|
||||
{
|
||||
"name": "bob",
|
||||
"priv": "33485a5b40b70730b3c3e468a8262543e5a4d9dc98de06399de66a4d579e02a7",
|
||||
"pub": "b8540c30e8fb348aed0abc8189cf8025ce09a9c5d74e0681c4e50f8d281da252"
|
||||
},
|
||||
{
|
||||
"name": "agree",
|
||||
"shared": "e6a3b1d2ae10c29b51519a71255af4bd20deee697c6ced1a44eadff428439e13"
|
||||
},
|
||||
{
|
||||
"name": "low-order-0",
|
||||
"peer": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"peer_rejected": true,
|
||||
"raised": true
|
||||
},
|
||||
{
|
||||
"name": "low-order-1",
|
||||
"peer": "0100000000000000000000000000000000000000000000000000000000000000",
|
||||
"peer_rejected": true,
|
||||
"raised": true
|
||||
}
|
||||
],
|
||||
"ed25519": [
|
||||
{
|
||||
"name": "vector-seed-a",
|
||||
"seed": "81712c4bef43282ffd12909479bece9da17d404bdbc629bec3a6fdf246f847fb",
|
||||
"pub": "7ce749eb2966b9b747543236ef5f61b46328a82f92677b82470439db065154e0",
|
||||
"message": "",
|
||||
"signature": "b20543ac2e0ba66c1b437de2afda7ca8ca6f9feb2af3e3e7ac3183e388d1786c9c027bfe549639140d0e45e7e850999b3fb992c7c003946c1c5aaeb09892cc0c",
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"name": "vector-seed-a",
|
||||
"seed": "81712c4bef43282ffd12909479bece9da17d404bdbc629bec3a6fdf246f847fb",
|
||||
"pub": "7ce749eb2966b9b747543236ef5f61b46328a82f92677b82470439db065154e0",
|
||||
"message": "736d6f6c6d61696c2f3120617574680000000000000000000000000000000000000000000000000000000000000000",
|
||||
"signature": "f7e74a0540e05843b52efb7dee4f3622ab8a88333142f6dd1d5386612f7f0f1410bd0b1f1ff09dea9419922b756920a4c1fb31a95eac3cc55a410d0d6f1dab03",
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"name": "vector-seed-a",
|
||||
"seed": "81712c4bef43282ffd12909479bece9da17d404bdbc629bec3a6fdf246f847fb",
|
||||
"pub": "7ce749eb2966b9b747543236ef5f61b46328a82f92677b82470439db065154e0",
|
||||
"message": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
|
||||
"signature": "e55b07b43bed9938bdf401d264226d22ea87f85867dc2f3a22edf129088e0a100dda4000c9c2075b946c12a645115911a041493bd3a6c6d6e3892233c7f3e209",
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"name": "vector-seed-a",
|
||||
"seed": "81712c4bef43282ffd12909479bece9da17d404bdbc629bec3a6fdf246f847fb",
|
||||
"pub": "7ce749eb2966b9b747543236ef5f61b46328a82f92677b82470439db065154e0",
|
||||
"message": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
|
||||
"signature": "e45b07b43bed9938bdf401d264226d22ea87f85867dc2f3a22edf129088e0a100dda4000c9c2075b946c12a645115911a041493bd3a6c6d6e3892233c7f3e209",
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"name": "vector-seed-b",
|
||||
"seed": "053a9de5a92ac01208edcd9f9d585ffa1462b60e895567d13a262ebbd5138b5d",
|
||||
"pub": "7068fc61a59adcb58e856393df939349389af09095fffdfb7374d07be183201e",
|
||||
"message": "",
|
||||
"signature": "2dfb4b1e65dfd4bf991ef50be88acddf2d59fe158daf2879bec5641ab80b1e0c542b9ea2bd9a6bc76f2020b76b14dc80ae9f68c62ea58dbd8f5b1990ff069e08",
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"name": "vector-seed-b",
|
||||
"seed": "053a9de5a92ac01208edcd9f9d585ffa1462b60e895567d13a262ebbd5138b5d",
|
||||
"pub": "7068fc61a59adcb58e856393df939349389af09095fffdfb7374d07be183201e",
|
||||
"message": "736d6f6c6d61696c2f3120617574680000000000000000000000000000000000000000000000000000000000000000",
|
||||
"signature": "f6d398cd25fec0ba882af13b85c6addcc2f546181fba84f8925e6e196b759897337a2291f4b73c40a062b0a2283ef096ded790154af58e9d4bd39c32b3db2f05",
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"name": "vector-seed-b",
|
||||
"seed": "053a9de5a92ac01208edcd9f9d585ffa1462b60e895567d13a262ebbd5138b5d",
|
||||
"pub": "7068fc61a59adcb58e856393df939349389af09095fffdfb7374d07be183201e",
|
||||
"message": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
|
||||
"signature": "24f8405160d7f8eca42d9bc5e703bc81e58082b34e5d0f28404e5a3518b500b46d48a9bd64c69f5dde38c978ae06c9ce5dfeabdfacd410753176ea11f171140e",
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"name": "vector-seed-b",
|
||||
"seed": "053a9de5a92ac01208edcd9f9d585ffa1462b60e895567d13a262ebbd5138b5d",
|
||||
"pub": "7068fc61a59adcb58e856393df939349389af09095fffdfb7374d07be183201e",
|
||||
"message": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
|
||||
"signature": "25f8405160d7f8eca42d9bc5e703bc81e58082b34e5d0f28404e5a3518b500b46d48a9bd64c69f5dde38c978ae06c9ce5dfeabdfacd410753176ea11f171140e",
|
||||
"valid": false
|
||||
}
|
||||
],
|
||||
"ed_to_x25519": [
|
||||
{
|
||||
"name": "vector-seed-a",
|
||||
"seed": "81712c4bef43282ffd12909479bece9da17d404bdbc629bec3a6fdf246f847fb",
|
||||
"x_priv": "30dc8ba3a49892d4b8a626cd371fd43bff4e5651c2a45f1be16e89d84fa89e68",
|
||||
"x_pub": "77bc3dae84c9b4085862725a21e0a366c09563d6da8f29c408ac2b6928937910"
|
||||
},
|
||||
{
|
||||
"name": "vector-seed-b",
|
||||
"seed": "053a9de5a92ac01208edcd9f9d585ffa1462b60e895567d13a262ebbd5138b5d",
|
||||
"x_priv": "b03deb6f9f4ab25d15471a355ff4ee23ea98f7d24695c500ed80b6af4b7b9963",
|
||||
"x_pub": "253d4b5b14df341a5dde486ddd588e292444118ed8eed1fe0738823b82e4243d"
|
||||
}
|
||||
],
|
||||
"envelope": {
|
||||
"sender_seed": "89c16df9e4352e706abe701928c230d8bd169cd31633bf4589c2d410cb3ae8cc",
|
||||
"recipient_seed": "6f845ccc435252d39dd08e964d219258e92c3d6c54af0376fa45d5e55eec0aaf",
|
||||
"esk": "c31fee505964c44b711cf354b431f9716c644a3dbf8c1d29c5e3cedf884d0a48",
|
||||
"body": "2d2d2d0a5375626a6563743a20766563746f720a2d2d2d0a68656c6c6f20626f620a",
|
||||
"time": 1730000000,
|
||||
"envelope": "534d4f4c01e048814b56d9b82e54fd367d3c980661313cc6a3d81a80315561fc0ac87f81184e49921528d72321669ae1b275229800d8786c1ecdd7d9331bcb2cc64dc27c0399b106b5061e9105d8adf82f6b7464930fa31cb08ba85dba4764ce14cf3ddc32599f43fea73ced76ffa3a3b768e5a127034f5e82d667687369c19f060e8eafb09da0e212683290f4e1a73ce072b94f053c9088652109d3639f7b9aa0d48619626ecefe33855aade6ab8bf9de14ebb7cf07eec0ef9c193ae4537c370183e0c8f7cd7230471b9d8e7389ac654e1b09dc9b0160c875270fdad218f0c9da735bab",
|
||||
"id": "b05d15a2ab5293164eda564de02a69019aa97895ff988f3d9fa2be5126516553",
|
||||
"unpadded_plaintext_len": 143
|
||||
},
|
||||
"noise": {
|
||||
"initiator_eph_priv": "af5f15993914cfd4e308856810d483ab25a383bfb2d708a0e15f80275f1fe6c2",
|
||||
"responder_eph_priv": "1c21927bb3e579efb69c937a2bf2e299ca1da9c83a62fb7f8ea1a375eb6f1c80",
|
||||
"server_static_priv": "c7b206a746c9b167da412a6b1a235869e316e9f08dbbc8478430b2998130f79e",
|
||||
"server_static_pub": "fa111eca8e853320f8e076674143fd4d1cd181bddeda7d9950a65922b9eafc32",
|
||||
"message1": "b8b73e6f132dab5659270e6496d2c575ae7daf45a0961ae8e19405a12ed1cc2e",
|
||||
"message2": "0b03ec78fd19cf7b8480881f33c6b4ca1094fac03c87860095c87c6737349c28256e498747bcf2018420d145c378e6605e63b217f92925ae5771dbe387b94cf9c995f7a3a8314fe2e671ca8fa1463e0642c1d7974f326737cadf630b8aac8ed9",
|
||||
"handshake_hash": "a039471479680a596a8a61b8827afb01853274b03de2870095edb9b7dd4e2c72",
|
||||
"assert_hash_equal": true,
|
||||
"initiator_frames": [
|
||||
{
|
||||
"plaintext": "70696e67",
|
||||
"sealed": "f1885096d9b9383b0bc38b08dff77c005d69f0f0"
|
||||
},
|
||||
{
|
||||
"plaintext": "7365636f6e64206672616d6520746f20746573742074686520636f756e746572",
|
||||
"sealed": "4a6481a2f7d0c909d9b321bc097e09a1929aeaf8647751f64d65b5e1f92abe960796dbf1c619bef48845aea257f2c73c"
|
||||
}
|
||||
],
|
||||
"responder_frames": [
|
||||
{
|
||||
"plaintext": "706f6e67",
|
||||
"sealed": "a7dda7fe3ef32435b3e72fda49714e9977318f0b"
|
||||
},
|
||||
{
|
||||
"plaintext": "787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878",
|
||||
"sealed": "9aa3af13c00a8a0cd81167b48a5443541e0c862297638e4b7e5c71196657bc565aad46c2d147b23f752ac48c29b69699209e252d3e840c13fc466dd0445515dec2d289902c7177a237f9256afe9508a45b64ea12cdb597a8c38b6ce0c43891ec772c21ca360980094268686642eae8e01ccf89650a427297dbab052aabfeb08adbb2087ae303dd168ceb3beffa63c8d3ccd1c5b2687c2a9c0d43eaab237fde648bf8b0f347b4952ff6da2212360b4a2a5b1316e9e776d82961c498e51f35a58c999f080ac1c12d7ac08f6ddd7addb70c37ea6bef42ed2c498362f4fd75a222068035a7f372c4f57e613427193ffd8ed40a2d0765ba62cbfd6cb5103165dc15be7ad2db723a4edc73cefe9f7fe5ff78a8ea06ecbc7c5135e5d810a1bd4e47f0cd4a1b6e8dd88b85236dd4b41296e00b7054139ee4fd4faa5876e01d62"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ void main() {
|
|||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final dir = await Directory.systemTemp.createTemp("smol-widget-test");
|
||||
Hive.init(dir.path);
|
||||
store = await SmolStore.open();
|
||||
store = await SmolStore.open(dbPath: "${dir.path}/widget.db");
|
||||
});
|
||||
|
||||
testWidgets("onboarding invites to create or restore an identity",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue