Identity Simplification (#71)

* Contract interface for bindings

* Add errorType

* Remove Identity files

* Update Echo_bot

* update cbindings

* Add installation name

* Update tests

* bump libchat dep
This commit is contained in:
Jazz Turner-Baggs 2026-02-22 17:51:59 -08:00 committed by GitHub
parent 714d97029c
commit c2196c77ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 22 additions and 265 deletions

View File

@ -8,8 +8,7 @@ import content_types
proc main() {.async.} =
let waku = initWakuClient(DefaultConfig())
let ident = createIdentity("EchoBot")
var chatClient = newClient(waku, ident)
var chatClient = newClient(waku).get()
chatClient.onNewMessage(proc(convo: Conversation, msg: ReceivedMessage) {.async.} =
info "New Message: ", convoId = convo.id(), msg= msg

View File

@ -26,12 +26,9 @@ proc main() {.async.} =
var waku_saro = initWakuClient(DefaultConfig())
var waku_raya = initWakuClient(DefaultConfig())
let sKey = loadPrivateKeyFromBytes(@[45u8, 216, 160, 24, 19, 207, 193, 214, 98, 92, 153, 145, 222, 247, 101, 99, 96, 131, 149, 185, 33, 187, 229, 251, 100, 158, 20, 131, 111, 97, 181, 210]).get()
let rKey = loadPrivateKeyFromBytes(@[43u8, 12, 160, 51, 212, 90, 199, 160, 154, 164, 129, 229, 147, 69, 151, 17, 239, 51, 190, 33, 86, 164, 50, 105, 39, 250, 182, 116, 138, 132, 114, 234]).get()
# Create Clients
var saro = newClient(waku_saro, Identity(name: "saro", privateKey: sKey))
var raya = newClient(waku_raya, Identity(name: "raya", privateKey: rKey))
var saro = newClient(waku_saro).get()
var raya = newClient(waku_raya).get()
# Wire Saro Callbacks
saro.onNewMessage(proc(convo: Conversation, msg: ReceivedMessage) {.async, closure.} =

View File

@ -8,7 +8,6 @@ import ffi
import src/chat
import src/chat/delivery/waku_client
import src/chat/identity
import library/utils
logScope:
@ -52,14 +51,11 @@ proc createChatClient(
for peer in config["staticPeers"]:
wakuCfg.staticPeers.add(peer.getStr())
# Create identity
let identity = createIdentity(name)
# Create Waku client
let wakuClient = initWakuClient(wakuCfg)
# Create Chat client
let client = newClient(wakuClient, identity)
let client = ?newClient(wakuClient, installation_name = name)
# Register event handlers
client.onNewMessage(chatCallbacks.onNewMessage)

View File

@ -10,7 +10,6 @@ import stew/byteutils
import
src/chat/client,
src/chat/identity,
src/chat/delivery/waku_client,
library/declare_lib,
library/utils

View File

@ -1,12 +1,10 @@
import chat/[
client,
delivery/waku_client,
identity,
types
]
export client, identity, waku_client
export identity.`$`
export client, waku_client
#export specific frames need by applications
export MessageId

View File

@ -14,7 +14,6 @@ import # Foreign
import #local
delivery/waku_client,
errors,
identity,
types,
utils
@ -53,16 +52,9 @@ type
DeliveryAckCallback* = proc(conversation: Conversation,
msgId: MessageId): Future[void] {.async.}
type KeyEntry* = object
keyType: string
privateKey: PrivateKey
timestamp: int64
type ChatClient* = ref object
libchatCtx: LibChat
ds*: WakuClient
id: string
inboundQueue: QueueRef
isRunning: bool
@ -74,34 +66,38 @@ type ChatClient* = ref object
# Constructors
#################################################
proc newClient*(ds: WakuClient, ident: Identity): ChatClient {.raises: [IOError, ValueError].} =
## Creates new instance of a `ChatClient` with a given `WakuConfig`
## TODO: (P1) Currently the passed in Identity is not used. Libchat Generates one for every invocation.
proc newClient*(ds: WakuClient, ephemeral: bool = true, installation_name: string = "default"): Result[ChatClient, ErrorType] =
## Creates new instance of a `ChatClient` with a given `WakuConfig`.
## A new installation is created if no saved installation with `installation_name` is found
if not ephemeral:
return err("persistence is not currently supported")
try:
var q = QueueRef(queue: newAsyncQueue[ChatPayload](10))
var c = ChatClient(
libchatCtx: newConversationsContext(),
libchatCtx: newConversationsContext(installation_name),
ds: ds,
id: ident.getName(),
inboundQueue: q,
isRunning: false,
newMessageCallbacks: @[],
newConvoCallbacks: @[])
notice "Client started", client = c.id
notice "Client started"
result = c
result = ok(c)
except Exception as e:
error "newCLient", err = e.msg
result = err(e.msg)
#################################################
# Parameter Access
#################################################
proc getId*(client: ChatClient): string =
result = client.id
result = client.libchatCtx.getInstallationName()
proc listConversations*(client: ChatClient): seq[Conversation] =

View File

@ -1,61 +0,0 @@
# Reference: https://github.com/vacp2p/mix/blob/main/src/curve25519.nim
import results
import bearssl/rand
import libp2p/crypto/curve25519
const FieldElementSize* = Curve25519KeySize
type FieldElement* = Curve25519Key
# Convert bytes to FieldElement
proc bytesToFieldElement*(bytes: openArray[byte]): Result[FieldElement, string] =
if bytes.len != FieldElementSize:
return err("Field element size must be 32 bytes")
ok(intoCurve25519Key(bytes))
# Convert FieldElement to bytes
proc fieldElementToBytes*(fe: FieldElement): seq[byte] =
fe.getBytes()
# Generate a random FieldElement
proc generateRandomFieldElement*(): Result[FieldElement, string] =
let rng = HmacDrbgContext.new()
if rng.isNil:
return err("Failed to creat HmacDrbgContext with system randomness")
ok(Curve25519Key.random(rng[]))
# Generate a key pair (private key and public key are both FieldElements)
proc generateKeyPair*(): Result[tuple[privateKey, publicKey: FieldElement], string] =
let privateKeyRes = generateRandomFieldElement()
if privateKeyRes.isErr:
return err(privateKeyRes.error)
let privateKey = privateKeyRes.get()
let publicKey = public(privateKey)
ok((privateKey, publicKey))
# Multiply a given Curve25519 point with a set of scalars
proc multiplyPointWithScalars*(
point: FieldElement, scalars: openArray[FieldElement]
): FieldElement =
var res = point
for scalar in scalars:
Curve25519.mul(res, scalar)
res
# Multiply the Curve25519 base point with a set of scalars
proc multiplyBasePointWithScalars*(
scalars: openArray[FieldElement]
): Result[FieldElement, string] =
if scalars.len <= 0:
return err("Atleast one scalar must be provided")
var res: FieldElement = public(scalars[0]) # Use the predefined base point
for i in 1 ..< scalars.len:
Curve25519.mul(res, scalars[i]) # Multiply with each scalar
ok(res)
# Compare two FieldElements
proc compareFieldElements*(a, b: FieldElement): bool =
a == b

View File

@ -1,58 +0,0 @@
import results
import libp2p/crypto/curve25519
import bearssl/rand
import ../utils
type PrivateKey* = object
bytes: Curve25519Key
type PublicKey* = distinct Curve25519Key # TODO: define outside of ECDH
proc bytes*(key: PublicKey): array[Curve25519KeySize, byte] =
cast[array[Curve25519KeySize, byte]](key)
proc get_addr*(pubkey: PublicKey): string =
# TODO: Needs Spec
result = hash_func(pubkey.bytes().bytesToHex())
proc bytes*(key: PrivateKey): 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 loadPrivateKeyFromBytes*(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 loadPublicKeyFromBytes*(bytes: openArray[byte]): Result[PublicKey, string] =
if bytes.len != Curve25519KeySize:
return err("Public key size must be 32 bytes")
ok(PublicKey(intoCurve25519Key(bytes)))
proc getPublicKey*(privateKey: PrivateKey): PublicKey =
PublicKey( 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())

View File

@ -1,44 +0,0 @@
import crypto/ecdh
import results
import strformat
import utils
export PublicKey, PrivateKey, loadPrivateKeyFromBytes, loadPublicKeyFromBytes
type
Identity* = object
name*: string
privateKey*: PrivateKey # TODO: protect key exposure
#################################################
# Constructors
#################################################
proc createIdentity*(name: string): Identity =
let privKey = createRandomKey().get()
result = Identity(name: name, privateKey: privKey)
#################################################
# Parameter Access
#################################################
proc getPubkey*(self: Identity): PublicKey =
result = self.privateKey.getPublicKey()
proc getAddr*(self: Identity): string =
result = get_addr(self.getPubKey())
proc getName*(self: Identity): string =
result = self.name
proc toHex(key: PublicKey): string =
bytesToHex(key.bytes())
proc `$`*(key: PublicKey): string =
let byteStr = toHex(key)
fmt"{byteStr[0..3]}..{byteStr[^4 .. ^1]}"

View File

@ -1,2 +1,4 @@
type MessageId* = string
type Content* = seq[byte]
type ErrorType* = string
type PublicKey* = array[32, byte]

View File

@ -1,3 +1 @@
# import individual test suites
import ./test_curve25519

View File

@ -1,13 +1,13 @@
# Smoke test: validates that the binary links all dependencies at runtime.
# No networking, no start(), no message exchange — just instantiation.
import results
import ../src/chat
proc main() =
try:
let waku = initWakuClient(DefaultConfig())
let ident = createIdentity("SmokeTest")
var client = newClient(waku, ident)
var client = newClient(waku).get()
if client.isNil:
raise newException(CatchableError, "newClient returned nil")
let id = client.getId()

View File

@ -1,65 +0,0 @@
import results
import unittest
import ../src/chat/crypto/ecdh # TODO use config.nims
import ../src/chat/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..<N:
if parseHex(hexStr[i*2..i*2+1], result[i]) == 0:
raise newException(ValueError, "Invalid hex pair: " & hexStr[i*2..i*2+1])
suite "X25519":
test "Key Loading":
let a_priv = loadPrivateKeyFromBytes(hexToArray[32](ks7748_a_priv)).get()
let a_pub = a_priv.getPublicKey()
check bytesToHex(a_pub.bytes, lowercase = true) == ks7748_a_pub
check bytesToHex(a_pub.bytes, lowercase = true) != ks7748_b_pub
let b_priv = loadPrivateKeyFromBytes(hexToArray[32](ks7748_b_priv)).get()
let b_pub = b_priv.getPublicKey()
check bytesToHex(b_pub.bytes, lowercase = true) != ks7748_a_pub
check bytesToHex(b_pub.bytes, lowercase = true) == ks7748_b_pub
test "ECDH":
let a_priv = loadPrivateKeyFromBytes(hexToArray[32](ks7748_a_priv)).get()
let a_pub = a_priv.getPublicKey()
let b_priv = loadPrivateKeyFromBytes(hexToArray[32](ks7748_b_priv)).get()
let b_pub = b_priv.getPublicKey()
let sk1 = Dh(a_priv, b_pub)
if sk1.isErr:
raise newException(ValueError, "ECDH1 failed: " & sk1.error)
let sk2 = Dh(b_priv, a_pub)
if sk2.isErr:
raise newException(ValueError, "ECDH2 failed: " & sk2.error)
check bytesToHex(sk1.get(), lowercase = true) == ks7748_shared_key
check bytesToHex(sk2.get(), lowercase = true) == ks7748_shared_key
# Run with: nim c -r test_example.nim

2
vendor/libchat vendored

@ -1 +1 @@
Subproject commit a9ca4ffb7de90ea4cd269350c189c19fb78a2589
Subproject commit eb941387dfba1e110e55e47c4e1374c6a6239b0c