From d0046eabf3e83b0c4ca638de3a6687a9100ff270 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Mon, 14 Jul 2025 19:39:21 -0700 Subject: [PATCH] Add ECDH functions --- src/crypto/ecdh.nim | 43 +++++++++++++++++++++++++ src/utils.nim | 8 ++++- tests/config.nims | 0 tests/test_curve25519.nim | 66 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 src/crypto/ecdh.nim create mode 100644 tests/config.nims create mode 100644 tests/test_curve25519.nim diff --git a/src/crypto/ecdh.nim b/src/crypto/ecdh.nim new file mode 100644 index 0000000..733e91e --- /dev/null +++ b/src/crypto/ecdh.nim @@ -0,0 +1,43 @@ +import results +import libp2p/crypto/curve25519 +import bearssl/rand + +type PrivateKey* = object + bytes: Curve25519Key + +type PublicKey* = object + bytes: Curve25519Key + + +proc bytes*(key: PrivateKey): Curve25519Key = + return key.bytes + +proc bytes*(key: PublicKey): Curve25519Key = + return key.bytes + +proc createRandomKey*(): Result[PrivateKey, string] = + let rng = HmacDrbgContext.new() + if rng.isNil: + return err("Failed to create HmacDrbgContext with system randomness") + ok(PrivateKey(bytes: Curve25519Key.random(rng[]))) + +proc loadKeyFromBytes*(bytes: openArray[byte]): Result[PrivateKey, string] = + if bytes.len != Curve25519KeySize: + return err("Private key size must be 32 bytes") + ok(PrivateKey(bytes: intoCurve25519Key(bytes))) + + +proc getPublicKey*(privateKey: PrivateKey): PublicKey = + PublicKey(bytes: public(privateKey.bytes)) + + +proc Dh*(privateKey: PrivateKey, publicKey: PublicKey): Result[seq[ + byte], string] = + + var outputKey = publicKey.bytes + try: + Curve25519.mul(outputKey, privateKey.bytes) + except CatchableError as e: + return err("Failed to compute shared secret: " & e.msg) + + return ok(outputKey.getBytes()) diff --git a/src/utils.nim b/src/utils.nim index 4ac5555..5f3e9be 100644 --- a/src/utils.nim +++ b/src/utils.nim @@ -2,6 +2,7 @@ import waku/waku_core import std/[random, times] import crypto import blake2 +import strutils proc getTimestamp*(): Timestamp = result = waku_core.getNanosecondTime(getTime().toUnix()) @@ -21,4 +22,9 @@ proc get_addr*(pubkey: SkPublicKey): string = result = hash_func(pubkey.toHexCompressed()) - +proc bytesToHex*(bytes: openarray[byte], lowercase: bool = false): string = + ## Convert bytes to hex string with case option + result = "" + for b in bytes: + let hex = b.toHex(2) + result.add(if lowercase: hex.toLower() else: hex) diff --git a/tests/config.nims b/tests/config.nims new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_curve25519.nim b/tests/test_curve25519.nim new file mode 100644 index 0000000..25a26a2 --- /dev/null +++ b/tests/test_curve25519.nim @@ -0,0 +1,66 @@ +# test_example.nim +import unittest +import ../src/crypto/ecdh # TODO use config.nims +import results +import ../src/utils + +# Key share test from RFC-7748: +const ks7748_a_priv = "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a" +const ks7748_a_pub = "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a" # Public key point (x co-ord) + +const ks7748_b_priv = "5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb" +const ks7748_b_pub = "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f" # Public key point (x co-ord)s + +const ks7748_shared_key = "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742" + +import parseutils + +proc hexToArray*[N: static[int]](hexStr: string): array[N, byte] = + ## Converts hex string to fixed-size byte array + if hexStr.len != N * 2: + raise newException(ValueError, + "Hex string length (" & $hexStr.len & ") doesn't match array size (" & $( + N*2) & ")") + + for i in 0..