diff --git a/coinlib/bin/build_wasm.dart b/coinlib/bin/build_wasm.dart index b6c5b2f..765b17a 100644 --- a/coinlib/bin/build_wasm.dart +++ b/coinlib/bin/build_wasm.dart @@ -71,6 +71,7 @@ RUN ${WASI_SDK_PATH}/bin/wasm-ld \ --export secp256k1_xonly_pubkey_parse \ --export secp256k1_schnorrsig_sign32 \ --export secp256k1_schnorrsig_verify \ + --export secp256k1_ecdh \ # The secp256k1 library object files src/libsecp256k1_la-secp256k1.o \ src/libsecp256k1_precomputed_la-precomputed_ecmult.o \ diff --git a/coinlib/lib/src/crypto/ec_private_key.dart b/coinlib/lib/src/crypto/ec_private_key.dart index 2e2d5b2..2137aa3 100644 --- a/coinlib/lib/src/crypto/ec_private_key.dart +++ b/coinlib/lib/src/crypto/ec_private_key.dart @@ -46,6 +46,12 @@ class ECPrivateKey { return newScalar == null ? null : ECPrivateKey(newScalar, compressed: compressed); } + /// Generates a 32-byte secret that can be used as a symmetric key shared + /// between two EC keys. The secret can be generated by using one private key + /// and the other public key. + Uint8List diffieHellman(ECPublicKey pubkey) + => secp256k1.ecdh(_data, pubkey.data); + /// Get the private key where the public key always has an even Y-coordinate /// for any X-coordinate. This is used for Schnorr signatures. ECPrivateKey get xonly => pubkey.yIsEven diff --git a/coinlib/lib/src/secp256k1/secp256k1.ffi.g.dart b/coinlib/lib/src/secp256k1/secp256k1.ffi.g.dart index 4008c91..4202a39 100644 --- a/coinlib/lib/src/secp256k1/secp256k1.ffi.g.dart +++ b/coinlib/lib/src/secp256k1/secp256k1.ffi.g.dart @@ -2094,6 +2094,75 @@ class NativeSecp256k1 { ffi.Pointer, int, ffi.Pointer)>(); + + /// An implementation of SHA256 hash function that applies to compressed public key. + /// Populates the output parameter with 32 bytes. + late final ffi.Pointer + _secp256k1_ecdh_hash_function_sha256 = + _lookup( + 'secp256k1_ecdh_hash_function_sha256'); + + secp256k1_ecdh_hash_function get secp256k1_ecdh_hash_function_sha256 => + _secp256k1_ecdh_hash_function_sha256.value; + + /// A default ECDH hash function (currently equal to secp256k1_ecdh_hash_function_sha256). + /// Populates the output parameter with 32 bytes. + late final ffi.Pointer + _secp256k1_ecdh_hash_function_default = + _lookup( + 'secp256k1_ecdh_hash_function_default'); + + secp256k1_ecdh_hash_function get secp256k1_ecdh_hash_function_default => + _secp256k1_ecdh_hash_function_default.value; + + /// Compute an EC Diffie-Hellman secret in constant time + /// + /// Returns: 1: exponentiation was successful + /// 0: scalar was invalid (zero or overflow) or hashfp returned 0 + /// Args: ctx: pointer to a context object. + /// Out: output: pointer to an array to be filled by hashfp. + /// In: pubkey: a pointer to a secp256k1_pubkey containing an initialized public key. + /// seckey: a 32-byte scalar with which to multiply the point. + /// hashfp: pointer to a hash function. If NULL, + /// secp256k1_ecdh_hash_function_sha256 is used + /// (in which case, 32 bytes will be written to output). + /// data: arbitrary data pointer that is passed through to hashfp + /// (can be NULL for secp256k1_ecdh_hash_function_sha256). + int secp256k1_ecdh( + ffi.Pointer ctx, + ffi.Pointer output, + ffi.Pointer pubkey, + ffi.Pointer seckey, + secp256k1_ecdh_hash_function hashfp, + ffi.Pointer data, + ) { + return _secp256k1_ecdh( + ctx, + output, + pubkey, + seckey, + hashfp, + data, + ); + } + + late final _secp256k1_ecdhPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + secp256k1_ecdh_hash_function, + ffi.Pointer)>>('secp256k1_ecdh'); + late final _secp256k1_ecdh = _secp256k1_ecdhPtr.asFunction< + int Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + secp256k1_ecdh_hash_function, + ffi.Pointer)>(); } final class max_align_t extends ffi.Opaque {} @@ -2310,6 +2379,29 @@ final class secp256k1_schnorrsig_extraparams extends ffi.Struct { external ffi.Pointer ndata; } +/// A pointer to a function that hashes an EC point to obtain an ECDH secret +/// +/// Returns: 1 if the point was successfully hashed. +/// 0 will cause secp256k1_ecdh to fail and return 0. +/// Other return values are not allowed, and the behaviour of +/// secp256k1_ecdh is undefined for other return values. +/// Out: output: pointer to an array to be filled by the function +/// In: x32: pointer to a 32-byte x coordinate +/// y32: pointer to a 32-byte y coordinate +/// data: arbitrary data pointer that is passed through +typedef secp256k1_ecdh_hash_function + = ffi.Pointer>; +typedef secp256k1_ecdh_hash_functionFunction = ffi.Int Function( + ffi.Pointer output, + ffi.Pointer x32, + ffi.Pointer y32, + ffi.Pointer data); +typedef Dartsecp256k1_ecdh_hash_functionFunction = int Function( + ffi.Pointer output, + ffi.Pointer x32, + ffi.Pointer y32, + ffi.Pointer data); + const int NULL = 0; const int SECP256K1_FLAGS_TYPE_MASK = 255; diff --git a/coinlib/lib/src/secp256k1/secp256k1_base.dart b/coinlib/lib/src/secp256k1/secp256k1_base.dart index bdbcf9e..8fe1065 100644 --- a/coinlib/lib/src/secp256k1/secp256k1_base.dart +++ b/coinlib/lib/src/secp256k1/secp256k1_base.dart @@ -88,6 +88,9 @@ abstract class Secp256k1Base< late int Function( CtxPtr, HeapArrayPtr, HeapArrayPtr, int, XPubKeyPtr, ) extSchnorrVerify; + late int Function( + CtxPtr, HeapArrayPtr, PubKeyPtr, HeapArrayPtr, NullPtr, NullPtr, + ) extEcdh; // Heap arrays @@ -471,6 +474,27 @@ abstract class Secp256k1Base< } + /// Generates a Diffie-Hellman shared 32-byte hash between a private and + /// public key where the hash can be used as a shared key. + Uint8List ecdh(Uint8List privKey, Uint8List pubkey) { + _requireLoad(); + + key32Array.load(privKey); + _parsePubkeyIntoPtr(pubkey); + + if ( + extEcdh( + ctxPtr, hashArray.ptr, pubKeyPtr, key32Array.ptr, nullPtr, + nullPtr, + ) != 1 + ) { + throw Secp256k1Exception("Cannot generate ECDH shared key"); + } + + return hashArray.list.sublist(0); + + } + /// Specialised sub-classes should override to set the value behind the /// sizeTPtr set sizeT(int size); diff --git a/coinlib/lib/src/secp256k1/secp256k1_io.dart b/coinlib/lib/src/secp256k1/secp256k1_io.dart index 459ffe2..d0faaa6 100644 --- a/coinlib/lib/src/secp256k1/secp256k1_io.dart +++ b/coinlib/lib/src/secp256k1/secp256k1_io.dart @@ -83,6 +83,7 @@ class Secp256k1 extends Secp256k1Base< extXOnlyPubkeyParse = _lib.secp256k1_xonly_pubkey_parse; extSchnorrSign32 = _lib.secp256k1_schnorrsig_sign32; extSchnorrVerify = _lib.secp256k1_schnorrsig_verify; + extEcdh = _lib.secp256k1_ecdh; // Set heap arrays key32Array = HeapArrayFfi(Secp256k1Base.privkeySize); diff --git a/coinlib/lib/src/secp256k1/secp256k1_web.dart b/coinlib/lib/src/secp256k1/secp256k1_web.dart index e1572e0..ae50589 100644 --- a/coinlib/lib/src/secp256k1/secp256k1_web.dart +++ b/coinlib/lib/src/secp256k1/secp256k1_web.dart @@ -98,6 +98,7 @@ class Secp256k1 extends Secp256k1Base< extSchnorrVerify = inst.functions["secp256k1_schnorrsig_verify"] as IntFunc5; + extEcdh = inst.functions["secp256k1_ecdh"] as IntFunc6; // Local functions for loading purposes final contextCreate = inst.functions["secp256k1_context_create"] diff --git a/coinlib/pubspec.yaml b/coinlib/pubspec.yaml index c1427be..d269f64 100644 --- a/coinlib/pubspec.yaml +++ b/coinlib/pubspec.yaml @@ -31,3 +31,4 @@ ffigen: entry-points: - '../coinlib_flutter/src/secp256k1/include/secp256k1_recovery.h' - '../coinlib_flutter/src/secp256k1/include/secp256k1_schnorrsig.h' + - '../coinlib_flutter/src/secp256k1/include/secp256k1_ecdh.h' diff --git a/coinlib/test/crypto/ec_private_key_test.dart b/coinlib/test/crypto/ec_private_key_test.dart index af92a10..729397a 100644 --- a/coinlib/test/crypto/ec_private_key_test.dart +++ b/coinlib/test/crypto/ec_private_key_test.dart @@ -88,6 +88,18 @@ void main() { } }); + test(".diffieHellman()", () { + final k1 = keyPairVectors.first.privateObj; + final k2 = keyPairVectors.last.privateObj; + final s1 = k1.diffieHellman(k2.pubkey); + final s2 = k2.diffieHellman(k1.pubkey); + final other = k1.diffieHellman(k1.pubkey); + // Secrets match + expect(bytesToHex(s1), bytesToHex(s2)); + // Not the same as secret generated not by same key pairs + expect(bytesToHex(s1), isNot(bytesToHex(other))); + }); + test(".xonly", () { // Already even-y = 1