Extends the Block 2 spike to attack the highest-risk unbuilt piece (private messaging) and to prove the Q2 architecture recommendation with running code: - NostrConnection: one shared socket+key+sign+REQ/EOSE lifecycle. Refactored NostrOfferTransport onto it; added NostrMessageTransport — two thin contracts on ONE connection (the 'one connection, three interfaces' shape). - NIP-44 v2 encryption (secp256k1 ECDH -> ChaCha20 + HMAC, length-hiding pad). - NIP-17/NIP-59 gift-wrap: rumor(14) -> seal(13) -> wrap(1059, ephemeral key). Alice->Bob DM round-trips through the relay: encrypted, sender authenticated, and metadata-private (relay/eavesdropper sees only ciphertext, an ephemeral author and the recipient p-tag — Alice stays hidden). Findings updated: NIP-17 risk drops Medium-High -> Medium (hardening, not feasibility); next de-risking target is the web of trust. NIP-44 not yet vector-verified (noted). 20 tests green, analyzer clean, Block 1 untouched.
55 lines
1.8 KiB
Dart
55 lines
1.8 KiB
Dart
import 'dart:typed_data';
|
|
|
|
import 'package:crypto/crypto.dart';
|
|
|
|
/// Minimal, self-contained HKDF-SHA256 (RFC 5869), synchronous.
|
|
///
|
|
/// Spike-local so the derivation stays pure Dart and easy to reason about. In
|
|
/// production this would reuse `commons_core`'s `Hkdf` (the `cryptography`
|
|
/// package already ships one — see backup_box.dart), keyed by a domain string.
|
|
Uint8List hkdfSha256({
|
|
required List<int> ikm,
|
|
required String info,
|
|
List<int> salt = const [],
|
|
int length = 32,
|
|
}) {
|
|
// Extract.
|
|
final actualSalt = salt.isEmpty ? Uint8List(32) : salt; // HashLen zeros
|
|
final prk = Hmac(sha256, actualSalt).convert(ikm).bytes;
|
|
|
|
// Expand.
|
|
final infoBytes = info.codeUnits;
|
|
final out = <int>[];
|
|
var previous = <int>[];
|
|
var counter = 1;
|
|
while (out.length < length) {
|
|
final input = <int>[...previous, ...infoBytes, counter];
|
|
previous = Hmac(sha256, prk).convert(input).bytes;
|
|
out.addAll(previous);
|
|
counter++;
|
|
}
|
|
return Uint8List.fromList(out.sublist(0, length));
|
|
}
|
|
|
|
/// HKDF-Extract (RFC 5869 §2.2): `PRK = HMAC(salt, ikm)`. NIP-44 uses this
|
|
/// alone to turn the ECDH secret into the conversation key.
|
|
Uint8List hkdfExtract({required List<int> salt, required List<int> ikm}) =>
|
|
Uint8List.fromList(Hmac(sha256, salt).convert(ikm).bytes);
|
|
|
|
/// HKDF-Expand (RFC 5869 §2.3) with a *byte* info (NIP-44 uses the nonce as
|
|
/// info), producing [length] bytes.
|
|
Uint8List hkdfExpandBytes({
|
|
required List<int> prk,
|
|
required List<int> info,
|
|
required int length,
|
|
}) {
|
|
final out = <int>[];
|
|
var previous = <int>[];
|
|
var counter = 1;
|
|
while (out.length < length) {
|
|
previous = Hmac(sha256, prk).convert([...previous, ...info, counter]).bytes;
|
|
out.addAll(previous);
|
|
counter++;
|
|
}
|
|
return Uint8List.fromList(out.sublist(0, length));
|
|
}
|