feat(block2): commons_core social foundation — derivation, types, interfaces, WoT
First production slice of Block 2 (transport foundation), on the vetted pure-Dart nostr package (LGPL-3.0, AGPL-compatible, Flutter-free): - NostrKeyDerivation: seed -> secp256k1 Nostr identity via domain-separated HKDF (reuses cryptography's Hkdf like BackupBox), one-way, versioned. Wraps nostr's Keys for signing. Tested (reproducible, avalanche, one-way, integrates with IdentityService seed). - Agnostic value types: Offer/OfferType/OfferStatus/DiscoveryQuery/PublishResult, Certification. - Transport interfaces: OfferTransport, MessageTransport (+PrivateMessage), TrustTransport — the three sibling contracts. - WebOfTrust: pure Duniter membership rule (threshold + distance, fixpoint). Promoted from the spike; TDD (7 cases). No network yet (that's the next slice). commons_core: 50 tests green, analyzer clean. Block 1 APIs unchanged.
This commit is contained in:
parent
05a9d45a82
commit
edb480b21b
11 changed files with 528 additions and 0 deletions
|
|
@ -0,0 +1,79 @@
|
|||
import 'package:cryptography/cryptography.dart';
|
||||
import 'package:nostr/nostr.dart';
|
||||
|
||||
/// A Nostr identity (secp256k1 / BIP340) deterministically derived from the
|
||||
/// Duniter/Ğ1-style root seed. Keeps "one identity, one backup": the user backs
|
||||
/// up only the root seed (as the printable recovery QR); this key regenerates on
|
||||
/// demand.
|
||||
///
|
||||
/// Derivation is one-way by construction — the seed feeds HKDF-SHA256, whose PRF
|
||||
/// (HMAC-SHA256) is not invertible, so neither the private nor the public Nostr
|
||||
/// key can leak the root seed. Validated by the Block 2 spike; see
|
||||
/// docs/design/spike-block2-findings.md §1.
|
||||
class NostrIdentity {
|
||||
NostrIdentity._(this._keys);
|
||||
|
||||
final Keys _keys;
|
||||
|
||||
/// 32-byte secp256k1 secret, lower-case hex. Secret — never shared.
|
||||
String get privateKeyHex => _keys.secret;
|
||||
|
||||
/// 32-byte BIP340 x-only public key, lower-case hex — the Nostr identity.
|
||||
String get publicKeyHex => _keys.public;
|
||||
|
||||
/// NIP-19 `npub…` (shareable) and `nsec…` (secret) encodings.
|
||||
String get npub => _keys.npub;
|
||||
String get nsec => _keys.nsec;
|
||||
|
||||
/// The underlying `nostr` key pair, for signing/encryption.
|
||||
Keys get keys => _keys;
|
||||
}
|
||||
|
||||
/// Derives the Nostr identity from a root [seed] (the 32 bytes
|
||||
/// [IdentityService.generateRootSeed] produces).
|
||||
///
|
||||
/// Domain-separated and versioned so a future scheme can coexist with old
|
||||
/// backups — the same discipline `BackupBox` uses. Changing [derivationInfo]
|
||||
/// silently rotates every user's Nostr identity, so it is effectively part of
|
||||
/// `schemaVersion`.
|
||||
///
|
||||
/// Reduces to a valid secp256k1 scalar in `[1, n-1]` by rejection (bumps a
|
||||
/// counter in the HKDF info on the astronomically rare miss), so the result
|
||||
/// stays a pure function of the seed.
|
||||
class NostrKeyDerivation {
|
||||
static const derivationInfo = 'org.comunes.tane/nostr/secp256k1/v1';
|
||||
|
||||
static final _hkdf = Hkdf(hmac: Hmac.sha256(), outputLength: 32);
|
||||
|
||||
/// secp256k1 group order n.
|
||||
static final BigInt _n = BigInt.parse(
|
||||
'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141',
|
||||
radix: 16,
|
||||
);
|
||||
|
||||
static Future<NostrIdentity> deriveFromSeed(List<int> seed) async {
|
||||
var counter = 0;
|
||||
while (true) {
|
||||
final key = await _hkdf.deriveKey(
|
||||
secretKey: SecretKey(seed),
|
||||
info: '$derivationInfo/$counter'.codeUnits,
|
||||
nonce: const [],
|
||||
);
|
||||
final bytes = await key.extractBytes();
|
||||
final d = _bytesToBigInt(bytes);
|
||||
if (d != BigInt.zero && d < _n) {
|
||||
final privHex = d.toRadixString(16).padLeft(64, '0');
|
||||
return NostrIdentity._(Keys(privHex));
|
||||
}
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BigInt _bytesToBigInt(List<int> bytes) {
|
||||
var result = BigInt.zero;
|
||||
for (final b in bytes) {
|
||||
result = (result << 8) | BigInt.from(b);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue