import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; import 'package:crypto/crypto.dart'; import 'package:pointycastle/export.dart'; import 'hkdf.dart'; /// NIP-44 v2 payload encryption (secp256k1 ECDH → conversation key → ChaCha20 + /// HMAC-SHA256, with length-hiding padding). This is the crypto NIP-17 private /// DMs ride on. Spike-grade: follows the NIP-44 *construction* faithfully but is /// NOT cross-checked against the reference test vectors — see findings §4. Good /// enough to prove the messaging flow and architecture round-trip. class Nip44 { static final _params = ECCurve_secp256k1(); /// secp256k1 field prime. static final BigInt _p = BigInt.parse( 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F', radix: 16, ); static final _salt = ascii.encode('nip44-v2'); /// ECDH conversation key shared by [privHexA] and x-only [pubHexB]: /// `HKDF-Extract(salt="nip44-v2", ikm = (d·P).x)`. Symmetric: A→B and B→A /// yield the same key. static Uint8List conversationKey(String privHexA, String pubHexB) { final d = _bi(privHexA); final point = _liftX(_bi(pubHexB)); final shared = (point * d)!; final x = shared.x!.toBigInteger()!; return hkdfExtract(salt: _salt, ikm: _to32(x)); } /// Encrypts [plaintext] under [convKey]; returns base64 NIP-44 payload. static String encrypt(Uint8List convKey, String plaintext, {Uint8List? nonce}) { final n = nonce ?? _randomBytes(32); final keys = hkdfExpandBytes(prk: convKey, info: n, length: 76); final chachaKey = keys.sublist(0, 32); final chachaNonce = keys.sublist(32, 44); final hmacKey = keys.sublist(44, 76); final padded = _pad(utf8.encode(plaintext)); final ciphertext = _chacha20(chachaKey, chachaNonce, padded); final mac = Hmac(sha256, hmacKey).convert([...n, ...ciphertext]).bytes; return base64.encode([2, ...n, ...ciphertext, ...mac]); } /// Decrypts a base64 NIP-44 payload under [convKey]. Throws on version/MAC /// failure (so an eavesdropper with the wrong key cannot read it). static String decrypt(Uint8List convKey, String payload) { final raw = base64.decode(payload); if (raw[0] != 2) throw const FormatException('unsupported nip44 version'); final n = raw.sublist(1, 33); final ciphertext = raw.sublist(33, raw.length - 32); final mac = raw.sublist(raw.length - 32); final keys = hkdfExpandBytes(prk: convKey, info: n, length: 76); final chachaKey = keys.sublist(0, 32); final chachaNonce = keys.sublist(32, 44); final hmacKey = keys.sublist(44, 76); final expected = Hmac(sha256, hmacKey).convert([...n, ...ciphertext]).bytes; if (!_constantTimeEquals(expected, mac)) { throw const FormatException('nip44 MAC mismatch (wrong key or tampered)'); } final padded = _chacha20(chachaKey, chachaNonce, Uint8List.fromList(ciphertext)); final len = (padded[0] << 8) | padded[1]; return utf8.decode(padded.sublist(2, 2 + len)); } // --- padding (NIP-44 length hiding) --- static Uint8List _pad(List plaintext) { final unpadded = plaintext.length; final paddedLen = _calcPaddedLen(unpadded); final buf = Uint8List(2 + paddedLen); buf[0] = (unpadded >> 8) & 0xff; buf[1] = unpadded & 0xff; buf.setRange(2, 2 + unpadded, plaintext); return buf; } static int _calcPaddedLen(int len) { if (len <= 32) return 32; final nextPower = 1 << ((len - 1).bitLength); final chunk = nextPower <= 256 ? 32 : nextPower ~/ 8; return chunk * ((len - 1) ~/ chunk + 1); } // --- primitives --- static Uint8List _chacha20(List key, List nonce, Uint8List input) { final engine = ChaCha7539Engine() ..init( true, ParametersWithIV( KeyParameter(Uint8List.fromList(key)), Uint8List.fromList(nonce), ), ); final out = Uint8List(input.length); engine.processBytes(input, 0, input.length, out, 0); return out; } /// BIP340 lift_x: reconstruct the curve point with even y from x-only [x]. static ECPoint _liftX(BigInt x) { final ySq = (x.modPow(BigInt.from(3), _p) + BigInt.from(7)) % _p; var y = ySq.modPow((_p + BigInt.one) ~/ BigInt.from(4), _p); if (y.isOdd) y = _p - y; return _params.curve.createPoint(x, y); } static BigInt _bi(String hex) => BigInt.parse(hex, radix: 16); static Uint8List _to32(BigInt v) { final out = Uint8List(32); var x = v; for (var i = 31; i >= 0; i--) { out[i] = (x & BigInt.from(0xff)).toInt(); x = x >> 8; } return out; } static bool _constantTimeEquals(List a, List b) { if (a.length != b.length) return false; var diff = 0; for (var i = 0; i < a.length; i++) { diff |= a[i] ^ b[i]; } return diff == 0; } static final _rng = Random.secure(); static Uint8List _randomBytes(int n) => Uint8List.fromList(List.generate(n, (_) => _rng.nextInt(256))); }