mirror of
https://github.com/block/buzz.git
synced 2026-10-30 05:51:46 +00:00
`ecdhSharedSecret`, the input to every NIP-44 conversation key
(preferences, read state, reminders, themes, mutes, stars, sections,
sort, pairing, agent observer), used pointycastle's BigInt point
multiplication. That costs about 5.9 ms per call on an iPhone SE (2nd
gen, Profile) and is variable time in the secret scalar. It now calls
libsecp256k1's constant-time `secp256k1_ecdh` through `ecdhX` from #8654
(39.8 µs on the same device), returning the same raw x-coordinate NIP-44
requires. The decoded secret key bytes are zeroed after the call.
Invalid keys now throw `ArgumentError` instead of pointycastle's errors;
callers already treat any throw as a failed derivation.
### Validation
- Added tests: `ecdh_test.dart` compares the native result with the
previous pointycastle algorithm for 32 random key pairs, checks the
NIP-44 v2 `get_conversation_key` vector (secret 1, peer pubkey of secret
2), and checks rejection of a zero secret, an off-curve public key and
wrong lengths.
- Full mobile analyze and test suite pass after rebasing onto main at
da1190e2c, including the desktop nostr-rs NIP-44 interop vector.
- On a device: an iPhone 12 mini Profile build of the full native stack,
connected to the internal relay.
buzz-review-completed
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: george <973be878edd6d4f79692a3827f20a6f7fe0ed0362f269ff6b6ba8867033d7d8e@sprout-oss.stage.blox.sqprod.co>
51 lines
1.6 KiB
Dart
51 lines
1.6 KiB
Dart
import 'dart:math' as math;
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:buzz_secp256k1/buzz_secp256k1.dart' as secp256k1;
|
|
|
|
/// 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, lifted to even Y.
|
|
///
|
|
/// Returns the 32-byte x-coordinate of the shared point, computed in constant
|
|
/// time by libsecp256k1. Throws [ArgumentError] when either key is invalid.
|
|
Uint8List ecdhSharedSecret(String privateKeyHex, String publicKeyHex) {
|
|
if (privateKeyHex.length != 64 || publicKeyHex.length != 64) {
|
|
throw ArgumentError('ECDH keys must be 64 hex characters');
|
|
}
|
|
final secretKey = hexToBytes(privateKeyHex);
|
|
try {
|
|
return secp256k1.ecdhX(
|
|
secretKey: secretKey,
|
|
publicKey: hexToBytes(publicKeyHex),
|
|
);
|
|
} finally {
|
|
secretKey.fillRange(0, secretKey.length, 0);
|
|
}
|
|
}
|
|
|
|
/// 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();
|
|
}
|