From 3bfba7cf250eb3a79e10c966f6cf15f8ed3f5c0e Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:16:16 -0800 Subject: [PATCH 1/8] Libchat library Integration (#55) * chore: add smoke test and redesign CI workflow (#62) Add a smoke test that validates the binary links all dependencies at runtime by instantiating a client without networking. Redesign CI into separate build and test jobs, with test gated on build. * Add libchat module * Add Context * Add libchat * Update to latest libchat * Remove stale files * Bump to latest Libchat * Update imports * Update client * Update library to work with Libchat * Fix examples * Remove Tui Examples - Replace with logos-core * Add Indentity Todo * fix: add `build-libchat` as dependency for examples, tests, and library (#59) The Rust liblogos_chat.so was not being built automatically, causing runtime failures when loading the shared library. * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add Todo for Sender data * Updated error log --------- Co-authored-by: osmaczko <33099791+osmaczko@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .gitmodules | 3 + Makefile | 15 +- config.nims | 3 + examples/bot_echo.nim | 2 +- examples/pingpong.nim | 37 +- examples/tui.nim | 7 - examples/tui/layout.nim | 78 ---- examples/tui/persistence.nim | 135 ------ examples/tui/tui.nim | 584 -------------------------- examples/tui/utils.nim | 53 --- nim_chat_poc.nimble | 1 + src/chat.nim | 8 +- src/chat/client.nim | 196 ++++----- src/chat/conversation_store.nim | 16 - src/chat/conversations.nim | 5 - src/chat/conversations/convo_impl.nim | 28 -- src/chat/conversations/convo_type.nim | 29 -- src/chat/conversations/message.nim | 11 - src/chat/conversations/private_v1.nim | 282 ------------- src/chat/crypto.nim | 35 -- src/chat/delivery/waku_client.nim | 7 +- src/chat/errors.nim | 1 + src/chat/identity.nim | 15 +- src/chat/inbox.nim | 171 -------- src/chat/links.nim | 58 --- src/chat/proto_types.nim | 104 ----- src/chat/utils.nim | 15 - src/content_types/all.nim | 2 - vendor/libchat | 1 + 29 files changed, 143 insertions(+), 1759 deletions(-) delete mode 100644 examples/tui.nim delete mode 100644 examples/tui/layout.nim delete mode 100644 examples/tui/persistence.nim delete mode 100644 examples/tui/tui.nim delete mode 100644 examples/tui/utils.nim delete mode 100644 src/chat/conversation_store.nim delete mode 100644 src/chat/conversations.nim delete mode 100644 src/chat/conversations/convo_impl.nim delete mode 100644 src/chat/conversations/convo_type.nim delete mode 100644 src/chat/conversations/message.nim delete mode 100644 src/chat/conversations/private_v1.nim delete mode 100644 src/chat/crypto.nim delete mode 100644 src/chat/inbox.nim delete mode 100644 src/chat/links.nim delete mode 100644 src/chat/proto_types.nim create mode 160000 vendor/libchat diff --git a/.gitmodules b/.gitmodules index b35d845..04c430b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -46,3 +46,6 @@ url = https://github.com/logos-messaging/nim-ffi.git ignore = untracked branch = master +[submodule "vendor/libchat"] + path = vendor/libchat + url = https://github.com/logos-messaging/libchat.git diff --git a/Makefile b/Makefile index d243276..2c3f724 100644 --- a/Makefile +++ b/Makefile @@ -50,6 +50,7 @@ update: | update-common clean: rm -rf build + cd vendor/libchat && cargo clean # must be included after the default target -include $(BUILD_SYSTEM_DIR)/makefiles/targets.mk @@ -86,9 +87,15 @@ build-waku-nat: @echo "Start building waku nat-libs" $(MAKE) -C vendor/nwaku nat-libs @echo "Completed building nat-libs" - + +.PHONY: build-libchat +build-libchat: + @echo "Start building libchat" + cd vendor/libchat && cargo build --release + @echo "Completed building libchat" + .PHONY: tests -tests: | build-waku-librln build-waku-nat nim_chat_poc.nims +tests: | build-waku-librln build-waku-nat build-libchat nim_chat_poc.nims echo -e $(BUILD_MSG) "build/$@" && \ $(ENV_SCRIPT) nim tests $(NIM_PARAMS) nim_chat_poc.nims @@ -98,7 +105,7 @@ tests: | build-waku-librln build-waku-nat nim_chat_poc.nims ########## # Ensure there is a nimble task with a name that matches the target -tui bot_echo pingpong: | build-waku-librln build-waku-nat nim_chat_poc.nims +tui bot_echo pingpong: | build-waku-librln build-waku-nat build-libchat nim_chat_poc.nims echo -e $(BUILD_MSG) "build/$@" && \ $(ENV_SCRIPT) nim $@ $(NIM_PARAMS) --path:src nim_chat_poc.nims @@ -118,7 +125,7 @@ endif LIBLOGOSCHAT := build/liblogoschat.$(LIBLOGOSCHAT_EXT) .PHONY: liblogoschat -liblogoschat: | build-waku-librln build-waku-nat nim_chat_poc.nims +liblogoschat: | build-waku-librln build-waku-nat build-libchat nim_chat_poc.nims echo -e $(BUILD_MSG) "$(LIBLOGOSCHAT)" && \ $(ENV_SCRIPT) nim liblogoschat $(NIM_PARAMS) --path:src nim_chat_poc.nims && \ echo -e "\n\x1B[92mLibrary built successfully:\x1B[39m" && \ diff --git a/config.nims b/config.nims index 27a5d0b..c0b0096 100644 --- a/config.nims +++ b/config.nims @@ -5,3 +5,6 @@ for dir in walkDir(thisDir() / "vendor"): if dir.kind == pcDir: switch("path", dir.path) switch("path", dir.path / "src") + +switch("path", thisDir() / "vendor/libchat/nim-bindings") +switch("path", thisDir() / "vendor/libchat/nim-bindings/src") \ No newline at end of file diff --git a/examples/bot_echo.nim b/examples/bot_echo.nim index 0e78481..fd2a5d2 100644 --- a/examples/bot_echo.nim +++ b/examples/bot_echo.nim @@ -24,7 +24,7 @@ proc main() {.async.} = await chatClient.start() info "EchoBot started" - info "Invite", link=chatClient.createIntroBundle().toLink() + info "Invite", link=chatClient.createIntroBundle() when isMainModule: waitFor main() diff --git a/examples/pingpong.nim b/examples/pingpong.nim index 8f07f19..5dbe00d 100644 --- a/examples/pingpong.nim +++ b/examples/pingpong.nim @@ -5,8 +5,6 @@ import strformat import chat import content_types -# TEsting -import ../src/chat/crypto proc getContent(content: ContentFrame): string = @@ -35,39 +33,38 @@ proc main() {.async.} = var saro = newClient(waku_saro, Identity(name: "saro", privateKey: sKey)) var raya = newClient(waku_raya, Identity(name: "raya", privateKey: rKey)) - var ri = 0 - # Wire Callbacks - saro.onNewMessage(proc(convo: Conversation, msg: ReceivedMessage) {.async.} = + # Wire Saro Callbacks + saro.onNewMessage(proc(convo: Conversation, msg: ReceivedMessage) {.async, closure.} = let contentFrame = msg.content.fromBytes() - echo " Saro <------ :: " & getContent(contentFrame) - await sleepAsync(5000.milliseconds) + notice " Saro <------ ", content = getContent(contentFrame) + await sleepAsync(1000.milliseconds) discard await convo.sendMessage(initTextFrame("Ping").toContentFrame().toBytes()) - - ) + ) saro.onDeliveryAck(proc(convo: Conversation, msgId: string) {.async.} = - echo " Saro -- Read Receipt for " & msgId + notice " Saro -- Read Receipt for ", msgId= msgId ) + # Wire Raya Callbacks + var i = 0 + raya.onNewConversation(proc(convo: Conversation) {.async.} = + notice " ------> Raya :: New Conversation: ", id = convo.id() + discard await convo.sendMessage(initTextFrame("Hello").toContentFrame().toBytes()) + ) raya.onNewMessage(proc(convo: Conversation,msg: ReceivedMessage) {.async.} = let contentFrame = msg.content.fromBytes() - echo fmt" ------> Raya :: from:{msg.sender} " & getContent(contentFrame) + notice " ------> Raya :: from: ", content= getContent(contentFrame) await sleepAsync(500.milliseconds) - discard await convo.sendMessage(initTextFrame("Pong" & $ri).toContentFrame().toBytes()) + discard await convo.sendMessage(initTextFrame("Pong" & $i).toContentFrame().toBytes()) await sleepAsync(800.milliseconds) - discard await convo.sendMessage(initTextFrame("Pong" & $ri).toContentFrame().toBytes()) - await sleepAsync(500.milliseconds) - discard await convo.sendMessage(initTextFrame("Pong" & $ri).toContentFrame().toBytes()) - inc ri + discard await convo.sendMessage(initTextFrame("Pang" & $i).toContentFrame().toBytes()) + inc i ) - raya.onNewConversation(proc(convo: Conversation) {.async.} = - echo " ------> Raya :: New Conversation: " & convo.id() - discard await convo.sendMessage(initTextFrame("Hello").toContentFrame().toBytes()) - ) + raya.onDeliveryAck(proc(convo: Conversation, msgId: string) {.async.} = echo " raya -- Read Receipt for " & msgId ) diff --git a/examples/tui.nim b/examples/tui.nim deleted file mode 100644 index 6120b20..0000000 --- a/examples/tui.nim +++ /dev/null @@ -1,7 +0,0 @@ -import chronos - -import tui/tui - - -when isMainModule: - waitFor main() diff --git a/examples/tui/layout.nim b/examples/tui/layout.nim deleted file mode 100644 index b72381a..0000000 --- a/examples/tui/layout.nim +++ /dev/null @@ -1,78 +0,0 @@ -import illwill - - -type - Pane* = object - xStart*: int - yStart*: int - width*: int - height*: int - -proc getPanes*(): seq[Pane] = - - let statusBarHeight = 6 - let convoWidth = 40 - let inboxHeight = 5 - let footerHeight = 14 - let modalXOffset = 10 - let modalHeight = 10 - - result = @[] - let statusBar = Pane( - xStart: 0, - yStart: 0, - width: terminalWidth(), - height: statusBarHeight - ) - result.add(statusBar) - - let convoPane = Pane( - xStart: 0, - yStart: statusBar.yStart + statusBar.height, - width: convoWidth, - height: terminalHeight() - statusBar.height - footerHeight - 1 - ) - result.add(convoPane) - - - let msgPane = Pane( - xStart: convoPane.width, - yStart: statusBar.yStart + statusBar.height, - width: terminalWidth() - convoPane.width, - height: convoPane.height - inboxHeight - ) - result.add(msgPane) - - let msgInputPane = Pane( - xStart: convoPane.width, - yStart: msgPane.yStart + msgPane.height, - width: msgPane.width, - height: inboxHeight - ) - result.add(msgInputPane) - - let footerPane = Pane( - xStart: 0, - yStart: convoPane.yStart + convoPane.height, - width: int(terminalWidth()), - height: footerHeight - ) - result.add(footerPane) - - - let modalPane = Pane( - xStart: modalXOffset, - yStart: int(terminalHeight()/2 - modalHeight/2), - width: int(terminalWidth() - 2*modalXOffset), - height: int(modalHeight) - ) - result.add(modalPane) - - -proc offsetPane(pane: Pane): Pane = - result = Pane( - xStart: pane.xStart , - yStart: pane.yStart + 1, - width: pane.width , - height: pane.height - 2 - ) diff --git a/examples/tui/persistence.nim b/examples/tui/persistence.nim deleted file mode 100644 index cfcfe46..0000000 --- a/examples/tui/persistence.nim +++ /dev/null @@ -1,135 +0,0 @@ -import chronicles -import chronos -import libp2p/crypto/crypto -import sequtils -import std/json -import std/jsonutils except distinctBase -import std/marshal -import std/options -import std/os -import std/streams -import strformat -import strutils -import tables - -import chat/crypto/ecdh -import chat/delivery/waku_client -import chat/identity - - -const REGISTRATION_DIR = ".registry" -const KEY_DIR = ".savedkeys" - - -type Config* = object - ident*: Identity - waku*: WakuConfig - -type SavedConfig* = object - name*: string - idkey*: seq[byte] - nodekey*: seq[byte] - port*: uint16 - clusterId*: uint16 - shardId*: seq[uint16] - pubsubTopic*: string - staticPeers*: seq[string] - - -proc toWakuConfig(s: SavedConfig): WakuConfig = - result = WakuConfig( - nodekey: crypto.PrivateKey.init(s.nodekey).get(), - port: s.port, - clusterId: s.clusterId, - shardId: s.shardId, - pubsubTopic: s.pubsubTopic, - staticPeers: s.staticPeers - ) - - -proc toIdent(s: SavedConfig): Identity = - result = Identity( - name: s.name, - privateKey: loadPrivateKeyFromBytes(s.idkey).get() - ) - - -proc register(name: string, multiAddr: string) {.async.} = - - notice "Registering Account", name=name, maddr=multiAddr - - if not dirExists(REGISTRATION_DIR): - createDir(REGISTRATION_DIR) - - try: - writeFile(joinPath(REGISTRATION_DIR, fmt"{name.toLower()}.maddr"), multiAddr) - except IOError as e: - echo "Failed to write registration file: ", e.msg - raise e - - -proc fetchRegistrations*(): Table[string, string] = - - let allFiles = toSeq(walkFiles(fmt"{REGISTRATION_DIR}/*")) - result = allFiles.mapIt((splitFile(it)[1], readFile(it).strip())).toTable() - - -proc loadCfg(file: string): Option[Config] = - let data = parseFile(file) - - let cfg = Config( - ident: toIdent(data.to(SavedConfig)), - waku: toWakuConfig(data.to(SavedConfig)) - ) - - result = some(cfg) - - -proc fetchCfg(name: string): Option[Config ] = - let allFiles = toSeq(walkFiles(fmt"{KEY_DIR}/*")) - - for file in allFiles: - if name == splitFile(file)[1]: - return loadCfg(file) - return none(Config) - - -proc saveCfg(name:string, cfg: Config) = - - let s = SavedConfig( - name: name, - idkey: cfg.ident.privatekey.bytes().toSeq(), - nodekey: cfg.waku.nodekey.getBytes().get(), - port: cfg.waku.port, - clusterId: cfg.waku.clusterId, - shardId: cfg.waku.shardId, - pubsubTopic: cfg.waku.pubsubTopic, - staticPeers: cfg.waku.staticPeers - ) - - let json = jsonutils.toJson(s) - - - if not dirExists(KEY_DIR): - createDir(KEY_DIR) - - try: - writeFile(joinPath(KEY_DIR, fmt"{name.toLower()}.cfg"), $json) - except IOError as e: - echo "Failed to write cfg file: ", e.msg - raise e - - -proc getCfg*(name: string): Future[Config] {.async.} = - let cfgOpt = fetchCfg(name) - if cfgOpt.isSome: - result = cfgOpt.get() - else: - let newCfg = Config( - ident: createIdentity(name), - waku: DefaultConfig() - ) - saveCfg(name, newCfg) - await register(name, newCfg.waku.getMultiAddr()) - - result = newCfg diff --git a/examples/tui/tui.nim b/examples/tui/tui.nim deleted file mode 100644 index 010f17d..0000000 --- a/examples/tui/tui.nim +++ /dev/null @@ -1,584 +0,0 @@ - -import algorithm -import chronicles -import chronos -import illwill -import libp2p/crypto/crypto -import times -import strformat -import strutils -import sugar -import tables - -import chat -import content_types/all - -import layout -import persistence -import utils - -const charVert = "│" -const charHoriz = "─" -const charTopLeft = "┌" -const charTopRight = "┐" -const charBottomLeft = "└" -const charBottomRight = "┘" - -type - - LogEntry = object - level: string - ts: DateTime - msg: string - - Message = object - sender: string - content: string - timestamp: DateTime - id: string - isAcknowledged: bool - - ConvoInfo = object - name: string - convo: Conversation - messages: seq[Message] - lastMsgTime*: DateTime - isTooLong*: bool - - ChatApp = ref object - client: Client - tb: TerminalBuffer - conversations: Table[string, ConvoInfo] - selectedConv: string - inputBuffer: string - inputInviteBuffer: string - scrollOffset: int - messageScrollOffset: int - inviteModal: bool - - currentInviteLink: string - isInviteReady: bool - peerCount: int - - logMsgs: seq[LogEntry] - -proc `==`(a,b: ConvoInfo):bool = - if a.name==b.name: - true - else: - false - - -################################################# -# Data Management -################################################# - -proc addMessage(conv: var ConvoInfo, messageId: MessageId, sender: string, content: string) = - - let now = now() - conv.messages.add(Message( - sender: sender, - id: messageId, - content: content, - timestamp: now, - isAcknowledged: false - )) - conv.lastMsgTime = now - - -proc mostRecentConvos(app: ChatApp): seq[ConvoInfo] = - var convos = collect(for v in app.conversations.values: v) - convos.sort(proc(a, b: ConvoInfo): int = -cmp(a.lastMsgTime, b.lastMsgTime)) - - return convos - -proc getSelectedConvo(app: ChatApp): ptr ConvoInfo = - if app.conversations.hasKey(app.selectedConv): - return addr app.conversations[app.selectedConv] - - return addr app.conversations[app.mostRecentConvos()[0].name] - - - -################################################# -# ChatSDK Setup -################################################# - -proc createChatClient(name: string): Future[Client] {.async.} = - var cfg = await getCfg(name) - result = newClient(cfg.waku, cfg.ident) - - -proc createInviteLink(app: var ChatApp): string = - app.client.createIntroBundle().toLink() - - -proc createConvo(app: ChatApp) {.async.} = - discard await app.client.newPrivateConversation(toBundle(app.inputInviteBuffer.strip()).get()) - -proc sendMessage(app: ChatApp, convoInfo: ptr ConvoInfo, msg: string) {.async.} = - - var msgId = "" - if convoInfo.convo != nil: - msgId = await convoInfo.convo.sendMessage(initTextFrame(msg).toContentFrame()) - - convoInfo[].addMessage(msgId, "You", app.inputBuffer) - - -proc setupChatSdk(app: ChatApp) = - - let client = app.client - - app.client.onNewMessage(proc(convo: Conversation, msg: ReceivedMessage) {.async.} = - info "New Message: ", convoId = convo.id(), msg= msg - app.logMsgs.add(LogEntry(level: "info",ts: now(), msg: "NewMsg")) - - var contentStr = case msg.content.contentType - of text: - decode(msg.content.bytes, TextFrame).get().text - of unknown: - "" - - app.conversations[convo.id()].messages.add(Message(sender: msg.sender.toHex(), content: contentStr, timestamp: now())) - ) - - app.client.onNewConversation(proc(convo: Conversation) {.async.} = - app.logMsgs.add(LogEntry(level: "info",ts: now(), msg: fmt"Adding Convo: {convo.id()}")) - info "New Conversation: ", convoId = convo.id() - - app.conversations[convo.id()] = ConvoInfo(name: convo.id(), convo: convo, messages: @[], lastMsgTime: now(), isTooLong: false) - ) - - app.client.onDeliveryAck(proc(convo: Conversation, msgId: string) {.async.} = - info "DeliveryAck", msgId=msgId - app.logMsgs.add(LogEntry(level: "info",ts: now(), msg: fmt"Ack:{msgId}")) - - var s = "" - var msgs = addr app.conversations[convo.id()].messages - for i in countdown(msgs[].high, 0): - s = fmt"{s},{msgs[i].id}" - var m = addr msgs[i] - - if m.id == msgId: - m.isAcknowledged = true - break # Stop after - ) - - -################################################# -# Draw Funcs -################################################# - -proc resetTuiCursor(tb: var TerminalBuffer) = - tb.setForegroundColor(fgWhite) - tb.setBackgroundColor(bgBlack) - - -proc drawOutline(tb: var TerminalBuffer, layout: Pane, color: ForegroundColor, - bg: BackgroundColor = bgBlack) = - - for x in layout.xStart+1.. yEnd: - convo.isTooLong = true - app.logMsgs.add(LogEntry(level: "info",ts: now(), msg: fmt" TOO LONG: {convo.name}")) - - return - tb.write(x, y, fmt"[{timeStr}] {deliveryIcon} {m.sender}") - y = y + 1 - - while remainingText.len > 0: - if y > yEnd: - convo.isTooLong = true - app.logMsgs.add(LogEntry(level: "info",ts: now(), msg: fmt" TOO LON2: {convo.name}")) - - return - - let (line, remain) = remainingText.splitAt(maxContentWidth) - remainingText = remain - - tb.write(x+3, y, line) - y = y + 1 - y = y + 1 - else: - var y = yEnd - for i in countdown(convo.messages.len-1, 0): - - let m = convo.messages[i] - let timeStr = m.timestamp.format("HH:mm:ss") - var deliveryIcon = " " - var remainingText = m.content - - if m.sender == "You": - tb.setForegroundColor(fgYellow) - deliveryIcon = if m.isAcknowledged: "✔" else: "◯" - else: - tb.setForegroundColor(fgGreen) - - # Print lines in reverse order - while remainingText.len > 0: - if (y <= yStart + 1): - return - var lineLen = remainingText.len mod maxContentWidth - if lineLen == 0: - lineLen = maxContentWidth - - let line = remainingText[^lineLen..^1] - remainingText = remainingText[0..^lineLen+1] - - tb.write(x+3, y, line) - y = y - 1 - - tb.write(x, y, fmt"[{timeStr}] {deliveryIcon} {m.sender}") - y = y - 2 - -proc drawMsgInput( app: ChatApp, layout: Pane) = - var tb = app.tb - drawOutline(tb, layout, fgCyan) - - - let inputY = layout.yStart + 2 - let paneStart = layout.xStart - - # Draw input prompt - tb.write(paneStart + 1, inputY, " > " & app.inputBuffer, fgWhite) - - # Draw cursor - let cursorX = paneStart + 3 + app.inputBuffer.len + 1 - if cursorX < paneStart + layout.width - 1: - tb.write(cursorX, inputY, "_", fgYellow) - - discard - -proc drawFooter( app: ChatApp, layout: Pane) = - var tb = app.tb - drawOutline(tb, layout, fgBlue) - - let xStart = layout.xStart + 3 - let yStart = layout.yStart + 2 - - for i in countdown(app.logMsgs.len - 1, 0): - let o = app.logMsgs[i] - let timeStr = o.ts.format("HH:mm:ss") - let s = fmt"[{timeStr}] {o.level} - {o.msg}" - app.tb.write( xStart, yStart+i*2, s ) - discard - - -proc drawModal(app: ChatApp, layout: Pane, - color: ForegroundColor, bg: BackgroundColor = bgBlack) = - - var tb = app.tb - tb.setForegroundColor(color) - tb.setBackgroundColor(bg) - for x in layout.xStart.. " & app.inputInviteBuffer) - - # Draw cursor - let cursorX = layout.xStart+5+1 + app.inputInviteBuffer.len - if cursorX < terminalWidth() - 1: - tb.write(cursorX, layout.yStart+inputLine, "_", fgYellow) - - tb.setForegroundColor(fgBlack) - tb.setBackgroundColor(bgGreen) - tb.write(layout.xStart+5, layout.yStart+inputLine+3, "InviteLink: " & app.currentInviteLink) - - resetTuiCursor(tb) - -################################################# -# Input Handling -################################################# - -proc gePreviousConv(app: ChatApp): string = - let convos = app.mostRecentConvos() - let i = convos.find(app.getSelectedConvo()[]) - - return convos[max(i-1, 0)].name - - -proc getNextConv(app: ChatApp): string = - let convos = app.mostRecentConvos() - var s = "" - for c in convos: - s = s & c.name - - let i = convos.find(app.getSelectedConvo()[]) - app.logMsgs.add(LogEntry(level: "info",ts: now(), msg: fmt"i:{convos[min(i+1, convos.len-1)].isTooLong}")) - - return convos[min(i+1, convos.len-1)].name - - -proc handleInput(app: ChatApp, key: Key) {.async.} = - case key - of Key.Up: - app.selectedConv = app.gePreviousConv() - of Key.Down: - app.selectedConv = app.getNextConv() - - of Key.PageUp: - app.messageScrollOffset = min(app.messageScrollOffset + 5, 0) - of Key.PageDown: - app.messageScrollOffset = max(app.messageScrollOffset - 5, - -(max(0, app.getSelectedConvo().messages.len - 10))) - of Key.Enter: - - if app.inviteModal: - notice "Enter Invite", link= app.inputInviteBuffer - app.inviteModal = false - app.isInviteReady = true - else: - if app.inputBuffer.len > 0 and app.conversations.len > 0: - - let sc = app.getSelectedConvo() - await app.sendMessage(sc, app.inputBuffer) - - app.inputBuffer = "" - app.messageScrollOffset = 0 # Auto-scroll to bottom - of Key.Backspace: - if app.inputBuffer.len > 0: - app.inputBuffer.setLen(app.inputBuffer.len - 1) - of Key.Tab: - app.inviteModal = not app.inviteModal - - if app.inviteModal: - app.currentInviteLink = app.client.createIntroBundle().toLink() - of Key.Escape, Key.CtrlC: - quit(0) - else: - # Handle regular character input - let ch = char(key) - if ch.isAlphaNumeric() or ch in " !@#$%^&*()_+-=[]{}|;':\",./<>?": - if app.inviteModal: - app.inputInviteBuffer.add(ch) - else: - app.inputBuffer.add(ch) - - -################################################# -# Tasks -################################################# - -proc appLoop(app: ChatApp, panes: seq[Pane]) : Future[void] {.async.} = - illwillInit(fullscreen = false) - # Clear buffer - while true: - await sleepAsync(chronos.milliseconds(5)) - app.tb.clear() - - drawStatusBar(app, panes[0], fgBlack, getIdColor(app.client.getId())) - drawConversationPane(app, panes[1]) - drawMsgPane(app, panes[2]) - - if app.inviteModal: - drawModal(app, panes[5], fgYellow, bgGreen) - else: - drawMsgInput(app, panes[3]) - - drawFooter(app, panes[4]) - - # Draw help text - app.tb.write(1, terminalHeight()-1, "Tab: Invite Modal | ↑/↓: Select conversation | PgUp/PgDn: Scroll messages | Enter: Send | Esc: Quit", fgGreen) - - # Display buffer - app.tb.display() - - # Handle input - let key = getKey() - await handleInput(app, key) - - if app.isInviteReady: - - try: - let sanitized = app.inputInviteBuffer.replace(" ", "").replace("\r","") - discard await app.client.newPrivateConversation(toBundle(app.inputInviteBuffer.strip()).get()) - - except Exception as e: - info "bad invite", invite = app.inputInviteBuffer - app.inputInviteBuffer = "" - app.isInviteReady = false - -proc peerWatch(app: ChatApp): Future[void] {.async.} = - while true: - await sleepAsync(chronos.seconds(1)) - app.peerCount = app.client.ds.getConnectedPeerCount() - - -################################################# -# Main -################################################# - -proc initChatApp(client: Client): Future[ChatApp] {.async.} = - - var app = ChatApp( - client: client, - tb: newTerminalBuffer(terminalWidth(), terminalHeight()), - conversations: initTable[string, ConvoInfo](), - selectedConv: "Bob", - inputBuffer: "", - scrollOffset: 0, - messageScrollOffset: 0, - isInviteReady: false, - peerCount: -1, - logMsgs: @[] - ) - - app.setupChatSdk() - await app.client.start() - - - # Add some sample conversations with messages - var sender = "Nobody" - var conv1 = ConvoInfo(name: "ReadMe", messages: @[]) - conv1.addMessage("",sender, "First start multiple clients and ensure, that he PeerCount is correct (it's listed in the top left corner)") - conv1.addMessage("",sender, "Once connected, The sender needs to get the recipients introduction link. The links contains the key material and information required to initialize a conversation. Press `Tab` to generate a link") - conv1.addMessage("",sender, "Paste the link from one client into another. This will start the initialization protocol, which will send an invite to the recipient and negotiate a conversation") - conv1.addMessage("",sender, "Once established, Applications are notified by a callback that a new conversation has been established, and participants can send messages") - - - - app.conversations[conv1.name] = conv1 - - return app - - -proc main*() {.async.} = - - let args = getCmdArgs() - let client = await createChatClient(args.username) - var app = await initChatApp(client) - - let tasks: seq[Future[void]] = @[ - appLoop(app, getPanes()), - peerWatch(app) - ] - - discard await allFinished(tasks) - -when isMainModule: - waitFor main() - -# this is not nim code \ No newline at end of file diff --git a/examples/tui/utils.nim b/examples/tui/utils.nim deleted file mode 100644 index 8b56ca4..0000000 --- a/examples/tui/utils.nim +++ /dev/null @@ -1,53 +0,0 @@ -import std/parseopt -import illwill -import times - -################################################# -# Command Line Args -################################################# - -type CmdArgs* = object - username*: string - invite*: string - -proc getCmdArgs*(): CmdArgs = - var username = "" - var invite = "" - for kind, key, val in getopt(): - case kind - of cmdArgument: - discard - of cmdLongOption, cmdShortOption: - case key - of "name", "n": - username = val - of "invite", "i": - invite = val - of cmdEnd: - break - if username == "": - username = "" - - result = CmdArgs(username: username, invite:invite) - - -################################################# -# Utils -################################################# - -proc getIdColor*(id: string): BackgroundColor = - var i = ord(id[0]) - - - let colors = @[bgCyan, - bgGreen, - bgMagenta, - bgRed, - bgYellow, - bgBlue - ] - - return colors[i mod colors.len] - -proc toStr*(ts: DateTime): string = - ts.format("HH:mm:ss") \ No newline at end of file diff --git a/nim_chat_poc.nimble b/nim_chat_poc.nimble index 04eaf01..c10c360 100644 --- a/nim_chat_poc.nimble +++ b/nim_chat_poc.nimble @@ -21,6 +21,7 @@ requires "nim >= 2.2.4", "regex", "web3", "https://github.com/jazzz/nim-sds#exports", + "libchat", "waku", "ffi" diff --git a/src/chat.nim b/src/chat.nim index 754d8da..8cabde4 100644 --- a/src/chat.nim +++ b/src/chat.nim @@ -1,17 +1,13 @@ import chat/[ client, - crypto, - conversations, delivery/waku_client, identity, - links, types ] -export client, conversations, identity, links, waku_client +export client, identity, waku_client +export identity.`$` #export specific frames need by applications export MessageId -export toHex -export crypto.`$` diff --git a/src/chat/client.nim b/src/chat/client.nim index ef3d6c4..c98e554 100644 --- a/src/chat/client.nim +++ b/src/chat/client.nim @@ -6,24 +6,15 @@ import # Foreign chronicles, chronos, - sds, - sequtils, - std/tables, - std/sequtils, + libchat, + std/options, strformat, - strutils, - tables, types import #local - conversations, - conversations/convo_impl, - crypto, delivery/waku_client, errors, identity, - inbox, - proto_types, types, utils @@ -35,6 +26,27 @@ logScope: # Definitions ################################################# +# Type used to return message data via callback +type ReceivedMessage* = ref object of RootObj + sender*: PublicKey + timestamp*: int64 + content*: seq[byte] + + +type ConvoType = enum + PrivateV1 + +type Conversation* = object + ctx: LibChat + convoId: string + ds: WakuClient + convo_type: ConvoType + + +proc id*(self: Conversation): string = + return self.convoId + + type MessageCallback* = proc(conversation: Conversation, msg: ReceivedMessage): Future[void] {.async.} NewConvoCallback* = proc(conversation: Conversation): Future[void] {.async.} @@ -48,13 +60,11 @@ type KeyEntry* = object timestamp: int64 type ChatClient* = ref object - ident: Identity + libchatCtx: LibChat ds*: WakuClient - keyStore: Table[string, KeyEntry] # Keyed by HexEncoded Public Key - conversations: Table[string, Conversation] # Keyed by conversation ID + id: string inboundQueue: QueueRef isRunning: bool - inbox: Inbox newMessageCallbacks: seq[MessageCallback] newConvoCallbacks: seq[NewConvoCallback] @@ -64,30 +74,24 @@ type ChatClient* = ref object # Constructors ################################################# -proc newClient*(ds: WakuClient, ident: Identity): ChatClient {.raises: [IOError, - ValueError, SerializationError].} = +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. try: - let rm = newReliabilityManager().valueOr: - raise newException(ValueError, fmt"SDS InitializationError") - - let defaultInbox = initInbox(ident) var q = QueueRef(queue: newAsyncQueue[ChatPayload](10)) - var c = ChatClient(ident: ident, + var c = ChatClient( + libchatCtx: newConversationsContext(), ds: ds, - keyStore: initTable[string, KeyEntry](), - conversations: initTable[string, Conversation](), + id: ident.getName(), inboundQueue: q, isRunning: false, - inbox: defaultInbox, newMessageCallbacks: @[], newConvoCallbacks: @[]) - c.conversations[defaultInbox.id()] = defaultInbox - notice "Client started", client = c.ident.getName(), - defaultInbox = defaultInbox, inTopic= topic_inbox(c.ident.get_addr()) + notice "Client started", client = c.id + result = c except Exception as e: error "newCLient", err = e.msg @@ -97,27 +101,12 @@ proc newClient*(ds: WakuClient, ident: Identity): ChatClient {.raises: [IOError, ################################################# proc getId*(client: ChatClient): string = - result = client.ident.getName() - -proc identity*(client: ChatClient): Identity = - result = client.ident - -proc defaultInboxConversationId*(self: ChatClient): string = - ## Returns the default inbox address for the client. - result = conversationIdFor(self.ident.getPubkey()) - -proc getConversationFromHint(self: ChatClient, - conversationHint: string): Result[Option[Conversation], string] = - - # TODO: Implementing Hinting - if not self.conversations.hasKey(conversationHint): - ok(none(Conversation)) - else: - ok(some(self.conversations[conversationHint])) + result = client.id proc listConversations*(client: ChatClient): seq[Conversation] = - result = toSeq(client.conversations.values()) + # TODO: (P1) Implement list conversations + result = @[] ################################################# # Callback Handling @@ -135,6 +124,7 @@ proc onNewConversation*(client: ChatClient, callback: NewConvoCallback) = proc notifyNewConversation(client: ChatClient, convo: Conversation) = for cb in client.newConvoCallbacks: + debug "calling OnConvo CB", client=client.getId(), len = client.newConvoCallbacks.len() discard cb(convo) proc onDeliveryAck*(client: ChatClient, callback: DeliveryAckCallback) = @@ -149,51 +139,47 @@ proc notifyDeliveryAck(client: ChatClient, convo: Conversation, # Functional ################################################# -proc createIntroBundle*(self: var ChatClient): IntroBundle = +proc createIntroBundle*(self: var ChatClient): seq[byte] = ## Generates an IntroBundle for the client, which includes ## the required information to send a message. - - # Create Ephemeral keypair, save it in the key store - let ephemeralKey = generateKey() - - self.keyStore[ephemeralKey.getPublicKey().bytes().bytesToHex()] = KeyEntry( - keyType: "ephemeral", - privateKey: ephemeralKey, - timestamp: getCurrentTimestamp() - ) - - result = IntroBundle( - ident: @(self.ident.getPubkey().bytes()), - ephemeral: @(ephemeralKey.getPublicKey().bytes()), - ) - + result = self.libchatCtx.createIntroductionBundle().valueOr: + error "could not create bundle",error=error, client = self.getId() + return + notice "IntroBundleCreated", client = self.getId(), - pubBytes = result.ident + bundle = result + +proc sendPayloads(ds: WakuClient, payloads: seq[PayloadResult]) = + for payload in payloads: + # TODO: (P2) surface errors + discard ds.sendBytes(payload.address, payload.data) ################################################# # Conversation Initiation ################################################# -proc addConversation*(client: ChatClient, convo: Conversation) = - notice "Creating conversation", client = client.getId(), convoId = convo.id() - client.conversations[convo.id()] = convo - client.notifyNewConversation(convo) proc getConversation*(client: ChatClient, convoId: string): Conversation = - notice "Get conversation", client = client.getId(), convoId = convoId - result = client.conversations[convoId] + result = Conversation(ctx:client.libchatCtx, convoId:convoId, ds: client.ds, convo_type: PrivateV1) proc newPrivateConversation*(client: ChatClient, - introBundle: IntroBundle, content: Content): Future[Option[ChatError]] {.async.} = - ## Creates a private conversation with the given `IntroBundle`. - ## `IntroBundles` are provided out-of-band. - let remote_pubkey = loadPublicKeyFromBytes(introBundle.ident).get() - let remote_ephemeralkey = loadPublicKeyFromBytes(introBundle.ephemeral).get() + introBundle: seq[byte], content: Content): Future[Option[ChatError]] {.async.} = - let convo = await client.inbox.inviteToPrivateConversation(client.ds,remote_pubkey, remote_ephemeralkey, content ) - client.addConversation(convo) # TODO: Fix re-entrantancy bug. Convo needs to be saved before payload is sent. + let res = client.libchatCtx.createNewPrivateConvo(introBundle, content) + let (convoId, payloads) = res.valueOr: + error "could not create bundle",error=error, client = client.getId() + return some(ChatError(code: errLibChat, context:fmt"got: {error}" )) + + client.ds.sendPayloads(payloads); + + + client.notifyNewConversation(Conversation(ctx: client.libchatCtx, + convoId : convoId, ds: client.ds, convo_type: ConvoType.PrivateV1 + )) + + notice "CREATED", client=client.getId(), convoId=convoId return none(ChatError) @@ -202,31 +188,33 @@ proc newPrivateConversation*(client: ChatClient, # Receives a incoming payload, decodes it, and processes it. ################################################# -proc parseMessage(client: ChatClient, msg: ChatPayload) {.raises: [ValueError, - SerializationError].} = - let envelopeRes = decode(msg.bytes, WapEnvelopeV1) - if envelopeRes.isErr: - debug "Failed to decode WapEnvelopeV1", client = client.getId(), err = envelopeRes.error - return - let envelope = envelopeRes.get() - - let convo = block: - let opt = client.getConversationFromHint(envelope.conversationHint).valueOr: - raise newException(ValueError, "Failed to get conversation: " & error) - - if opt.isSome(): - opt.get() - else: - let k = toSeq(client.conversations.keys()).join(", ") - warn "No conversation found", client = client.getId(), - hint = envelope.conversationHint, knownIds = k - return +proc parseMessage(client: ChatClient, msg: ChatPayload) {.raises: [ValueError].} = try: - convo.handleFrame(client, envelope.payload) + let opt_content = client.libchatCtx.handlePayload(msg.bytes).valueOr: + error "handlePayload" , error=error, client=client.getId() + return + + if opt_content.isSome(): + let content = opt_content.get() + let convo = client.getConversation(content.conversationId) + # TODO: (P1) Add sender information from LibChat. + let msg = ReceivedMessage(sender: nil,timestamp:getCurrentTimestamp(),content: content.data ) + client.notifyNewMessage(convo, msg) + else: + debug "Parsed message generated no content", client=client.getId() + except Exception as e: error "HandleFrame Failed", error = e.msg +proc sendMessage*(convo: Conversation, content: Content) : Future[MessageId] {.async, gcsafe.} = + let payloads = convo.ctx.sendContent(convo.convoId, content).valueOr: + error "SendMessage", e=error + return "error" + + convo.ds.sendPayloads(payloads); + + ################################################# # Async Tasks ################################################# @@ -237,20 +225,9 @@ proc messageQueueConsumer(client: ChatClient) {.async.} = while client.isRunning: let message = await client.inboundQueue.queue.get() + debug "Got WakuMessage", client = client.getId() , topic= message.content_topic, len=message.bytes.len() - let topicRes = inbox.parseTopic(message.contentTopic).or(private_v1.parseTopic(message.contentTopic)) - if topicRes.isErr: - debug "Invalid content topic", client = client.getId(), err = topicRes.error, contentTopic = message.contentTopic - continue - - notice "Inbound Message Received", client = client.getId(), - contentTopic = message.contentTopic, len = message.bytes.len() - try: - client.parseMessage(message) - - except CatchableError as e: - error "Error in message listener", err = e.msg, - pubsub = message.pubsubTopic, contentTopic = message.contentTopic + client.parseMessage(message) ################################################# @@ -271,5 +248,6 @@ proc start*(client: ChatClient) {.async.} = proc stop*(client: ChatClient) {.async.} = ## Stop the client. await client.ds.stop() + client.libchatCtx.destroy() client.isRunning = false notice "Client stopped", client = client.getId() diff --git a/src/chat/conversation_store.nim b/src/chat/conversation_store.nim deleted file mode 100644 index e629ea7..0000000 --- a/src/chat/conversation_store.nim +++ /dev/null @@ -1,16 +0,0 @@ -import ./conversations/[convo_type, message] -import identity -import types - -type ConvoId = string - -type - ConversationStore* = concept - proc addConversation(self: Self, convo: Conversation) - proc getConversation(self: Self, convoId: string): Conversation - proc identity(self: Self): Identity - proc getId(self: Self): string - - proc notifyNewMessage(self: Self, convo: Conversation, msg: ReceivedMessage) - proc notifyDeliveryAck(self: Self, convo: Conversation, - msgId: MessageId) diff --git a/src/chat/conversations.nim b/src/chat/conversations.nim deleted file mode 100644 index eb426a7..0000000 --- a/src/chat/conversations.nim +++ /dev/null @@ -1,5 +0,0 @@ -import - ./conversations/[convo_type, private_v1, message] - - -export private_v1, convo_type, message diff --git a/src/chat/conversations/convo_impl.nim b/src/chat/conversations/convo_impl.nim deleted file mode 100644 index 60fb9e5..0000000 --- a/src/chat/conversations/convo_impl.nim +++ /dev/null @@ -1,28 +0,0 @@ -import ../conversation_store -import ../conversations -import ../inbox - - -proc getType(convo: Conversation): ConvoTypes = - if convo of Inbox: - return InboxV1Type - - elif convo of PrivateV1: - return PrivateV1Type - - else: - raise newException(Defect, "Conversation Type not processed") - -proc handleFrame*[T: ConversationStore](convo: Conversation, client: T, - bytes: seq[byte]) = - - case convo.getType(): - of InboxV1Type: - let inbox = Inbox(convo) - inbox.handleFrame(client, bytes) - - of PrivateV1Type: - let priv = PrivateV1(convo) - priv.handleFrame(client, bytes) - - diff --git a/src/chat/conversations/convo_type.nim b/src/chat/conversations/convo_type.nim deleted file mode 100644 index b810eed..0000000 --- a/src/chat/conversations/convo_type.nim +++ /dev/null @@ -1,29 +0,0 @@ -import chronos -import strformat -import strutils - -import ../proto_types -import ../utils -import ../types - -type - ConvoTypes* = enum - InboxV1Type, PrivateV1Type - -type - Conversation* = ref object of RootObj - name: string - -proc `$`(conv: Conversation): string = - fmt"Convo: {conv.name}" - -# TODO: Removing the raises clause and the exception raise causes this -# error --> ...src/chat_sdk/client.nim(166, 9) Error: addConversation(client, convo) can raise an unlisted exception: Exception -# Need better understanding of NIMs Exception model -method id*(self: Conversation): string {.raises: [Defect, ValueError].} = - # TODO: make this a compile time check - panic("ProgramError: Missing concrete implementation") - -method sendMessage*(convo: Conversation, content_frame: Content) : Future[MessageId] {.async, base, gcsafe.} = - # TODO: make this a compile time check - panic("ProgramError: Missing concrete implementation") diff --git a/src/chat/conversations/message.nim b/src/chat/conversations/message.nim deleted file mode 100644 index 6f82edf..0000000 --- a/src/chat/conversations/message.nim +++ /dev/null @@ -1,11 +0,0 @@ -import ../crypto - -# How to surface different verifability of properties across conversation types - - -type ReceivedMessage* = ref object of RootObj - sender*: PublicKey - timestamp*: int64 - content*: seq[byte] - - diff --git a/src/chat/conversations/private_v1.nim b/src/chat/conversations/private_v1.nim deleted file mode 100644 index 9e8b264..0000000 --- a/src/chat/conversations/private_v1.nim +++ /dev/null @@ -1,282 +0,0 @@ - -import blake2 -import chronicles -import chronos -import sds -import std/[sequtils, strutils, strformat] -import std/algorithm -import sugar -import tables - -import ../conversation_store -import ../crypto -import ../delivery/waku_client - -import ../[ - identity, - errors, - proto_types, - types, - utils -] -import convo_type -import message - -import ../../naxolotl as nax - -const TopicPrefixPrivateV1 = "/convo/private/" - -type - ReceivedPrivateV1Message* = ref object of ReceivedMessage - -proc initReceivedMessage(sender: PublicKey, timestamp: int64, content: Content) : ReceivedPrivateV1Message = - ReceivedPrivateV1Message(sender:sender, timestamp:timestamp, content:content) - - -type - PrivateV1* = ref object of Conversation - ds: WakuClient - sdsClient: ReliabilityManager - owner: Identity - participant: PublicKey - discriminator: string - doubleratchet: naxolotl.Doubleratchet - -proc derive_topic(participant: PublicKey): string = - ## Derives a topic from the participants' public keys. - return TopicPrefixPrivateV1 & participant.get_addr() - -proc getTopicInbound*(self: PrivateV1): string = - ## Returns the topic where the local client is listening for messages - return derive_topic(self.owner.getPubkey()) - -proc getTopicOutbound*(self: PrivateV1): string = - ## Returns the topic where the remote recipient is listening for messages - return derive_topic(self.participant) - -## Parses the topic to extract the conversation ID. -proc parseTopic*(topic: string): Result[string, ChatError] = - if not topic.startsWith(TopicPrefixPrivateV1): - return err(ChatError(code: errTopic, context: "Invalid topic prefix")) - - let id = topic.split('/')[^1] - if id == "": - return err(ChatError(code: errTopic, context: "Empty conversation ID")) - - return ok(id) - -proc allParticipants(self: PrivateV1): seq[PublicKey] = - return @[self.owner.getPubkey(), self.participant] - -proc getConvoIdRaw(participants: seq[PublicKey], - discriminator: string): string = - # This is a placeholder implementation. - var addrs = participants.map(x => x.get_addr()); - addrs.sort() - addrs.add(discriminator) - let raw = addrs.join("|") - return utils.hash_func(raw) - -proc getConvoId*(self: PrivateV1): string = - return getConvoIdRaw(@[self.owner.getPubkey(), self.participant], self.discriminator) - - -proc calcMsgId(self: PrivateV1, msgBytes: seq[byte]): string = - let s = fmt"{self.getConvoId()}|{msgBytes}" - result = getBlake2b(s, 16, "") - - -proc encrypt*(convo: PrivateV1, plaintext: var seq[byte]): EncryptedPayload = - - let (header, ciphertext) = convo.doubleratchet.encrypt(plaintext) #TODO: Associated Data - - result = EncryptedPayload(doubleratchet: proto_types.DoubleRatchet( - dh: toSeq(header.dhPublic), - msgNum: header.msgNumber, - prevChainLen: header.prevChainLen, - ciphertext: ciphertext) - ) - -proc decrypt*(convo: PrivateV1, enc: EncryptedPayload): Result[seq[byte], ChatError] = - # Ensure correct type as received - if enc.doubleratchet.ciphertext == @[]: - return err(ChatError(code: errTypeError, context: "Expected doubleratchet encrypted payload got ???")) - - let dr = enc.doubleratchet - - var header = DrHeader( - msgNumber: dr.msgNum, - prevChainLen: dr.prevChainLen - ) - copyMem(addr header.dhPublic[0], unsafeAddr dr.dh[0], dr.dh.len) # TODO: Avoid this copy - - convo.doubleratchet.decrypt(header, dr.ciphertext, @[]).mapErr(proc(e: NaxolotlError): ChatError = ChatError(code: errWrapped, context: repr(e) )) - - - -proc wireCallbacks(convo: PrivateV1, deliveryAckCb: proc( - conversation: Conversation, - msgId: string): Future[void] {.async.} = nil) = - ## Accepts lambdas/functions to be called from Reliability Manager callbacks. - let funcMsg = proc(messageId: SdsMessageID, - channelId: SdsChannelID) {.gcsafe.} = - debug "sds message ready", messageId = messageId, - channelId = channelId - - let funcDeliveryAck = proc(messageId: SdsMessageID, - channelId: SdsChannelID) {.gcsafe.} = - debug "sds message ack", messageId = messageId, - channelId = channelId - - if deliveryAckCb != nil: - asyncSpawn deliveryAckCb(convo, messageId) - - let funcDroppedMsg = proc(messageId: SdsMessageID, missingDeps: seq[ - SdsMessageID], channelId: SdsChannelID) {.gcsafe.} = - debug "sds message missing", messageId = messageId, - missingDeps = missingDeps, channelId = channelId - - convo.sdsClient.setCallbacks( - funcMsg, funcDeliveryAck, funcDroppedMsg - ) - - - -proc initPrivateV1*(owner: Identity, ds:WakuClient, participant: PublicKey, seedKey: array[32, byte], - discriminator: string = "default", isSender: bool, deliveryAckCb: proc( - conversation: Conversation, - msgId: string): Future[void] {.async.} = nil): - PrivateV1 = - - var rm = newReliabilityManager().valueOr: - raise newException(ValueError, fmt"sds initialization: {repr(error)}") - - let dr = if isSender: - initDoubleratchetSender(seedKey, participant.bytes) - else: - initDoubleratchetRecipient(seedKey, owner.privateKey.bytes) - - result = PrivateV1( - ds: ds, - sdsClient: rm, - owner: owner, - participant: participant, - discriminator: discriminator, - doubleratchet: dr - ) - - result.wireCallbacks(deliveryAckCb) - - result.sdsClient.ensureChannel(result.getConvoId()).isOkOr: - raise newException(ValueError, "bad sds channel") - -proc encodeFrame*(self: PrivateV1, msg: PrivateV1Frame): (MessageId, EncryptedPayload) = - - let frameBytes = encode(msg) - let msgId = self.calcMsgId(frameBytes) - var sdsPayload = self.sdsClient.wrapOutgoingMessage(frameBytes, msgId, - self.getConvoId()).valueOr: - raise newException(ValueError, fmt"sds wrapOutgoingMessage failed: {repr(error)}") - - result = (msgId, self.encrypt(sdsPayload)) - -proc sendFrame(self: PrivateV1, ds: WakuClient, - msg: PrivateV1Frame): Future[MessageId]{.async.} = - let (msgId, encryptedPayload) = self.encodeFrame(msg) - discard ds.sendPayload(self.getTopicOutbound(), encryptedPayload.toEnvelope( - self.getConvoId())) - - result = msgId - - -method id*(self: PrivateV1): string = - return getConvoIdRaw(self.allParticipants(), self.discriminator) - -proc handleFrame*[T: ConversationStore](convo: PrivateV1, client: T, - encPayload: EncryptedPayload) = - ## Dispatcher for Incoming `PrivateV1Frames`. - ## Calls further processing depending on the kind of frame. - - if convo.doubleratchet.dhSelfPublic() == encPayload.doubleratchet.dh: - info "outgoing message, no need to handle", convo = convo.id() - return - - let plaintext = convo.decrypt(encPayload).valueOr: - error "decryption failed", error = error - return - - let (frameData, missingDeps, channelId) = convo.sdsClient.unwrapReceivedMessage( - plaintext).valueOr: - raise newException(ValueError, fmt"Failed to unwrap SDS message:{repr(error)}") - - debug "sds unwrap", convo = convo.id(), missingDeps = missingDeps, - channelId = channelId - - let frame = decode(frameData, PrivateV1Frame).valueOr: - raise newException(ValueError, "Failed to decode SdsM: " & error) - - if frame.sender == @(convo.owner.getPubkey().bytes()): - notice "Self Message", convo = convo.id() - return - - case frame.getKind(): - of typeContent: - # TODO: Using client.getId() results in an error in this context - client.notifyNewMessage(convo, initReceivedMessage(convo.participant, frame.timestamp, frame.content)) - - of typePlaceholder: - notice "Got Placeholder", text = frame.placeholder.counter - -proc handleFrame*[T: ConversationStore](convo: PrivateV1, client: T, - bytes: seq[byte]) = - ## Dispatcher for Incoming `PrivateV1Frames`. - ## Calls further processing depending on the kind of frame. - let encPayload = decode(bytes, EncryptedPayload).valueOr: - raise newException(ValueError, fmt"Failed to decode EncryptedPayload: {repr(error)}") - - convo.handleFrame(client,encPayload) - - -method sendMessage*(convo: PrivateV1, content_frame: Content) : Future[MessageId] {.async.} = - - try: - let frame = PrivateV1Frame(sender: @(convo.owner.getPubkey().bytes()), - timestamp: getCurrentTimestamp(), content: content_frame) - - result = await convo.sendFrame(convo.ds, frame) - except Exception as e: - error "Unknown error in PrivateV1:SendMessage" - - -## Encrypts content without sending it. -proc encryptMessage*(self: PrivateV1, content_frame: Content) : (MessageId, EncryptedPayload) = - - try: - let frame = PrivateV1Frame( - sender: @(self.owner.getPubkey().bytes()), - timestamp: getCurrentTimestamp(), - content: content_frame - ) - - result = self.encodeFrame(frame) - - except Exception as e: - error "Unknown error in PrivateV1:EncryptMessage" - -proc initPrivateV1Sender*(sender:Identity, - ds: WakuClient, - participant: PublicKey, - seedKey: array[32, byte], - content: Content, - deliveryAckCb: proc(conversation: Conversation, msgId: string): Future[void] {.async.} = nil): (PrivateV1, EncryptedPayload) = - let convo = initPrivateV1(sender, ds, participant, seedKey, "default", true, deliveryAckCb) - - # Encrypt Content with Convo - let contentFrame = PrivateV1Frame(sender: @(sender.getPubkey().bytes()), timestamp: getCurrentTimestamp(), content: content) - let (msg_id, encPayload) = convo.encryptMessage(content) - result = (convo, encPayload) - - -proc initPrivateV1Recipient*(owner:Identity,ds: WakuClient, participant: PublicKey, seedKey: array[32, byte], deliveryAckCb: proc( - conversation: Conversation, msgId: string): Future[void] {.async.} = nil): PrivateV1 = - initPrivateV1(owner,ds, participant, seedKey, "default", false, deliveryAckCb) diff --git a/src/chat/crypto.nim b/src/chat/crypto.nim deleted file mode 100644 index 1aa4cc5..0000000 --- a/src/chat/crypto.nim +++ /dev/null @@ -1,35 +0,0 @@ -import proto_types - -import strformat -import crypto/ecdh -import std/[sysrand] -import results -import utils - -export PublicKey, PrivateKey, bytes, createRandomKey, loadPrivateKeyFromBytes, loadPublicKeyFromBytes, - getPublicKey, Dh, Result, get_addr, `$` - - -proc encrypt_plain*[T: EncryptableTypes](frame: T): EncryptedPayload = - return EncryptedPayload( - plaintext: Plaintext(payload: encode(frame)), - ) - -proc decrypt_plain*[T: EncryptableTypes](ciphertext: Plaintext, t: typedesc[ - T]): Result[T, string] = - - let obj = decode(ciphertext.payload, T) - if obj.isErr: - return err("Protobuf decode failed: " & obj.error) - result = ok(obj.get()) - -proc generate_key*(): PrivateKey = - createRandomKey().get() - - -proc toHex*(key: PublicKey): string = - bytesToHex(key.bytes()) - -proc `$`*(key: PublicKey): string = - let byteStr = toHex(key) - fmt"{byteStr[0..3]}..{byteStr[^4 .. ^1]}" diff --git a/src/chat/delivery/waku_client.nim b/src/chat/delivery/waku_client.nim index 4662120..70f59a2 100644 --- a/src/chat/delivery/waku_client.nim +++ b/src/chat/delivery/waku_client.nim @@ -20,7 +20,6 @@ import waku_filter_v2/client, ] -import ../proto_types logScope: topics = "chat waku" @@ -72,9 +71,9 @@ proc DefaultConfig*(): WakuConfig = shardId: @[shardId], pubsubTopic: &"/waku/2/rs/{clusterId}/{shardId}", staticPeers: @[]) -proc sendPayload*(client: WakuClient, contentTopic: string, - env: WapEnvelopeV1) {.async.} = - let bytes = encode(env) + +proc sendBytes*(client: WakuClient, contentTopic: string, + bytes: seq[byte]) {.async.} = let msg = WakuMessage(contentTopic: contentTopic, payload: bytes) let res = await client.node.publish(some(PubsubTopic(client.cfg.pubsubTopic)), msg) diff --git a/src/chat/errors.nim b/src/chat/errors.nim index c74b5b2..f5b93e3 100644 --- a/src/chat/errors.nim +++ b/src/chat/errors.nim @@ -9,6 +9,7 @@ type errTypeError errWrapped errTopic + errLibChat proc `$`*(x: ChatError): string = fmt"ChatError(code={$x.code}, context: {x.context})" diff --git a/src/chat/identity.nim b/src/chat/identity.nim index 37b4267..794fcc1 100644 --- a/src/chat/identity.nim +++ b/src/chat/identity.nim @@ -1,6 +1,10 @@ -import crypto +import crypto/ecdh import results +import strformat +import utils + +export PublicKey, PrivateKey, loadPrivateKeyFromBytes, loadPublicKeyFromBytes type @@ -28,6 +32,13 @@ proc getPubkey*(self: Identity): PublicKey = 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]}" + diff --git a/src/chat/inbox.nim b/src/chat/inbox.nim deleted file mode 100644 index 7af319a..0000000 --- a/src/chat/inbox.nim +++ /dev/null @@ -1,171 +0,0 @@ -import std/[strutils] - -import - chronicles, - chronos, - results, - strformat - -import - conversations/convo_type, - conversations, - conversation_store, - crypto, - delivery/waku_client, - errors, - identity, - proto_types, - types - -logScope: - topics = "chat inbox" - -type - Inbox* = ref object of Conversation - identity: Identity - inbox_addr: string - -const - TopicPrefixInbox = "/inbox/" - -proc `$`*(conv: Inbox): string = - fmt"Inbox: addr->{conv.inbox_addr}" - - -proc initInbox*(ident: Identity): Inbox = - ## Initializes an Inbox object with the given address and invite callback. - return Inbox(identity: ident) - -proc encrypt*(frame: InboxV1Frame): EncryptedPayload = - return encrypt_plain(frame) - -proc decrypt*(inbox: Inbox, encbytes: EncryptedPayload): Result[InboxV1Frame, string] = - let res_frame = decrypt_plain(encbytes.plaintext, InboxV1Frame) - if res_frame.isErr: - error "Failed to decrypt frame: ", err = res_frame.error - return err("Failed to decrypt frame: " & res_frame.error) - result = res_frame - -proc wrap_env*(payload: EncryptedPayload, convo_id: string): WapEnvelopeV1 = - let bytes = encode(payload) - let salt = generateSalt() - - return WapEnvelopeV1( - payload: bytes, - salt: salt, - conversation_hint: convo_id, - ) - -proc conversation_id_for*(pubkey: PublicKey): string = - ## Generates a conversation ID based on the public key. - return "/convo/inbox/v1/" & pubkey.get_addr() - -# TODO derive this from instance of Inbox -proc topic_inbox*(client_addr: string): string = - return TopicPrefixInbox & client_addr - -proc parseTopic*(topic: string): Result[string, ChatError] = - if not topic.startsWith(TopicPrefixInbox): - return err(ChatError(code: errTopic, context: "Invalid inbox topic prefix")) - - let id = topic.split('/')[^1] - if id == "": - return err(ChatError(code: errTopic, context: "Empty inbox id")) - - return ok(id) - -method id*(convo: Inbox): string = - return conversation_id_for(convo.identity.getPubkey()) - -## Encrypt and Send a frame to the remote account -proc sendFrame(ds: WakuClient, remote: PublicKey, frame: InboxV1Frame ): Future[void] {.async.} = - let env = wrapEnv(encrypt(frame),conversation_id_for(remote) ) - await ds.sendPayload(topic_inbox(remote.get_addr()), env) - - -proc newPrivateInvite(initator_static: PublicKey, - initator_ephemeral: PublicKey, - recipient_static: PublicKey, - recipient_ephemeral: uint32, - payload: EncryptedPayload) : InboxV1Frame = - - let invite = InvitePrivateV1( - initiator: @(initator_static.bytes()), - initiatorEphemeral: @(initator_ephemeral.bytes()), - participant: @(recipient_static.bytes()), - participantEphemeralId: 0, - discriminator: "", - initial_message: payload - ) - result = InboxV1Frame(invitePrivateV1: invite, recipient: "") - -################################################# -# Conversation Creation -################################################# - -## Establish a PrivateConversation with a remote client -proc inviteToPrivateConversation*(self: Inbox, ds: Wakuclient, remote_static: PublicKey, remote_ephemeral: PublicKey, content: Content ) : Future[PrivateV1] {.async.} = - # Create SeedKey - # TODO: Update key derivations when noise is integrated - var local_ephemeral = generateKey() - var sk{.noInit.} : array[32, byte] = default(array[32, byte]) - - # Initialize PrivateConversation - let (convo, encPayload) = initPrivateV1Sender(self.identity, ds, remote_static, sk, content, nil) - result = convo - - # # Build Invite - let frame = newPrivateInvite(self.identity.getPubkey(), local_ephemeral.getPublicKey(), remote_static, 0, encPayload) - - # Send - await sendFrame(ds, remote_static, frame) - -## Receive am Invitation to create a new private conversation -proc createPrivateV1FromInvite*[T: ConversationStore](client: T, - invite: InvitePrivateV1) = - - let destPubkey = loadPublicKeyFromBytes(invite.initiator).valueOr: - raise newException(ValueError, "Invalid public key in intro bundle.") - - let deliveryAckCb = proc( - conversation: Conversation, - msgId: string): Future[void] {.async.} = - client.notifyDeliveryAck(conversation, msgId) - - # TODO: remove placeholder key - var key : array[32, byte] = default(array[32,byte]) - - let convo = initPrivateV1Recipient(client.identity(), client.ds, destPubkey, key, deliveryAckCb) - notice "Creating PrivateV1 conversation", client = client.getId(), - convoId = convo.getConvoId() - - convo.handleFrame(client, invite.initial_message) - - # Calling `addConversation` must only occur after the conversation is completely configured. - # The client calls the OnNewConversation callback, which returns execution to the application. - client.addConversation(convo) - -proc handleFrame*[T: ConversationStore](convo: Inbox, client: T, bytes: seq[ - byte]) = - ## Dispatcher for Incoming `InboxV1Frames`. - ## Calls further processing depending on the kind of frame. - - let enc = decode(bytes, EncryptedPayload).valueOr: - raise newException(ValueError, "Failed to decode payload") - - let frame = convo.decrypt(enc).valueOr: - error "Decrypt failed", client = client.getId(), error = error - raise newException(ValueError, "Failed to Decrypt MEssage: " & - error) - - case getKind(frame): - of typeInvitePrivateV1: - createPrivateV1FromInvite(client, frame.invitePrivateV1) - - of typeNote: - notice "Receive Note", client = client.getId(), text = frame.note.text - - -method sendMessage*(convo: Inbox, content_frame: Content) : Future[MessageId] {.async.} = - warn "Cannot send message to Inbox" - result = "program_error" diff --git a/src/chat/links.nim b/src/chat/links.nim deleted file mode 100644 index e877235..0000000 --- a/src/chat/links.nim +++ /dev/null @@ -1,58 +0,0 @@ -import base64 -import chronos -import strformat -import strutils - -import libp2p/crypto/crypto - -import ../content_types/all -import proto_types -import utils - - -################################################# -# Link Generation -################################################# - -proc toBundle*(link: string): Result[IntroBundle, string] = - # Check scheme - if not link.startsWith("wap://"): - return err("InvalidScheme") - - # Remove scheme - let path = link[6..^1] - - # Split by '/' - let parts = path.split('/') - - # Expected format: ident/{ident}/ephemeral/{ephemeral}/eid/{eid} - if parts.len != 6: - return err("InvalidFormat") - - # Validate structure - if parts[0] != "ident" or parts[2] != "ephemeral" or parts[4] != "eid": - return err("InvalidFormat") - - # Extract values - let ident = decode(parts[1]).toBytes() - let ephemeral = decode(parts[3]).toBytes() - - let eid = int32(parseInt(parts[5])) # TODO: catch parse error - - # Validate non-empty - if ident.len == 0: - return err("MissingIdent") - if ephemeral.len == 0: - return err("MissingEphemeral") - - return ok(IntroBundle( - ident: ident, - ephemeral: ephemeral, - ephemeral_id: eid - )) - - -proc toLink*(intro: IntroBundle): string = - let ident = encode(intro.ident, safe = true) - let ephemeral = intro.ephemeral.toHex() - result = fmt"wap://ident/{ident}/ephemeral/{ephemeral}/eid/{intro.ephemeral_id}" diff --git a/src/chat/proto_types.nim b/src/chat/proto_types.nim deleted file mode 100644 index a608046..0000000 --- a/src/chat/proto_types.nim +++ /dev/null @@ -1,104 +0,0 @@ -# Can this be an external package? It would be preferable to have these types -# easy to import and use. - -import protobuf_serialization # This import is needed or th macro will not work -import protobuf_serialization/proto_parser -import results -import std/random - -export protobuf_serialization - -import_proto3 "../../protos/inbox.proto" -# import_proto3 "../protos/invite.proto" // Import3 follows protobuf includes so this will result in a redefinition error -import_proto3 "../../protos/envelope.proto" - -import_proto3 "../../protos/private_v1.proto" - -type EncryptableTypes = InboxV1Frame | EncryptedPayload - -export EncryptedPayload -export InboxV1Frame -export PrivateV1Frame - -export EncryptableTypes - - - - -proc encode*(frame: object): seq[byte] = - ## Encodes the frame into a byte sequence using Protobuf serialization. - result = Protobuf.encode(frame) - - -proc decode*[T: object] (bytes: seq[byte], proto: typedesc[ - T]): Result[T, string] = - ## Encodes the frame into a byte sequence using Protobuf serialization. - - try: - result = ok(Protobuf.decode(bytes, proto)) - except ProtobufError as e: - result = err("Failed to decode payload: " & e.msg) - -type - IntroBundle {.proto3.} = object - ident* {.fieldNumber: 1.}: seq[byte] - ephemeral* {.fieldNumber: 2.}: seq[byte] - ephemeral_id* {.fieldNumber: 3.}: int32 - - -export IntroBundle - -proc generateSalt*(): uint64 = - randomize() - result = 0 - for i in 0 ..< 8: - result = result or (uint64(rand(255)) shl (i * 8)) - - -proc toEnvelope*(payload: EncryptedPayload, convo_id: string): WapEnvelopeV1 = - let bytes = encode(payload) - let salt = generateSalt() - - # TODO: Implement hinting - return WapEnvelopeV1( - payload: bytes, - salt: salt, - conversation_hint: convo_id, - ) - -########################################################### -# nim-serialize-protobuf does not support oneof fields. -# As a stop gap each object using oneof fields, needs -# a implementation to look up the type. -# -# The valid field is determined by the fields which -# is not set to the default value -########################################################### - -type - InboxV1FrameType* = enum - type_InvitePrivateV1, type_Note - -proc getKind*(obj: InboxV1Frame): InboxV1FrameType = - - if obj.invite_private_v1 != InvitePrivateV1(): - return type_InvitePrivateV1 - - if obj.note != Note(): - return type_Note - - raise newException(ValueError, "Un handled one of type") - -type - PrivateV1FrameType* = enum - type_Content, type_Placeholder - -proc getKind*(obj: PrivateV1Frame): PrivateV1FrameType = - - if obj.content != @[]: - return type_Content - - if obj.placeholder != Placeholder(): - return type_Placeholder - - raise newException(ValueError, "Un handled one of type") diff --git a/src/chat/utils.nim b/src/chat/utils.nim index d6b3a26..9be2dcf 100644 --- a/src/chat/utils.nim +++ b/src/chat/utils.nim @@ -6,7 +6,6 @@ import strutils proc getCurrentTimestamp*(): Timestamp = result = waku_core.getNanosecondTime(getTime().toUnix()) - proc hash_func*(s: string | seq[byte]): string = # This should be Blake2s but it does not exist so substituting with Blake2b result = getBlake2b(s, 4, "") @@ -17,17 +16,3 @@ proc bytesToHex*[T](bytes: openarray[T], lowercase: bool = false): string = for b in bytes: let hex = b.toHex(2) result.add(if lowercase: hex.toLower() else: hex) - -proc toBytes*(s: string): seq[byte] = - result = cast[seq[byte]](s) - -proc toUtfString*(b: seq[byte]): string = - result = cast[string](b) - -macro panic*(reason: string): untyped = - result = quote do: - let pos = instantiationInfo() - echo `reason` & " ($1:$2)" % [ - pos.filename, $pos.line] - echo "traceback:\n", getStackTrace() - quit(1) diff --git a/src/content_types/all.nim b/src/content_types/all.nim index f4b6425..0ced84a 100644 --- a/src/content_types/all.nim +++ b/src/content_types/all.nim @@ -6,8 +6,6 @@ import protobuf_serialization/proto_parser import results import strformat -import ../chat/proto_types - export protobuf_serialization diff --git a/vendor/libchat b/vendor/libchat new file mode 160000 index 0000000..d903eac --- /dev/null +++ b/vendor/libchat @@ -0,0 +1 @@ +Subproject commit d903eac011f31b9db83c0860235341d4340cf5f0 From d421690aaf6e964fa0bc24e3b8268fdf0ac6e49f Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:11:27 -0800 Subject: [PATCH 2/8] fix: sender key error (#67) --- src/chat/client.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat/client.nim b/src/chat/client.nim index c98e554..0545930 100644 --- a/src/chat/client.nim +++ b/src/chat/client.nim @@ -199,7 +199,7 @@ proc parseMessage(client: ChatClient, msg: ChatPayload) {.raises: [ValueError].} let content = opt_content.get() let convo = client.getConversation(content.conversationId) # TODO: (P1) Add sender information from LibChat. - let msg = ReceivedMessage(sender: nil,timestamp:getCurrentTimestamp(),content: content.data ) + let msg = ReceivedMessage(timestamp:getCurrentTimestamp(),content: content.data ) client.notifyNewMessage(convo, msg) else: debug "Parsed message generated no content", client=client.getId() From ecfe9218c3cf9f90bb37c0f59f5a81d1619b84cb Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:11:51 -0800 Subject: [PATCH 3/8] Remove unused submodules (#68) * Remove uneeded vendored deps * remove naxoltl * remove proto definitions --- .gitmodules | 25 ---- protos/conversations/group_v1.proto | 28 ---- protos/encryption.proto | 26 ---- protos/envelope.proto | 16 --- protos/inbox.proto | 17 --- protos/invite.proto | 14 -- protos/private_v1.proto | 19 --- protos/reliability.proto | 23 --- src/naxolotl.nim | 7 - src/naxolotl/chacha.nim | 63 --------- src/naxolotl/curve25519.nim | 123 ---------------- src/naxolotl/errors.nim | 11 -- src/naxolotl/naxolotl.nim | 210 ---------------------------- src/naxolotl/types.nim | 18 --- src/naxolotl/utils.nim | 55 -------- vendor/constantine | 1 - vendor/illwill | 1 - vendor/nim-ffi | 1 - vendor/nim-sds | 1 - vendor/nim_chacha20_poly1305 | 1 - 20 files changed, 660 deletions(-) delete mode 100644 protos/conversations/group_v1.proto delete mode 100644 protos/encryption.proto delete mode 100644 protos/envelope.proto delete mode 100644 protos/inbox.proto delete mode 100644 protos/invite.proto delete mode 100644 protos/private_v1.proto delete mode 100644 protos/reliability.proto delete mode 100644 src/naxolotl.nim delete mode 100644 src/naxolotl/chacha.nim delete mode 100644 src/naxolotl/curve25519.nim delete mode 100644 src/naxolotl/errors.nim delete mode 100644 src/naxolotl/naxolotl.nim delete mode 100644 src/naxolotl/types.nim delete mode 100644 src/naxolotl/utils.nim delete mode 160000 vendor/constantine delete mode 160000 vendor/illwill delete mode 160000 vendor/nim-ffi delete mode 160000 vendor/nim-sds delete mode 160000 vendor/nim_chacha20_poly1305 diff --git a/.gitmodules b/.gitmodules index 04c430b..b6ecc2e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,11 +6,6 @@ url = https://github.com/waku-org/nwaku.git ignore = untracked branch = master -[submodule "vendor/nim-sds"] - path = vendor/nim-sds - url = https://github.com/jazzz/nim-sds.git - ignore = untracked - branch = master [submodule "vendor/nim-protobuf-serialization"] path = vendor/nim-protobuf-serialization url = https://github.com/status-im/nim-protobuf-serialization.git @@ -26,26 +21,6 @@ url = https://github.com/narimiran/blake2.git ignore = untracked branch = master -[submodule "vendor/illwill"] - path = vendor/illwill - url = https://github.com/johnnovak/illwill.git - ignore = untracked - branch = master -[submodule "vendor/nim_chacha20_poly1305"] - path = vendor/nim_chacha20_poly1305 - url = https://github.com/lantos-lgtm/nim_chacha20_poly1305.git - ignore = untracked - branch = master -[submodule "vendor/constantine"] - path = vendor/constantine - url = https://github.com/mratsim/constantine.git - ignore = untracked - branch = master -[submodule "vendor/nim-ffi"] - path = vendor/nim-ffi - url = https://github.com/logos-messaging/nim-ffi.git - ignore = untracked - branch = master [submodule "vendor/libchat"] path = vendor/libchat url = https://github.com/logos-messaging/libchat.git diff --git a/protos/conversations/group_v1.proto b/protos/conversations/group_v1.proto deleted file mode 100644 index fde7013..0000000 --- a/protos/conversations/group_v1.proto +++ /dev/null @@ -1,28 +0,0 @@ -syntax = "proto3"; - -package wap.convos.group_v1; - -import "base.proto"; -import "common_frames.proto"; - - - -message ConversationInvite_GroupV1 { - repeated string participants = 1; -} - - - -message GroupV1Frame { - // SDS like information: Message ID and channel_id extracted for utility - string message_id = 2; - string channel_id = 3; // Channel_id is associated with a set of participants - // This conflicts with conversation based encryption, - // need to ensure the derived sender is a valid participant - base.ReliabilityInfo reliability_info = 10; - - oneof frame_type { - common_frames.ContentFrame content = 100; - // ... - } -} diff --git a/protos/encryption.proto b/protos/encryption.proto deleted file mode 100644 index cda0fb0..0000000 --- a/protos/encryption.proto +++ /dev/null @@ -1,26 +0,0 @@ -syntax = "proto3"; - -package wap.encryption; - - -// TODO: This also encompasses plaintexts, is there a better name? -// Alternatives: ??? -message EncryptedPayload { - - oneof encryption { - encryption.Plaintext plaintext = 1; - encryption.Doubleratchet doubleratchet = 2; - } -} - -message Plaintext { - bytes payload=1; -} - -message Doubleratchet { - bytes dh = 1; // 32 byte array - uint32 msgNum = 2; - uint32 prevChainLen = 3; - bytes ciphertext = 4; - string aux = 5; -} diff --git a/protos/envelope.proto b/protos/envelope.proto deleted file mode 100644 index 6a961d4..0000000 --- a/protos/envelope.proto +++ /dev/null @@ -1,16 +0,0 @@ -syntax = "proto3"; - -package wap.envelope; - - -/////////////////////////////////////////////////////////////////////////////// -// Payload Framing Messages -/////////////////////////////////////////////////////////////////////////////// - -message WapEnvelopeV1 { - - string conversation_hint = 1; - uint64 salt = 2; - - bytes payload = 5; -} diff --git a/protos/inbox.proto b/protos/inbox.proto deleted file mode 100644 index 438b977..0000000 --- a/protos/inbox.proto +++ /dev/null @@ -1,17 +0,0 @@ -syntax = "proto3"; - -package wap.inbox; - -import "invite.proto"; - -message Note{ - string text = 1; -} - -message InboxV1Frame { - string recipient = 1; - oneof frame_type { - invite.InvitePrivateV1 invite_private_v1 = 10; - Note note = 11; - } -} diff --git a/protos/invite.proto b/protos/invite.proto deleted file mode 100644 index da9f560..0000000 --- a/protos/invite.proto +++ /dev/null @@ -1,14 +0,0 @@ -syntax = "proto3"; - -package wap.invite; - -import "encryption.proto"; - -message InvitePrivateV1 { - bytes initiator = 1; - bytes initiator_ephemeral = 2; - bytes participant = 3; - int32 participant_ephemeral_id= 4; - string discriminator = 5; - encryption.EncryptedPayload initial_message = 6; -} diff --git a/protos/private_v1.proto b/protos/private_v1.proto deleted file mode 100644 index 7dd69ff..0000000 --- a/protos/private_v1.proto +++ /dev/null @@ -1,19 +0,0 @@ -syntax = "proto3"; - -package wap.convos.private_v1; - - -message Placeholder { - uint32 counter = 1; -} - -message PrivateV1Frame { - string conversation_id = 1; - bytes sender = 2; - int64 timestamp = 3; // Sender reported timestamp - oneof frame_type { - bytes content = 10; - Placeholder placeholder = 11; - // .... - } -} diff --git a/protos/reliability.proto b/protos/reliability.proto deleted file mode 100644 index f71f9b2..0000000 --- a/protos/reliability.proto +++ /dev/null @@ -1,23 +0,0 @@ -syntax = "proto3"; - -package wap.reliability; - -/////////////////////////////////////////////////////////////////////////////// -// SDS Payloads -/////////////////////////////////////////////////////////////////////////////// - -message HistoryEntry { - string message_id = 1; // Unique identifier of the SDS message, as defined in `Message` - bytes retrieval_hint = 2; // Optional information to help remote parties retrieve this SDS - // message; For example, A Waku deterministic message hash or routing payload hash - } - - message ReliablePayload { - string message_id = 2; - string channel_id = 3; - int32 lamport_timestamp = 10; - repeated HistoryEntry causal_history = 11; - bytes bloom_filter = 12; - // Optional field causes errors in nim protobuf generation. Removing for now as optional is implied anways. - bytes content = 20; - } diff --git a/src/naxolotl.nim b/src/naxolotl.nim deleted file mode 100644 index f19bbaf..0000000 --- a/src/naxolotl.nim +++ /dev/null @@ -1,7 +0,0 @@ -import naxolotl/[ - naxolotl, - curve25519, - errors -] - -export naxolotl, curve25519, NaxolotlError \ No newline at end of file diff --git a/src/naxolotl/chacha.nim b/src/naxolotl/chacha.nim deleted file mode 100644 index a20bab9..0000000 --- a/src/naxolotl/chacha.nim +++ /dev/null @@ -1,63 +0,0 @@ -import nim_chacha20_poly1305/[common, chacha20_poly1305, poly1305] -import std/[sysrand] -import results -import strformat - -import types -import errors - - -proc encryptWithChaCha20Poly1305*(msgKey: MessageKey, plaintext: var openArray[byte], associatedData: openArray[byte]) : (Nonce, CipherText) = - - var nonce : Nonce - discard urandom(nonce) - - var tag: Tag - var ciphertext = newSeq[byte](plaintext.len + tag.len) - - var counter : Counter = 0 - - # TODO: check plaintext mutability requirement - chacha20_aead_poly1305_encrypt( - Key(msgKey), - nonce, - counter, - associatedData, - plaintext, - ciphertext.toOpenArray(0, plaintext.high), - tag - ) - - # Combine tag with cipherkey for ease of transport and consistency with other implementations - copyMem(addr ciphertext[plaintext.len], unsafeAddr tag[0], tag.len) - (nonce, ciphertext) - - -proc decryptWithChaCha20Poly1305*(msgKey: MessageKey, nonce: Nonce, ciphertext: var openArray[byte], associatedData: openArray[byte]) : Result[seq[byte], NaxolotlError] = - var tag : Tag - if ciphertext.len <= tag.len: - return err(NaxolotlError(code: errInvalidInput, context: fmt"ciphertext is less than {tag.len} bytes. Expected `ciphertext || tag`" )) - - copyMem(addr tag[0], unsafeAddr ciphertext[^tag.len], tag.len) - - var plaintext = newSeq[byte](ciphertext.len - tag.len) - - var computedTag: Tag - var counter : Counter = 0 - - chacha20_aead_poly1305_decrypt( - Key(msgKey), - nonce, - counter, - associatedData, - plaintext, - ciphertext.toOpenArray(0,ciphertext.high - tag.len), - computedTag - ) - - if not poly1305_verify(tag, computedTag): - return err(NaxolotlError(code: errMessageAuthentication, context: fmt"Got Tag: {tag} expected: {computedTag}")) - - - ok(plaintext) - \ No newline at end of file diff --git a/src/naxolotl/curve25519.nim b/src/naxolotl/curve25519.nim deleted file mode 100644 index 140b928..0000000 --- a/src/naxolotl/curve25519.nim +++ /dev/null @@ -1,123 +0,0 @@ -# See https://github.com/vacp2p/nim-libp2p/blob/master/libp2p/crypto/curve25519.nim - -import bearssl/[ec, rand] -import results -from stew/assign2 import assign -export results - -const Curve25519KeySize* = 32 - -type - Curve25519* = object - Curve25519Key* = array[Curve25519KeySize, byte] - Curve25519Error* = enum - Curver25519GenError - -proc intoCurve25519Key*(s: openArray[byte]): Curve25519Key = - assert s.len == Curve25519KeySize - assign(result, s) - -proc getBytes*(key: Curve25519Key): seq[byte] = - @key - -proc byteswap(buf: var Curve25519Key) {.inline.} = - for i in 0 ..< 16: - let x = buf[i] - buf[i] = buf[31 - i] - buf[31 - i] = x - -proc mul*(_: type[Curve25519], point: var Curve25519Key, multiplier: Curve25519Key) = - let defaultBrEc = ecGetDefault() - - # multiplier needs to be big-endian - var multiplierBs = multiplier - multiplierBs.byteswap() - let res = defaultBrEc.mul( - addr point[0], - Curve25519KeySize, - addr multiplierBs[0], - Curve25519KeySize, - EC_curve25519, - ) - assert res == 1 - -proc mulgen(_: type[Curve25519], dst: var Curve25519Key, point: Curve25519Key) = - let defaultBrEc = ecGetDefault() - - var rpoint = point - rpoint.byteswap() - - let size = - defaultBrEc.mulgen(addr dst[0], addr rpoint[0], Curve25519KeySize, EC_curve25519) - - assert size == Curve25519KeySize - -proc public*(private: Curve25519Key): Curve25519Key = - Curve25519.mulgen(result, private) - -proc random*(_: type[Curve25519Key], rng: var HmacDrbgContext): Curve25519Key = - var res: Curve25519Key - let defaultBrEc = ecGetDefault() - let len = ecKeygen( - PrngClassPointerConst(addr rng.vtable), defaultBrEc, nil, addr res[0], EC_curve25519 - ) - # Per bearssl documentation, the keygen only fails if the curve is - # unrecognised - - doAssert len == Curve25519KeySize, "Could not generate curve" - - res - -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 diff --git a/src/naxolotl/errors.nim b/src/naxolotl/errors.nim deleted file mode 100644 index 57ac784..0000000 --- a/src/naxolotl/errors.nim +++ /dev/null @@ -1,11 +0,0 @@ - -type - NaxolotlError* = object of CatchableError - code*: ErrorCode - context*: string - - ErrorCode* = enum - errDecryption - errMessageAuthentication - errInvalidInput - errProgram \ No newline at end of file diff --git a/src/naxolotl/naxolotl.nim b/src/naxolotl/naxolotl.nim deleted file mode 100644 index d9058fd..0000000 --- a/src/naxolotl/naxolotl.nim +++ /dev/null @@ -1,210 +0,0 @@ -import curve25519 -import results -import chronicles -import nim_chacha20_poly1305/common -import strformat -import strutils -import sequtils -import tables - -import chacha -import types -import utils -import errors - -const maxSkip = 10 - - -type Doubleratchet* = object - dhSelf: PrivateKey - dhRemote: PublicKey - - rootKey: RootKey - chainKeySend: ChainKey - chainKeyRecv: ChainKey - - msgCountSend: MsgCount - msgCountRecv: MsgCount - prevChainLen: MsgCount - - # TODO: SkippedKeys - skippedMessageKeys: Table[(PublicKey,MsgCount), MessageKey] - -const DomainSepKdfRoot = "DoubleRatchetRootKey" - - -type DrHeader* = object - dhPublic*: PublicKey - msgNumber*: uint32 - prevChainLen*: uint32 - - - -func keyId(dh:PublicKey, recvCount: MsgCount ): KeyId = - (dh, recvCount) - -func hex(a: openArray[byte]) : string = - a.mapIt(&"{it:02X}").join("") - -proc `$`*(x: DrHeader): string = - "DrHeader(pubKey=" & hex(x.dhPublic) & ", msgNum=" & $x.msgNumber & ", msgNum=" & $x.prevChainLen & ")" - - -proc `$`*(key: array[32, byte]): string = - let byteStr = hex(key) - fmt"{byteStr[0..5]}..{byteStr[^6 .. ^1]}" - - -proc generateDhKey() : PrivateKey = - result = generateKeypair().get()[0] - -################################################# -# Kdf -################################################# - - -func kdfRoot(rootKey: RootKey, dhOutput:DhDerivedKey): (RootKey, ChainKey) = - - var salt = rootKey - var ikm = dhOutput - let info = cast[seq[byte]](DomainSepKdfRoot) - - hkdfSplit(salt, ikm, info) - -func kdfChain(chainKey: ChainKey): (MessageKey, ChainKey) = - - let msgKey = hkdfExtract(chainKey, [0x01u8]) - let chainKey = hkdfExtract(chainKey, [0x02u8]) - - return(msgKey, chainKey) - -func dhRatchetSend(self: var Doubleratchet) = - # Perform DH Ratchet step when receiving a new peer key. - let dhOutput : DhDerivedKey = dhExchange(self.dhSelf, self.dhRemote).get() - let (newRootKey, newChainKeySend) = kdfRoot(self.rootKey, dhOutput) - self.rootKey = newRootKey - self.chainKeySend = newChainKeySend - self.msgCountSend = 0 - -proc dhRatchetRecv(self: var Doubleratchet, remotePublickey: PublicKey ) = - self.prevChainLen = self.msgCountSend - self.msgCountSend = 0 - self.msgCountRecv = 0 - - self.dhRemote = remotePublickey - - let dhOutputPre = self.dhSelf.dhExchange(self.dhRemote).get() - let (newRootKey, newChainKeyRecv) = kdfRoot(self.rootKey, dhOutputPre) - self.rootKey = newRootKey - self.chainKeyRecv = newChainKeyRecv - - self.dhSelf = generateDhKey() - - let dhOutputPost = self.dhSelf.dhExchange(self.dhRemote).get() - (self.rootKey, self.chainKeySend) = kdfRoot(self.rootKey, dhOutputPost) - - -proc skipMessageKeys(self: var Doubleratchet, until: MsgCount): Result[(), string] = - - if self.msgCountRecv + maxSkip < until: - return err("Too many skipped messages") - - while self.msgCountRecv < until: - let (msgKey, chainKey) = kdfChain(self.chainKeyRecv) - self.chainKeyRecv = chainKey - - let keyId = keyId(self.dhRemote, self.msgCountRecv) - self.skippedMessageKeys[keyId] = msgKey - inc self.msgCountRecv - - ok(()) - -proc encrypt(self: var Doubleratchet, plaintext: var seq[byte], associatedData: openArray[byte]): (DrHeader, CipherText) = - - let (msgKey, chainKey) = kdfChain(self.chainKeySend) - self.chainKeySend = chainKey - let header = DrHeader( - dhPublic: self.dhSelf.public, #TODO Serialize - msgNumber: self.msgCountSend, - prevChainLen: self.prevChainLen) - - self.msgCountSend = self.msgCountSend + 1 - - - var (nonce, ciphertext) = encryptWithChaCha20Poly1305(msgKey, plaintext, associatedData) - - # TODO: optimize copies - var output : seq[byte] - output.add(nonce) - output.add(ciphertext) - - (header, output) - - -proc decrypt*(self: var Doubleratchet, header: DrHeader, ciphertext: CipherText, associatedData: openArray[byte] ) : Result[seq[byte], NaxolotlError] = - - let peerPublic = header.dhPublic - var msgKey : MessageKey - - # Check Skipped Keys - let keyId = keyId(header.dhPublic, header.msgNumber) - if self.skippedMessageKeys.hasKey(keyId): - debug "detected skipped message", keyId = keyId - msgKey = self.skippedMessageKeys[keyId] - else: - if (peerPublic != self.dhRemote): - let r = self.skipMessageKeys(header.prevChainLen) - if r.isErr: - error "skipMessages", error = r.error() - self.dhRatchetRecv(peerPublic) - let r = self.skipMessageKeys(header.msgNumber) - if r.isErr: - error "skipMessages", error = r.error() - - (msgKey, self.chainKeyRecv) = kdfChain(self.chainKeyRecv) - inc self.msgCountRecv - - var nonce : Nonce - copyMem(addr nonce[0], unsafeAddr ciphertext[0], Nonce.len) - var cipherTag = ciphertext[Nonce.len .. ^1] - - result = decryptWithChaCha20Poly1305(msgKey,nonce, cipherTag, associatedData ) - - if result.isOk: - # TODO: persist chainKey state changes - self.skippedMessageKeys.del(keyId) - - -proc encrypt*(self: var Doubleratchet, plaintext: var seq[byte]) : (DrHeader, CipherText) = - encrypt(self, plaintext,@[]) - - -proc initDoubleratchetSender*(sharedSecret: array[32, byte], dhRemote: PublicKey): Doubleratchet = - - result = Doubleratchet( - dhSelf: generateDhKey(), - dhRemote: dhRemote, - rootKey: RootKey(sharedSecret), - msgCountSend: 0, - msgCountRecv: 0, - prevChainLen: 0, - skippedMessageKeys: initTable[(PublicKey, MsgCount), MessageKey]() - ) - - # Update RK, CKs - result.dhRatchetSend() - -proc initDoubleratchetRecipient*(sharedSecret: array[32, byte], dhSelf: PrivateKey): Doubleratchet = - - result = Doubleratchet( - dhSelf: dhSelf, - #dhRemote: None, - rootKey: RootKey(sharedSecret), - msgCountSend: 0, - msgCountRecv: 0, - prevChainLen: 0, - skippedMessageKeys: initTable[(PublicKey, MsgCount), MessageKey]() - ) - -func dhSelfPublic*(self: Doubleratchet): PublicKey = - self.dhSelf.public \ No newline at end of file diff --git a/src/naxolotl/types.nim b/src/naxolotl/types.nim deleted file mode 100644 index a4504f4..0000000 --- a/src/naxolotl/types.nim +++ /dev/null @@ -1,18 +0,0 @@ -import curve25519 - -type PrivateKey* = Curve25519Key -type PublicKey* = Curve25519Key - -type RootKey* = array[32, byte] -type ChainKey* = array[32, byte] -type MessageKey* = array[32, byte] -type DhDerivedKey* = array[32, byte] - -type GenericArray* = array[32, byte] - -type CipherText* = seq[byte] - -type MsgCount* = uint32 -type KeyId* = (PublicKey, MsgCount) - -const KeyLen* = 32 \ No newline at end of file diff --git a/src/naxolotl/utils.nim b/src/naxolotl/utils.nim deleted file mode 100644 index 4ec9964..0000000 --- a/src/naxolotl/utils.nim +++ /dev/null @@ -1,55 +0,0 @@ -import constantine/hashes -import constantine/kdf/kdf_hkdf -import curve25519 -import results - -import errors -import types - - -func hkdfExtract*(key: openArray[byte], seed: openArray[byte]) : GenericArray = - - assert GenericArray.len == sha256.digestSize() - - var ctx{.noInit.}: HKDF[sha256] - var prk{.noInit.}: array[sha256.digestSize(), byte] - ctx.hkdfExtract(prk, key, seed) - - return prk - - - -func hkdfExtractExpand*(output: var openArray[byte], salt: openArray[byte], ikm: openArray[byte], info: openArray[byte] ) = - var ctx{.noInit.}: HKDF[sha256] - var prk{.noInit.}: array[sha256.digestSize(), byte] - ctx.hkdfExtract(prk, salt, ikm) - ctx.hkdfExpand(output, prk, info, true) - - -func hkdfSplit*(salt: GenericArray, ikm: GenericArray, info: openArray[byte] ) : (RootKey, ChainKey) = - - var output : array[KeyLen*2 , byte] - - hkdfExtractExpand(output, salt, ikm, info) - - var out1 : array[KeyLen, byte] - var out2 : array[KeyLen, byte] - - # Unsafe memcopy - copyMem(addr out1[0], addr output[0], KeyLen) - copyMem(addr out2[0], addr output[32], KeyLen) - - result = (out1,out2) - - - -func dhExchange*(a: PrivateKey, b: PublicKey): Result[DhDerivedKey, NaxolotlError] = - var dhOuput = b - - try: - Curve25519.mul(dhOuput, a) - except CatchableError as e: - return err(NaxolotlError( code: errProgram, context: e.msg)) - ok(DhDerivedKey(dhOuput)) - - diff --git a/vendor/constantine b/vendor/constantine deleted file mode 160000 index d6aae1e..0000000 --- a/vendor/constantine +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d6aae1eca3775d6317e11b169edef0249162ce22 diff --git a/vendor/illwill b/vendor/illwill deleted file mode 160000 index 99a120f..0000000 --- a/vendor/illwill +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 99a120f7f69868b94f5d35ce7e21dd12535de70c diff --git a/vendor/nim-ffi b/vendor/nim-ffi deleted file mode 160000 index 06111de..0000000 --- a/vendor/nim-ffi +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 06111de155253b34e47ed2aaed1d61d08d62cc1b diff --git a/vendor/nim-sds b/vendor/nim-sds deleted file mode 160000 index 6a05cfd..0000000 --- a/vendor/nim-sds +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6a05cfd2c954886ebbe46adb222ecc1dc9117fd0 diff --git a/vendor/nim_chacha20_poly1305 b/vendor/nim_chacha20_poly1305 deleted file mode 160000 index ad06aff..0000000 --- a/vendor/nim_chacha20_poly1305 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ad06aff319bdb5d61cebd56d2d0e31c3516112c6 From 79a5a329c4652009afc4adc4c27f9bcb7cbc3777 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:12:19 -0800 Subject: [PATCH 4/8] Rename Prep (#63) * Remove Nim-Chat-POC references * Update nimble file. * Remove references to ChatSDK as a product --- .gitignore | 4 ++-- DEVELOPING.md | 12 ++++++------ Makefile | 14 +++++++------- README.md | 2 +- library/liblogoschat.nim | 4 ++-- nim_chat_poc.nimble => logos_chat.nimble | 12 ++++++------ 6 files changed, 24 insertions(+), 24 deletions(-) rename nim_chat_poc.nimble => logos_chat.nimble (87%) diff --git a/.gitignore b/.gitignore index 7fe05ef..d81d275 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ /waku_vibe_template /waku_vibe_template.dSYM -/nim_chat_poc +/logos_chat *.dSYM nimble.develop nimble.paths @@ -21,7 +21,7 @@ nimble.paths /tags # a symlink that can't be added to the repo because of Windows -/nim_chat_poc.nims +/logos_chat.nims # Ignore dynamic, static libs and libtool archive files *.so diff --git a/DEVELOPING.md b/DEVELOPING.md index 32fe621..e707d70 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -5,9 +5,9 @@ ```mermaid flowchart TD - NIM("nim-chat-poc
c-bingings, process control, networking
") + NIM("Logos-Chat
c-bingings, process control, networking
") LIB("libchat
encryption, encoding
") - PROTO("chat_proto
protobufs, language specific types
") + PROTO("chat-proto
protobufs, language specific types
") LMN("logos-messaging-nim
p2p networking
") @@ -22,13 +22,13 @@ style LMN fill:#fff ## Repositories -### nim-chat-poc +### logos-chat Root of the Logos chat SDK, written in Nim. Handles async operations and bridges network operations with the cryptographic backend. **Responsibilities:** -- C bindings to libchat +- C bindings to Libchat - Async execution (Chronos) - Network integration @@ -42,12 +42,12 @@ Operates as a pipeline: accepts either encrypted payloads or plaintext content, - Encoding/decoding -### logos-messaging-nim +### logos-message-delivery P2P networking layer using logos-messaging protocol. Provides decentralized message transport. This is an external dependency. -### chat_proto +### chat-proto Protobuf definitions. Defines the protobufs used in the logos-chat protocol and provides generated types for various languages. \ No newline at end of file diff --git a/Makefile b/Makefile index 2c3f724..236d4fa 100644 --- a/Makefile +++ b/Makefile @@ -41,12 +41,12 @@ define test_name $(shell echo '$(MAKECMDGOALS)' | cut -d' ' -f3-) endef -nim_chat_poc.nims: - ln -s nim_chat_poc.nimble $@ +logos_chat.nims: + ln -s logos_chat.nimble $@ update: | update-common - rm -rf nim_chat_poc.nims && \ - $(MAKE) nim_chat_poc.nims $(HANDLE_OUTPUT) + rm -rf logos_chat.nims && \ + $(MAKE) logos_chat.nims $(HANDLE_OUTPUT) clean: rm -rf build @@ -97,7 +97,7 @@ build-libchat: .PHONY: tests tests: | build-waku-librln build-waku-nat build-libchat nim_chat_poc.nims echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim tests $(NIM_PARAMS) nim_chat_poc.nims + $(ENV_SCRIPT) nim tests $(NIM_PARAMS) logos_chat.nims ########## @@ -107,7 +107,7 @@ tests: | build-waku-librln build-waku-nat build-libchat nim_chat_poc.nims # Ensure there is a nimble task with a name that matches the target tui bot_echo pingpong: | build-waku-librln build-waku-nat build-libchat nim_chat_poc.nims echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim $@ $(NIM_PARAMS) --path:src nim_chat_poc.nims + $(ENV_SCRIPT) nim $@ $(NIM_PARAMS) --path:src logos_chat.nims ########### ## Library ## @@ -127,7 +127,7 @@ LIBLOGOSCHAT := build/liblogoschat.$(LIBLOGOSCHAT_EXT) .PHONY: liblogoschat liblogoschat: | build-waku-librln build-waku-nat build-libchat nim_chat_poc.nims echo -e $(BUILD_MSG) "$(LIBLOGOSCHAT)" && \ - $(ENV_SCRIPT) nim liblogoschat $(NIM_PARAMS) --path:src nim_chat_poc.nims && \ + $(ENV_SCRIPT) nim liblogoschat $(NIM_PARAMS) --path:src logos_chat.nims && \ echo -e "\n\x1B[92mLibrary built successfully:\x1B[39m" && \ echo " $(shell pwd)/$(LIBLOGOSCHAT)" diff --git a/README.md b/README.md index 746e9b2..0129dc9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Nim Chat POC +# Logos Chat This is the technical proof of a modular e2ee chat protocol using Waku. You can find discussion and details [here](https://github.com/waku-org/specs/pull/73) diff --git a/library/liblogoschat.nim b/library/liblogoschat.nim index 5d86a6e..f0f87f3 100644 --- a/library/liblogoschat.nim +++ b/library/liblogoschat.nim @@ -1,7 +1,7 @@ -## liblogoschat - C bindings for the Chat SDK +## liblogoschat - C bindings for Logos-Chat ## Main entry point for the shared library ## -## This library exposes the Chat SDK functionality through a C-compatible FFI interface. +## This library exposes the chat functionality through a C-compatible FFI interface. ## It uses nim-ffi for thread-safe async request handling. import std/[json, options] diff --git a/nim_chat_poc.nimble b/logos_chat.nimble similarity index 87% rename from nim_chat_poc.nimble rename to logos_chat.nimble index c10c360..cb4a705 100644 --- a/nim_chat_poc.nimble +++ b/logos_chat.nimble @@ -1,11 +1,11 @@ # Package version = "0.1.0" -author = "jazzz" -description = "An example of the chat sdk in Nim" +author = "Logos.co" +description = "LogosChat is a decentralized permissionless messaging protocol." license = "MIT" srcDir = "src" -bin = @["nim_chat_poc"] +bin = @["logos_chat"] # Dependencies @@ -76,8 +76,8 @@ task bot_echo, "Build the EchoBot example": task pingpong, "Build the Pingpong example": let name = "pingpong" - buildBinary name, "examples/", "-d:chronicles_log_level='INFO' -d:chronicles_disabled_topics='waku node' " + buildBinary name, "./", "-d:chronicles_log_level='INFO' -d:chronicles_disabled_topics='waku node' " -task liblogoschat, "Build the Chat SDK shared library (C bindings)": - buildLibrary "logoschat", "library/", +task liblogoschat, "Build the Logos-Chat shared library (C bindings)": + buildLibrary "logoschat", "library/", "-d:chronicles_log_level='INFO' -d:chronicles_enabled=on --path:src --path:vendor/nim-ffi" From 8ea018d2a9adf69a39dbb1d914ad4dc32497c1f9 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:37:18 -0800 Subject: [PATCH 5/8] Rename nims file (#70) --- Makefile | 6 +++--- logos_chat.nimble | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 236d4fa..db259d3 100644 --- a/Makefile +++ b/Makefile @@ -95,7 +95,7 @@ build-libchat: @echo "Completed building libchat" .PHONY: tests -tests: | build-waku-librln build-waku-nat build-libchat nim_chat_poc.nims +tests: | build-waku-librln build-waku-nat build-libchat logos_chat.nims echo -e $(BUILD_MSG) "build/$@" && \ $(ENV_SCRIPT) nim tests $(NIM_PARAMS) logos_chat.nims @@ -105,7 +105,7 @@ tests: | build-waku-librln build-waku-nat build-libchat nim_chat_poc.nims ########## # Ensure there is a nimble task with a name that matches the target -tui bot_echo pingpong: | build-waku-librln build-waku-nat build-libchat nim_chat_poc.nims +tui bot_echo pingpong: | build-waku-librln build-waku-nat build-libchat logos_chat.nims echo -e $(BUILD_MSG) "build/$@" && \ $(ENV_SCRIPT) nim $@ $(NIM_PARAMS) --path:src logos_chat.nims @@ -125,7 +125,7 @@ endif LIBLOGOSCHAT := build/liblogoschat.$(LIBLOGOSCHAT_EXT) .PHONY: liblogoschat -liblogoschat: | build-waku-librln build-waku-nat build-libchat nim_chat_poc.nims +liblogoschat: | build-waku-librln build-waku-nat build-libchat logos_chat.nims echo -e $(BUILD_MSG) "$(LIBLOGOSCHAT)" && \ $(ENV_SCRIPT) nim liblogoschat $(NIM_PARAMS) --path:src logos_chat.nims && \ echo -e "\n\x1B[92mLibrary built successfully:\x1B[39m" && \ diff --git a/logos_chat.nimble b/logos_chat.nimble index cb4a705..51bc072 100644 --- a/logos_chat.nimble +++ b/logos_chat.nimble @@ -76,7 +76,7 @@ task bot_echo, "Build the EchoBot example": task pingpong, "Build the Pingpong example": let name = "pingpong" - buildBinary name, "./", "-d:chronicles_log_level='INFO' -d:chronicles_disabled_topics='waku node' " + buildBinary name, "examples/", "-d:chronicles_log_level='INFO' -d:chronicles_disabled_topics='waku node' " task liblogoschat, "Build the Logos-Chat shared library (C bindings)": buildLibrary "logoschat", "library/", From 240a46e475ee89bfed0af0a28d31709208f0912d Mon Sep 17 00:00:00 2001 From: osmaczko <33099791+osmaczko@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:18:43 +0100 Subject: [PATCH 6/8] fix: update library bindings for libchat integration (#69) --- examples/cbindings/Makefile | 2 +- examples/cbindings/cbindings_chat_tui.c | 23 +++++---------------- library/api/client_api.nim | 10 --------- library/api/conversation_api.nim | 27 +++++++++---------------- library/api/identity_api.nim | 17 ++++------------ library/liblogoschat.h | 11 ++++------ library/liblogoschat.nim | 2 -- src/chat/client.nim | 6 +++++- vendor/libchat | 2 +- 9 files changed, 29 insertions(+), 71 deletions(-) diff --git a/examples/cbindings/Makefile b/examples/cbindings/Makefile index aa26a01..ed2d923 100644 --- a/examples/cbindings/Makefile +++ b/examples/cbindings/Makefile @@ -2,7 +2,7 @@ CC = gcc CFLAGS = -Wall -Wextra -I../../library -pthread -LDFLAGS = -L../../build -lchat -lncurses -Wl,-rpath,../../build +LDFLAGS = -L../../build -llogoschat -lncurses -Wl,-rpath,../../build BUILD_DIR = ../../build TARGET = $(BUILD_DIR)/cbindings_chat_tui diff --git a/examples/cbindings/cbindings_chat_tui.c b/examples/cbindings/cbindings_chat_tui.c index 16c4a9e..5bc3faf 100644 --- a/examples/cbindings/cbindings_chat_tui.c +++ b/examples/cbindings/cbindings_chat_tui.c @@ -30,9 +30,7 @@ static const size_t MAX_INPUT_LEN = 2048; // Application state structures typedef struct { char current_convo[128]; - char inbox_id[128]; char my_name[64]; - char my_address[128]; void *ctx; } ChatState; @@ -442,23 +440,13 @@ static void bundle_callback(int ret, const char *msg, size_t len, void *userData static void identity_callback(int ret, const char *msg, size_t len, void *userData) { (void)userData; (void)len; if (ret == RET_OK) { - const char *keys[] = {"name", "address"}; - char *values[] = {g_app.chat.my_name, g_app.chat.my_address}; - size_t sizes[] = {sizeof(g_app.chat.my_name), sizeof(g_app.chat.my_address)}; - json_extract(msg, keys, values, sizes, 2); - - char buf[256]; - snprintf(buf, sizeof(buf), "Identity: %s (%.24s...)", g_app.chat.my_name, g_app.chat.my_address); - add_log(buf); - } -} + const char *keys[] = {"name"}; + char *values[] = {g_app.chat.my_name}; + size_t sizes[] = {sizeof(g_app.chat.my_name)}; + json_extract(msg, keys, values, sizes, 1); -static void inbox_callback(int ret, const char *msg, size_t len, void *userData) { - (void)userData; - if (ret == RET_OK && len > 0) { - snprintf(g_app.chat.inbox_id, sizeof(g_app.chat.inbox_id), "%.*s", (int)len, msg); char buf[256]; - snprintf(buf, sizeof(buf), "Inbox: %.24s...", g_app.chat.inbox_id); + snprintf(buf, sizeof(buf), "Identity: %s", g_app.chat.my_name); add_log(buf); } } @@ -713,7 +701,6 @@ int main(int argc, char *argv[]) { add_log("Starting client..."); chat_start(g_app.chat.ctx, general_callback, NULL); chat_get_identity(g_app.chat.ctx, identity_callback, NULL); - chat_get_default_inbox_id(g_app.chat.ctx, inbox_callback, NULL); add_message("Welcome to Chat TUI!"); add_message("Type /help for commands, /quit to exit"); diff --git a/library/api/client_api.nim b/library/api/client_api.nim index 0edb798..10c8d76 100644 --- a/library/api/client_api.nim +++ b/library/api/client_api.nim @@ -7,7 +7,6 @@ import chronos import ffi import src/chat -import src/chat/proto_types import src/chat/delivery/waku_client import src/chat/identity import library/utils @@ -119,15 +118,6 @@ proc chat_get_id( let clientId = ctx.myLib[].getId() return ok(clientId) -proc chat_get_default_inbox_id( - ctx: ptr FFIContext[ChatClient], - callback: FFICallBack, - userData: pointer -) {.ffi.} = - ## Get the default inbox conversation ID - let inboxId = ctx.myLib[].defaultInboxConversationId() - return ok(inboxId) - ################################################# # Conversation List Operations ################################################# diff --git a/library/api/conversation_api.nim b/library/api/conversation_api.nim index 97d7919..faab6fd 100644 --- a/library/api/conversation_api.nim +++ b/library/api/conversation_api.nim @@ -1,14 +1,13 @@ ## Conversation API - FFI bindings for conversation operations ## Uses the {.ffi.} pragma for async request handling -import std/[json, options] +import std/options import chronicles import chronos import ffi import stew/byteutils import src/chat -import src/chat/proto_types import library/utils logScope: @@ -22,32 +21,24 @@ proc chat_new_private_conversation( ctx: ptr FFIContext[ChatClient], callback: FFICallBack, userData: pointer, - introBundleJson: cstring, + introBundleStr: cstring, contentHex: cstring ) {.ffi.} = ## Create a new private conversation with the given IntroBundle - ## introBundleJson: JSON string with {"ident": "hex...", "ephemeral": "hex..."} + ## introBundleStr: Intro bundle ASCII string as returned by chat_create_intro_bundle ## contentHex: Initial message content as hex-encoded string try: - let bundleJson = parseJson($introBundleJson) - - # Parse IntroBundle from JSON - let identBytes = hexToSeqByte(bundleJson["ident"].getStr()) - let ephemeralBytes = hexToSeqByte(bundleJson["ephemeral"].getStr()) - - let introBundle = IntroBundle( - ident: identBytes, - ephemeral: ephemeralBytes - ) - + # Convert bundle string to seq[byte] + let bundle = toBytes($introBundleStr) + # Convert hex content to bytes let content = hexToSeqByte($contentHex) - + # Create the conversation - let errOpt = await ctx.myLib[].newPrivateConversation(introBundle, content) + let errOpt = await ctx.myLib[].newPrivateConversation(bundle, content) if errOpt.isSome(): return err("failed to create conversation: " & $errOpt.get()) - + return ok("") except CatchableError as e: error "chat_new_private_conversation failed", error = e.msg diff --git a/library/api/identity_api.nim b/library/api/identity_api.nim index 2503242..439a72a 100644 --- a/library/api/identity_api.nim +++ b/library/api/identity_api.nim @@ -8,8 +8,6 @@ import ffi import stew/byteutils import src/chat -import src/chat/crypto -import src/chat/proto_types import library/utils logScope: @@ -25,12 +23,9 @@ proc chat_get_identity( userData: pointer ) {.ffi.} = ## Get the client identity - ## Returns JSON string: {"name": "...", "address": "...", "pubkey": "hex..."} - let ident = ctx.myLib[].identity() + ## Returns JSON string: {"name": "..."} let identJson = %*{ - "name": ident.getName(), - "address": ident.getAddr(), - "pubkey": ident.getPubkey().toHex() + "name": ctx.myLib[].getId() } return ok($identJson) @@ -44,11 +39,7 @@ proc chat_create_intro_bundle( userData: pointer ) {.ffi.} = ## Create an IntroBundle for initiating private conversations - ## Returns JSON string: {"ident": "hex...", "ephemeral": "hex..."} + ## Returns the intro bundle as an ASCII string (format: logos_chatintro__) let bundle = ctx.myLib[].createIntroBundle() - let bundleJson = %*{ - "ident": bundle.ident.toHex(), - "ephemeral": bundle.ephemeral.toHex() - } - return ok($bundleJson) + return ok(string.fromBytes(bundle)) diff --git a/library/liblogoschat.h b/library/liblogoschat.h index d0aac93..487a903 100644 --- a/library/liblogoschat.h +++ b/library/liblogoschat.h @@ -58,9 +58,6 @@ void set_event_callback(void *ctx, FFICallBack callback, void *userData); // Get the client's identifier int chat_get_id(void *ctx, FFICallBack callback, void *userData); -// Get the default inbox conversation ID -int chat_get_default_inbox_id(void *ctx, FFICallBack callback, void *userData); - ////////////////////////////////////////////////////////////////////////////// // Conversation Operations ////////////////////////////////////////////////////////////////////////////// @@ -75,10 +72,10 @@ int chat_get_conversation(void *ctx, FFICallBack callback, void *userData, const char *convoId); // Create a new private conversation with the given IntroBundle -// introBundleJson: JSON string with {"ident": "hex...", "ephemeral": "hex..."} +// introBundleStr: Intro bundle ASCII string as returned by chat_create_intro_bundle // contentHex: Initial message content as hex-encoded string int chat_new_private_conversation(void *ctx, FFICallBack callback, - void *userData, const char *introBundleJson, + void *userData, const char *introBundleStr, const char *contentHex); // Send a message to a conversation @@ -93,11 +90,11 @@ int chat_send_message(void *ctx, FFICallBack callback, void *userData, ////////////////////////////////////////////////////////////////////////////// // Get the client identity -// Returns JSON: {"name": "...", "address": "...", "pubkey": "hex..."} +// Returns JSON: {"name": "..."} int chat_get_identity(void *ctx, FFICallBack callback, void *userData); // Create an IntroBundle for initiating private conversations -// Returns JSON: {"ident": "hex...", "ephemeral": "hex..."} +// Returns the intro bundle as an ASCII string (format: logos_chatintro__) int chat_create_intro_bundle(void *ctx, FFICallBack callback, void *userData); #ifdef __cplusplus diff --git a/library/liblogoschat.nim b/library/liblogoschat.nim index f0f87f3..87f8de0 100644 --- a/library/liblogoschat.nim +++ b/library/liblogoschat.nim @@ -10,10 +10,8 @@ import stew/byteutils import src/chat/client, - src/chat/conversations, src/chat/identity, src/chat/delivery/waku_client, - src/chat/proto_types, library/declare_lib, library/utils diff --git a/src/chat/client.nim b/src/chat/client.nim index 0545930..3717311 100644 --- a/src/chat/client.nim +++ b/src/chat/client.nim @@ -139,7 +139,7 @@ proc notifyDeliveryAck(client: ChatClient, convo: Conversation, # Functional ################################################# -proc createIntroBundle*(self: var ChatClient): seq[byte] = +proc createIntroBundle*(self: ChatClient): seq[byte] = ## Generates an IntroBundle for the client, which includes ## the required information to send a message. result = self.libchatCtx.createIntroductionBundle().valueOr: @@ -198,6 +198,10 @@ proc parseMessage(client: ChatClient, msg: ChatPayload) {.raises: [ValueError].} if opt_content.isSome(): let content = opt_content.get() let convo = client.getConversation(content.conversationId) + + if content.isNewConvo: + client.notifyNewConversation(convo) + # TODO: (P1) Add sender information from LibChat. let msg = ReceivedMessage(timestamp:getCurrentTimestamp(),content: content.data ) client.notifyNewMessage(convo, msg) diff --git a/vendor/libchat b/vendor/libchat index d903eac..a9ca4ff 160000 --- a/vendor/libchat +++ b/vendor/libchat @@ -1 +1 @@ -Subproject commit d903eac011f31b9db83c0860235341d4340cf5f0 +Subproject commit a9ca4ffb7de90ea4cd269350c189c19fb78a2589 From 04f46d1797bd86c8169670713d68cb3946e92ec9 Mon Sep 17 00:00:00 2001 From: Patryk Osmaczko <33099791+osmaczko@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:19:07 +0100 Subject: [PATCH 7/8] fix: re-add `vendor/nim-ffi` submodule to fix C-bindings build `requires` in the nimble file alone is not sufficient, NBS never calls `nimble install` or clones URLs from the `requires` block at build time. The package must be present as a git submodule for NBS to discover and link it. Migrating to `nimble install` is a separate task. --- .gitmodules | 4 ++++ logos_chat.nimble | 4 +--- vendor/nim-ffi | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) create mode 160000 vendor/nim-ffi diff --git a/.gitmodules b/.gitmodules index b6ecc2e..34eaae6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -24,3 +24,7 @@ [submodule "vendor/libchat"] path = vendor/libchat url = https://github.com/logos-messaging/libchat.git +[submodule "vendor/nim-ffi"] + path = vendor/nim-ffi + url = https://github.com/logos-messaging/nim-ffi.git + branch = master diff --git a/logos_chat.nimble b/logos_chat.nimble index 51bc072..bcf896c 100644 --- a/logos_chat.nimble +++ b/logos_chat.nimble @@ -15,12 +15,10 @@ requires "nim >= 2.2.4", "blake2", "chronicles", "libp2p", - "nimchacha20poly1305", # TODO: remove "confutils", "eth", "regex", "web3", - "https://github.com/jazzz/nim-sds#exports", "libchat", "waku", "ffi" @@ -79,5 +77,5 @@ task pingpong, "Build the Pingpong example": buildBinary name, "examples/", "-d:chronicles_log_level='INFO' -d:chronicles_disabled_topics='waku node' " task liblogoschat, "Build the Logos-Chat shared library (C bindings)": - buildLibrary "logoschat", "library/", + buildLibrary "logoschat", "library/", "-d:chronicles_log_level='INFO' -d:chronicles_enabled=on --path:src --path:vendor/nim-ffi" diff --git a/vendor/nim-ffi b/vendor/nim-ffi new file mode 160000 index 0000000..c2c03f1 --- /dev/null +++ b/vendor/nim-ffi @@ -0,0 +1 @@ +Subproject commit c2c03f1f0f300ec3aab8b733fe97575f452f6133 From 4e6fcc7ac2456a08d94601cccafdfc1b509040ab Mon Sep 17 00:00:00 2001 From: Patryk Osmaczko <33099791+osmaczko@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:37:34 +0100 Subject: [PATCH 8/8] fix: remove stale naxolotl test src/naxolotl was deleted in #68 but test_naxolotl.nim and its entry in all_tests.nim were left behind, causing make tests to fail with "cannot open file: ../src/naxolotl". --- tests/all_tests.nim | 1 - tests/test_naxolotl.nim | 188 ---------------------------------------- 2 files changed, 189 deletions(-) delete mode 100644 tests/test_naxolotl.nim diff --git a/tests/all_tests.nim b/tests/all_tests.nim index cb136ba..c75d9cc 100644 --- a/tests/all_tests.nim +++ b/tests/all_tests.nim @@ -1,4 +1,3 @@ # import individual test suites import ./test_curve25519 -import ./test_naxolotl diff --git a/tests/test_naxolotl.nim b/tests/test_naxolotl.nim deleted file mode 100644 index 9ce0c41..0000000 --- a/tests/test_naxolotl.nim +++ /dev/null @@ -1,188 +0,0 @@ - -import unittest -import results -import random -import sequtils -import std/md5 -import strformat -import strutils - -import ../src/naxolotl -import ../src/naxolotl/utils -import ../src/naxolotl/types - - - -# 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..