Merge pull request #66 from logos-messaging/logos_chat_integration

CRC-001 - Integration
This commit is contained in:
Jazz Turner-Baggs 2026-02-18 13:53:36 -08:00 committed by GitHub
commit fb347d7974
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
60 changed files with 193 additions and 2697 deletions

4
.gitignore vendored
View File

@ -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

24
.gitmodules vendored
View File

@ -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,23 +21,10 @@
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/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
ignore = untracked
branch = master

View File

@ -5,9 +5,9 @@
```mermaid
flowchart TD
NIM("<a href="https://github.com/logos-messaging/nim-chat-poc"><b>nim-chat-poc</b><br>c-bingings, process control, networking</a>")
NIM("<a href="https://github.com/logos-messaging/logos-chat"><b>Logos-Chat</b><br>c-bingings, process control, networking</a>")
LIB("<a href="https://github.com/logos-messaging/libchat"><b>libchat</b><br>encryption, encoding</a>")
PROTO("<a href="https://github.com/logos-messaging/chat_proto"><b>chat_proto</b><br>protobufs, language specific types</a>")
PROTO("<a href="https://github.com/logos-messaging/chat-proto"><b>chat-proto</b><br>protobufs, language specific types</a>")
LMN("<a href="https://github.com/logos-messaging/logos-messaging-nim"><b>logos-messaging-nim</b><br>p2p networking</a>")
@ -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.

View File

@ -41,15 +41,16 @@ 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
cd vendor/libchat && cargo clean
# must be included after the default target
-include $(BUILD_SYSTEM_DIR)/makefiles/targets.mk
@ -86,11 +87,17 @@ 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 logos_chat.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
##########
@ -98,9 +105,9 @@ 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 logos_chat.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 ##
@ -118,9 +125,9 @@ 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 logos_chat.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)"

View File

@ -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)

View File

@ -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")

View File

@ -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()

View File

@ -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

View File

@ -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");

View File

@ -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
)

View File

@ -1,7 +0,0 @@
import chronos
import tui/tui
when isMainModule:
waitFor main()

View File

@ -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
)

View File

@ -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

View File

@ -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:
"<Unhandled Message Type>"
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..<layout.xStart+layout.width:
tb.write(x, layout.yStart, charHoriz, color, bg)
tb.write(x, layout.yStart+layout.height-1, charHoriz, color, bg)
for y in layout.yStart+1..<layout.yStart+layout.height:
tb.write(layout.xStart, y, charVert, color, bg)
tb.write(layout.xStart+layout.width-1, y, charVert, color, bg)
tb.write(layout.xStart, layout.yStart, charTopLeft, color, bg)
tb.write(layout.xStart+layout.width-1, layout.yStart, charTopRight, color, bg)
tb.write(layout.xStart, layout.yStart+layout.height-1, charBottomLeft, color, bg)
tb.write(layout.xStart+layout.width-1, layout.yStart+layout.height-1,
charBottomRight, color, bgBlack)
proc drawStatusBar(app: ChatApp, layout: Pane , fg: ForegroundColor, bg: BackgroundColor) =
var tb = app.tb
tb.setForegroundColor(fg)
tb.setBackgroundColor(bg)
for x in layout.xStart..<layout.width:
for y in layout.yStart..<layout.height:
tb.write(x, y, " ")
var i = layout.yStart + 1
var chunk = layout.width - 9
tb.write(1, i, "Name: " & app.client.getName())
inc i
tb.write(1, i, fmt"PeerCount: {app.peerCount}")
inc i
tb.write(1, i, "Link: ")
for a in 0..(app.currentInviteLink.len div chunk) :
tb.write(1+6 , i, app.currentInviteLink[a*chunk ..< min((a+1)*chunk, app.currentInviteLink.len)])
inc i
resetTuiCursor(tb)
proc drawConvoItem(app: ChatApp,
layout: Pane, convo: ConvoInfo, isSelected: bool = false) =
var tb = app.tb
let xOffset = 3
let yOffset = 1
let dt = convo.lastMsgTime
let c = if isSelected: fgMagenta else: fgCyan
if isSelected:
tb.setForegroundColor(fgMagenta)
else:
tb.setForegroundColor(fgCyan)
drawOutline(tb, layout, c)
tb.write(layout.xStart+xOffset, layout.yStart+yOffset, convo.name)
tb.write(layout.xStart+xOffset, layout.yStart+yOffset+1,dt.format("yyyy-MM-dd HH:mm:ss"))
proc drawConversationPane( app: ChatApp, layout: Pane) =
var a = layout
a.width -= 2
a.height = 6
for convo in app.mostRecentConvos():
drawConvoItem(app, a, convo, app.selectedConv == convo.name)
a.yStart = a.yStart + a.height
proc splitAt(s: string, index: int): (string, string) =
let splitIndex = min(index, s.len)
return (s[0..<splitIndex], s[splitIndex..^1])
# Eww
proc drawMsgPane( app: ChatApp, layout: Pane) =
var tb = app.tb
drawOutline(tb, layout, fgYellow)
var convo = app.getSelectedConvo()
let xStart = layout.xStart+1
let yStart = layout.yStart+1
let w = layout.width
let h = layout.height
let maxContentWidth = w - 10
let xEnd = layout.xStart + maxContentWidth
let yEnd = layout.yStart + layout.height - 2
tb.setForegroundColor(fgGreen)
let x = xStart + 2
if not convo.isTooLong:
var y = yStart
for i in 0..convo.messages.len-1:
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)
if y > 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..<layout.xStart+layout.width:
for y in layout.yStart..<layout.yStart+layout.height:
tb.write(x, y, " ", color)
tb.setForegroundColor(fgBlack)
tb.setBackgroundColor(bgGreen)
tb.write(layout.xStart + 2, layout.yStart + 2, "Paste Invite")
tb.setForegroundColor(fgWhite)
tb.setBackgroundColor(bgBlack)
let inputLine = 5
for i in layout.xStart+3..<layout.xStart+layout.width-3:
for y in (layout.yStart + inputLine - 1)..<(layout.yStart+inputLine + 2):
tb.write(i, y, " ")
# Draw input prompt
tb.write(layout.xStart+5, layout.yStart+inputLine, "> " & 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

View File

@ -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 = "<anonymous>"
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")

View File

@ -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
#################################################

View File

@ -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

View File

@ -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_<version>_<base64url payload>)
let bundle = ctx.myLib[].createIntroBundle()
let bundleJson = %*{
"ident": bundle.ident.toHex(),
"ephemeral": bundle.ephemeral.toHex()
}
return ok($bundleJson)
return ok(string.fromBytes(bundle))

View File

@ -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_<version>_<base64url payload>)
int chat_create_intro_bundle(void *ctx, FFICallBack callback, void *userData);
#ifdef __cplusplus

View File

@ -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]
@ -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

View File

@ -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
@ -15,12 +15,11 @@ 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"
@ -77,6 +76,6 @@ task pingpong, "Build the Pingpong example":
let name = "pingpong"
buildBinary name, "examples/", "-d:chronicles_log_level='INFO' -d:chronicles_disabled_topics='waku node' "
task liblogoschat, "Build the Chat SDK shared library (C bindings)":
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"

View File

@ -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;
// ...
}
}

View File

@ -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;
}

View File

@ -1,16 +0,0 @@
syntax = "proto3";
package wap.envelope;
///////////////////////////////////////////////////////////////////////////////
// Payload Framing Messages
///////////////////////////////////////////////////////////////////////////////
message WapEnvelopeV1 {
string conversation_hint = 1;
uint64 salt = 2;
bytes payload = 5;
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
// ....
}
}

View File

@ -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;
}

View File

@ -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.`$`

View File

@ -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: 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,37 @@ 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)
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)
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 +229,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 +252,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()

View File

@ -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)

View File

@ -1,5 +0,0 @@
import
./conversations/[convo_type, private_v1, message]
export private_v1, convo_type, message

View File

@ -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)

View File

@ -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")

View File

@ -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]

View File

@ -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)

View File

@ -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]}"

View File

@ -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)

View File

@ -9,6 +9,7 @@ type
errTypeError
errWrapped
errTopic
errLibChat
proc `$`*(x: ChatError): string =
fmt"ChatError(code={$x.code}, context: {x.context})"

View File

@ -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]}"

View File

@ -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"

View File

@ -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}"

View File

@ -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")

View File

@ -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)

View File

@ -6,8 +6,6 @@ import protobuf_serialization/proto_parser
import results
import strformat
import ../chat/proto_types
export protobuf_serialization

View File

@ -1,7 +0,0 @@
import naxolotl/[
naxolotl,
curve25519,
errors
]
export naxolotl, curve25519, NaxolotlError

View File

@ -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)

View File

@ -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

View File

@ -1,11 +0,0 @@
type
NaxolotlError* = object of CatchableError
code*: ErrorCode
context*: string
ErrorCode* = enum
errDecryption
errMessageAuthentication
errInvalidInput
errProgram

View File

@ -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

View File

@ -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

View File

@ -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))

View File

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

View File

@ -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..<N:
result[i] = byte(parseHexInt(hexStr[2*i .. 2*i+1]))
func loadTestKeys() : (array[32,byte],array[32,byte],array[32,byte],array[32,byte]) =
let a_priv = hexToArray[32](ks7748_a_priv)
let a_pub = hexToArray[32](ks7748_a_pub)
let b_priv = hexToArray[32](ks7748_b_priv)
let b_pub = hexToArray[32](ks7748_b_pub)
(a_priv, a_pub, b_priv, b_pub)
proc createTestInstances(b: array[32, byte], bpub: array[32, byte],sk: array[32, byte]) : (Doubleratchet, Doubleratchet) =
let adr = initDoubleratchetSender(sk, bpub)
let bdr = initDoubleratchetRecipient(sk, b)
(adr,bdr)
suite "Doubleratchet":
test "roundtrip":
let (a_priv, a_pub, b_priv, b_pub) = loadTestKeys()
let sk = hexToArray[32](ks7748_shared_key)
var (adr, bdr) = createTestInstances(b_priv, b_pub, sk)
var msg :seq[byte] = @[1,2,3,4,5,6,7,8,9,10]
let (header, ciphertext) = adr.encrypt(msg)
let r = bdr.decrypt(header, ciphertext, @[])
assert r.isOk()
assert r.get() == msg
test "skipped_msg":
let (a_priv, a_pub, b_priv, b_pub) = loadTestKeys()
let sk = hexToArray[32](ks7748_shared_key)
var (adr, bdr) = createTestInstances(b_priv, b_pub, sk)
var msg0 :seq[byte] = @[1,2,3,4,5,6,7,8,9,10]
var msg1 :seq[byte] = @[6,7,8,9,10,1,2,3,4,5]
var m :seq[byte] = @[9,10,1,2,3,4,5,6,7,8]
discard adr.encrypt(msg0)
discard adr.encrypt(msg1)
let (header, ciphertext) = adr.encrypt(m)
let r = bdr.decrypt(header, ciphertext, @[])
assert r.isOk()
let recv_msg = r.get()
assert recv_msg == m
test "out of order":
let (a_priv, a_pub, b_priv, b_pub) = loadTestKeys()
let sk = hexToArray[32](ks7748_shared_key)
var (adr, bdr) = createTestInstances(b_priv, b_pub, sk)
var msg : seq[ seq[byte]]= @[
@[1,2,3,4,5,6,7,8,9,10],
@[43,32,1,2],
@[212,122,0,21,23,4,5,71,29,84,167,31,0,1,92,211,5,53,65],
@[122,0,21,23,4,5,71,29,84,167,31,0,1,92,211,5,53,65,212],
@[14,35,17,22,14,63,34,0,1,217,218,29,51,69,33],
]
var ciphertexts : seq[(DrHeader, seq[byte])] = @[]
for m in mitems(msg):
ciphertexts.add(adr.encrypt(m))
randomize(1233) # make reproducible
var indicies = toSeq(0..msg.high)
shuffle(indicies)
for i in indicies:
var m = msg[i]
var (header, ciphertext) = ciphertexts[i]
let r = bdr.decrypt(header, ciphertext, @[])
assert r.isOk()
assert r.get() == m
test "tag mismatch":
let (a_priv, a_pub, b_priv, b_pub) = loadTestKeys()
let sk = hexToArray[32](ks7748_shared_key)
var (adr, bdr) = createTestInstances(b_priv, b_pub, sk)
var msg :seq[byte] = @[1,2,3,4,5,6,7,8,9,10]
var (header, ciphertext) = adr.encrypt(msg)
# Modify the tag
ciphertext[^4] = ciphertext[^4] xor 1
let r = bdr.decrypt(header, ciphertext, @[])
assert r.isErr()
test "decryption failure":
let (a_priv, a_pub, b_priv, b_pub) = loadTestKeys()
let sk = hexToArray[32](ks7748_shared_key)
var (adr, bdr) = createTestInstances(b_priv, b_pub, sk)
var msg :seq[byte] = @[1,2,3,4,5,6,7,8,9,10]
var (header, ciphertext) = adr.encrypt(msg)
ciphertext[15] = ciphertext[15] xor 1
let r = bdr.decrypt(header, ciphertext, @[])
assert r.isErr()
test "dh_key_updates":
let (a_priv, a_pub, b_priv, b_pub) = loadTestKeys()
let sk = hexToArray[32](ks7748_shared_key)
var (adr, bdr) = createTestInstances(b_priv, b_pub, sk)
var last_dh_a : PublicKey
var last_dh_b : PublicKey
proc step(src: var DoubleRatchet, dst: var DoubleRatchet, m: var seq[byte], ) : PublicKey =
let (header, ciphertext) = src.encrypt(m)
let r = dst.decrypt(header, ciphertext, @[])
assert m == r.get()
return header.dhPublic
for i in 0..10:
var ma = toMD5(fmt"M{i}_a").toSeq()
var mb = toMD5(fmt"M{i}_b").toSeq()
let dh_a = step(adr, bdr, ma)
let dh_b = step(bdr, adr, mb)
assert dh_a != last_dh_a
assert dh_b != last_dh_b
assert dh_a != dh_b
last_dh_a = dh_a
last_dh_b = dh_b

1
vendor/constantine vendored

@ -1 +0,0 @@
Subproject commit d6aae1eca3775d6317e11b169edef0249162ce22

1
vendor/illwill vendored

@ -1 +0,0 @@
Subproject commit 99a120f7f69868b94f5d35ce7e21dd12535de70c

1
vendor/libchat vendored Submodule

@ -0,0 +1 @@
Subproject commit a9ca4ffb7de90ea4cd269350c189c19fb78a2589

2
vendor/nim-ffi vendored

@ -1 +1 @@
Subproject commit 06111de155253b34e47ed2aaed1d61d08d62cc1b
Subproject commit c2c03f1f0f300ec3aab8b733fe97575f452f6133

1
vendor/nim-sds vendored

@ -1 +0,0 @@
Subproject commit 6a05cfd2c954886ebbe46adb222ecc1dc9117fd0

@ -1 +0,0 @@
Subproject commit ad06aff319bdb5d61cebd56d2d0e31c3516112c6