feat: import Chinese-localized Buzz source snapshot
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled

Signed-off-by: cls_宁波本机 <908705107@qq.com>
This commit is contained in:
2026-08-13 18:34:25 +08:00
parent 61c3fa1df9
commit 9dfa06ffee
3785 changed files with 1085458 additions and 2 deletions
+65
View File
@@ -0,0 +1,65 @@
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:pointycastle/ecc/curves/secp256k1.dart';
/// Compute secp256k1 ECDH shared secret (raw x-coordinate, unhashed).
///
/// Both [privateKeyHex] and [publicKeyHex] are 64-char lowercase hex strings
/// (32 bytes each). The public key is an x-only BIP-340 key.
///
/// Returns the 32-byte x-coordinate of the shared point.
Uint8List ecdhSharedSecret(String privateKeyHex, String publicKeyHex) {
final params = ECCurve_secp256k1();
// Parse private key scalar.
final d = BigInt.parse(privateKeyHex, radix: 16);
// Lift x-only public key to a full curve point (assume even y / prefix 02).
final xBytes = hexToBytes(publicKeyHex);
final compressed = Uint8List(33);
compressed[0] = 0x02; // even y
compressed.setRange(1, 33, xBytes);
final pubPoint = params.curve.decodePoint(compressed);
if (pubPoint == null) {
throw ArgumentError('Invalid public key: cannot decode point');
}
// Scalar multiplication: shared = d * Q
final shared = pubPoint * d;
if (shared == null || shared.isInfinity) {
throw StateError('ECDH produced point at infinity');
}
// Extract 32-byte x-coordinate (big-endian, zero-padded).
return bigIntTo32Bytes(shared.x!.toBigInteger()!);
}
/// Generate secure random bytes.
Uint8List secureRandomBytes(int length) {
final rng = math.Random.secure();
final data = Uint8List(length);
for (var i = 0; i < length; i++) {
data[i] = rng.nextInt(256);
}
return data;
}
// ── Hex helpers (shared across crypto modules) ──────────────────────────────
Uint8List hexToBytes(String hex) {
final result = Uint8List(hex.length ~/ 2);
for (var i = 0; i < result.length; i++) {
result[i] = int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16);
}
return result;
}
String bytesToHex(Uint8List bytes) {
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
}
Uint8List bigIntTo32Bytes(BigInt value) {
final hexStr = value.toRadixString(16).padLeft(64, '0');
return hexToBytes(hexStr);
}
+44
View File
@@ -0,0 +1,44 @@
import 'dart:typed_data';
import 'package:pointycastle/api.dart';
import 'package:pointycastle/digests/sha256.dart';
import 'package:pointycastle/macs/hmac.dart';
/// HKDF-SHA256 Extract: PRK = HMAC-SHA256(salt, ikm).
Uint8List hkdfExtract(Uint8List salt, Uint8List ikm) {
final hmac = HMac(SHA256Digest(), 64);
hmac.init(KeyParameter(salt.isEmpty ? Uint8List(32) : salt));
final out = Uint8List(32);
hmac.update(ikm, 0, ikm.length);
hmac.doFinal(out, 0);
return out;
}
/// HKDF-SHA256 Expand: OKM = HKDF-Expand(prk, info, length).
Uint8List hkdfExpand(Uint8List prk, Uint8List info, int length) {
final hmac = HMac(SHA256Digest(), 64);
hmac.init(KeyParameter(prk));
final n = (length + 31) ~/ 32; // ceil(length / hashLen)
final okm = Uint8List(n * 32);
var prev = Uint8List(0);
for (var i = 1; i <= n; i++) {
hmac.reset();
hmac.update(prev, 0, prev.length);
hmac.update(info, 0, info.length);
final counter = Uint8List.fromList([i]);
hmac.update(counter, 0, 1);
prev = Uint8List(32);
hmac.doFinal(prev, 0);
okm.setRange((i - 1) * 32, i * 32, prev);
}
return Uint8List.sublistView(okm, 0, length);
}
/// Convenience: HKDF-SHA256(ikm, salt, info, length).
Uint8List hkdf(Uint8List ikm, Uint8List salt, Uint8List info, int length) {
final prk = hkdfExtract(salt, ikm);
return hkdfExpand(prk, info, length);
}
+169
View File
@@ -0,0 +1,169 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:pointycastle/api.dart';
import 'package:pointycastle/digests/sha256.dart';
import 'package:pointycastle/macs/hmac.dart';
import 'package:pointycastle/stream/chacha7539.dart';
import 'ecdh.dart';
import 'hkdf.dart';
/// NIP-44 v2 conversation key derivation.
///
/// conversation_key = HKDF-Extract(salt="nip44-v2", ikm=ecdh_shared_secret)
Uint8List getConversationKey(String senderPrivHex, String receiverPubHex) {
final shared = ecdhSharedSecret(senderPrivHex, receiverPubHex);
final salt = utf8.encode('nip44-v2');
return hkdfExtract(Uint8List.fromList(salt), shared);
}
/// NIP-44 v2 encrypt.
///
/// Returns base64-encoded payload: version(1) || nonce(32) || ciphertext || mac(32)
String nip44Encrypt(Uint8List conversationKey, String plaintext) {
final plaintextBytes = utf8.encode(plaintext);
if (plaintextBytes.isEmpty || plaintextBytes.length > 65535) {
throw ArgumentError('Plaintext must be 1-65535 bytes');
}
final padded = _pad(Uint8List.fromList(plaintextBytes));
final nonce = secureRandomBytes(32);
// Derive message keys: chacha_key(32) + chacha_nonce(12) + hmac_key(32) = 76
final messageKeys = hkdfExpand(conversationKey, nonce, 76);
final chachaKey = Uint8List.sublistView(messageKeys, 0, 32);
final chachaNonce = Uint8List.sublistView(messageKeys, 32, 44);
final hmacKey = Uint8List.sublistView(messageKeys, 44, 76);
final ciphertext = _chacha20(chachaKey, chachaNonce, padded);
// HMAC-SHA256 over nonce || ciphertext.
final mac = _hmacSha256(hmacKey, _concat([nonce, ciphertext]));
// Assemble: version(0x02) || nonce(32) || ciphertext || mac(32)
final payload = _concat([
Uint8List.fromList([0x02]),
nonce,
ciphertext,
mac,
]);
return base64.encode(payload);
}
/// NIP-44 v2 decrypt.
///
/// Takes base64-encoded payload, returns plaintext string.
String nip44Decrypt(Uint8List conversationKey, String payloadBase64) {
final payload = base64.decode(payloadBase64);
// Minimum: version(1) + nonce(32) + min_ciphertext(32) + mac(32) = 97
if (payload.length < 97) {
throw FormatException('NIP-44 payload too short: ${payload.length}');
}
if (payload[0] != 0x02) {
throw FormatException('NIP-44 unsupported version: ${payload[0]}');
}
final nonce = Uint8List.sublistView(payload, 1, 33);
final ciphertext = Uint8List.sublistView(payload, 33, payload.length - 32);
final receivedMac = Uint8List.sublistView(payload, payload.length - 32);
final messageKeys = hkdfExpand(conversationKey, nonce, 76);
final chachaKey = Uint8List.sublistView(messageKeys, 0, 32);
final chachaNonce = Uint8List.sublistView(messageKeys, 32, 44);
final hmacKey = Uint8List.sublistView(messageKeys, 44, 76);
// Verify HMAC.
final expectedMac = _hmacSha256(hmacKey, _concat([nonce, ciphertext]));
if (!constantTimeEquals(receivedMac, expectedMac)) {
throw FormatException('NIP-44 HMAC verification failed');
}
final padded = _chacha20(chachaKey, chachaNonce, ciphertext);
return _unpad(padded);
}
// ── Padding ─────────────────────────────────────────────────────────────────
int _calcPaddedLen(int unpaddedLen) {
if (unpaddedLen <= 0) throw ArgumentError('Length must be > 0');
if (unpaddedLen <= 32) return 32;
final nextPower = 1 << (_log2(unpaddedLen - 1) + 1);
final chunk = nextPower <= 256 ? 32 : nextPower ~/ 8;
return chunk * (((unpaddedLen - 1) ~/ chunk) + 1);
}
int _log2(int x) {
var result = 0;
while ((1 << (result + 1)) <= x) {
result++;
}
return result;
}
Uint8List _pad(Uint8List plaintext) {
final len = plaintext.length;
final paddedLen = _calcPaddedLen(len);
// Result: [2-byte BE length][plaintext][zero padding]
final result = Uint8List(2 + paddedLen);
result[0] = (len >> 8) & 0xFF;
result[1] = len & 0xFF;
result.setRange(2, 2 + len, plaintext);
return result;
}
String _unpad(Uint8List padded) {
if (padded.length < 2) throw FormatException('Padded data too short');
final len = (padded[0] << 8) | padded[1];
if (len == 0 || 2 + len > padded.length) {
throw FormatException('Invalid padding length: $len');
}
return utf8.decode(padded.sublist(2, 2 + len));
}
// ── ChaCha20 (IETF, 12-byte nonce) ─────────────────────────────────────────
Uint8List _chacha20(Uint8List key, Uint8List nonce, Uint8List data) {
final cipher = ChaCha7539Engine();
cipher.init(false, ParametersWithIV(KeyParameter(key), nonce));
return cipher.process(data);
}
// ── HMAC-SHA256 ─────────────────────────────────────────────────────────────
Uint8List _hmacSha256(Uint8List key, Uint8List data) {
final hmac = HMac(SHA256Digest(), 64);
hmac.init(KeyParameter(key));
hmac.update(data, 0, data.length);
final out = Uint8List(32);
hmac.doFinal(out, 0);
return out;
}
// ── Helpers ─────────────────────────────────────────────────────────────────
Uint8List _concat(List<Uint8List> parts) {
final totalLen = parts.fold<int>(0, (sum, p) => sum + p.length);
final result = Uint8List(totalLen);
var offset = 0;
for (final part in parts) {
result.setRange(offset, offset + part.length, part);
offset += part.length;
}
return result;
}
/// Constant-time byte comparison to prevent timing side-channels.
bool constantTimeEquals(Uint8List a, Uint8List b) {
if (a.length != b.length) return false;
var result = 0;
for (var i = 0; i < a.length; i++) {
result |= a[i] ^ b[i];
}
return result == 0;
}
+74
View File
@@ -0,0 +1,74 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:nostr/nostr.dart' as nostr;
import 'package:pointycastle/digests/sha256.dart';
/// NIP-OA (Owner Attestation) — verify the `auth` tag on a kind:0 profile
/// that proves an owner key authorized an agent key.
///
/// Tag format: ["auth", "<owner-pubkey-hex>", "<conditions>", "<sig-hex>"]
/// Preimage: "nostr:agent-auth:" + agent_pubkey_hex + ":" + conditions
/// Signature: BIP-340 Schnorr over SHA256(preimage) by the owner key.
///
/// Mirrors `profile_valid_oa_owner_pubkey` in desktop/src-tauri: the tag is
/// verified against the profile event author, so a forged or stale marker
/// cannot turn a person into an agent.
///
/// Returns the owner pubkey (lowercase hex) for the first valid auth tag,
/// or null if none verifies.
String? verifiedOaOwnerPubkey(List<List<String>> tags, String agentPubkey) {
final agent = agentPubkey.toLowerCase();
for (final tag in tags) {
if (tag.length != 4 || tag[0] != 'auth') continue;
final owner = tag[1].toLowerCase();
final conditions = tag[2];
final sig = tag[3];
// Self-attestation is meaningless and rejected.
if (owner == agent) continue;
if (owner.length != 64 || sig.length != 128) continue;
if (!_validConditions(conditions)) continue;
final preimage = utf8.encode('nostr:agent-auth:$agent:$conditions');
final digest = SHA256Digest().process(Uint8List.fromList(preimage));
final message = digest
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join();
try {
if (nostr.Schnorr.verify(
publicKey: owner,
message: message,
signature: sig,
)) {
return owner;
}
} catch (_) {
// Malformed hex — treat as an invalid tag.
}
}
return null;
}
/// Validate the NIP-OA `conditions` string: empty, or `&`-joined clauses of
/// `kind=<n>`, `created_at<<n>`, or `created_at><n>` with canonical decimals.
bool _validConditions(String conditions) {
if (conditions.isEmpty) return true;
if (conditions.contains(RegExp(r'\s'))) return false;
for (final clause in conditions.split('&')) {
final match = RegExp(
r'^(?:kind=|created_at<|created_at>)(0|[1-9][0-9]*)$',
).firstMatch(clause);
if (match == null) return false;
final value = int.tryParse(match.group(1)!);
if (value == null || value > 4294967295) return false;
if (clause.startsWith('kind=') && value > 65535) return false;
}
return true;
}