mirror of
https://github.com/logos-messaging/logos-chat.git
synced 2026-03-07 16:03:07 +00:00
Merge branch 'main' into jazzz/ci_action_push
This commit is contained in:
commit
464fffc0e7
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,6 +1,6 @@
|
|||||||
/waku_vibe_template
|
/waku_vibe_template
|
||||||
/waku_vibe_template.dSYM
|
/waku_vibe_template.dSYM
|
||||||
/nim_chat_poc
|
/logos_chat
|
||||||
*.dSYM
|
*.dSYM
|
||||||
nimble.develop
|
nimble.develop
|
||||||
nimble.paths
|
nimble.paths
|
||||||
@ -21,7 +21,7 @@ nimble.paths
|
|||||||
/tags
|
/tags
|
||||||
|
|
||||||
# a symlink that can't be added to the repo because of Windows
|
# 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
|
# Ignore dynamic, static libs and libtool archive files
|
||||||
*.so
|
*.so
|
||||||
|
|||||||
24
.gitmodules
vendored
24
.gitmodules
vendored
@ -6,11 +6,6 @@
|
|||||||
url = https://github.com/waku-org/nwaku.git
|
url = https://github.com/waku-org/nwaku.git
|
||||||
ignore = untracked
|
ignore = untracked
|
||||||
branch = master
|
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"]
|
[submodule "vendor/nim-protobuf-serialization"]
|
||||||
path = vendor/nim-protobuf-serialization
|
path = vendor/nim-protobuf-serialization
|
||||||
url = https://github.com/status-im/nim-protobuf-serialization.git
|
url = https://github.com/status-im/nim-protobuf-serialization.git
|
||||||
@ -26,23 +21,10 @@
|
|||||||
url = https://github.com/narimiran/blake2.git
|
url = https://github.com/narimiran/blake2.git
|
||||||
ignore = untracked
|
ignore = untracked
|
||||||
branch = master
|
branch = master
|
||||||
[submodule "vendor/illwill"]
|
[submodule "vendor/libchat"]
|
||||||
path = vendor/illwill
|
path = vendor/libchat
|
||||||
url = https://github.com/johnnovak/illwill.git
|
url = https://github.com/logos-messaging/libchat.git
|
||||||
ignore = untracked
|
|
||||||
branch = master
|
|
||||||
[submodule "vendor/nim_chacha20_poly1305"]
|
|
||||||
path = vendor/nim_chacha20_poly1305
|
|
||||||
url = https://github.com/lantos-lgtm/nim_chacha20_poly1305.git
|
|
||||||
ignore = untracked
|
|
||||||
branch = master
|
|
||||||
[submodule "vendor/constantine"]
|
|
||||||
path = vendor/constantine
|
|
||||||
url = https://github.com/mratsim/constantine.git
|
|
||||||
ignore = untracked
|
|
||||||
branch = master
|
|
||||||
[submodule "vendor/nim-ffi"]
|
[submodule "vendor/nim-ffi"]
|
||||||
path = vendor/nim-ffi
|
path = vendor/nim-ffi
|
||||||
url = https://github.com/logos-messaging/nim-ffi.git
|
url = https://github.com/logos-messaging/nim-ffi.git
|
||||||
ignore = untracked
|
|
||||||
branch = master
|
branch = master
|
||||||
|
|||||||
@ -5,9 +5,9 @@
|
|||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
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>")
|
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>")
|
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
|
## Repositories
|
||||||
|
|
||||||
### nim-chat-poc
|
### logos-chat
|
||||||
Root of the Logos chat SDK, written in Nim.
|
Root of the Logos chat SDK, written in Nim.
|
||||||
|
|
||||||
Handles async operations and bridges network operations with the cryptographic backend.
|
Handles async operations and bridges network operations with the cryptographic backend.
|
||||||
|
|
||||||
**Responsibilities:**
|
**Responsibilities:**
|
||||||
- C bindings to libchat
|
- C bindings to Libchat
|
||||||
- Async execution (Chronos)
|
- Async execution (Chronos)
|
||||||
- Network integration
|
- Network integration
|
||||||
|
|
||||||
@ -42,12 +42,12 @@ Operates as a pipeline: accepts either encrypted payloads or plaintext content,
|
|||||||
- Encoding/decoding
|
- Encoding/decoding
|
||||||
|
|
||||||
|
|
||||||
### logos-messaging-nim
|
### logos-message-delivery
|
||||||
P2P networking layer using logos-messaging protocol.
|
P2P networking layer using logos-messaging protocol.
|
||||||
|
|
||||||
Provides decentralized message transport. This is an external dependency.
|
Provides decentralized message transport. This is an external dependency.
|
||||||
|
|
||||||
### chat_proto
|
### chat-proto
|
||||||
Protobuf definitions.
|
Protobuf definitions.
|
||||||
|
|
||||||
Defines the protobufs used in the logos-chat protocol and provides generated types for various languages.
|
Defines the protobufs used in the logos-chat protocol and provides generated types for various languages.
|
||||||
29
Makefile
29
Makefile
@ -41,15 +41,16 @@ define test_name
|
|||||||
$(shell echo '$(MAKECMDGOALS)' | cut -d' ' -f3-)
|
$(shell echo '$(MAKECMDGOALS)' | cut -d' ' -f3-)
|
||||||
endef
|
endef
|
||||||
|
|
||||||
nim_chat_poc.nims:
|
logos_chat.nims:
|
||||||
ln -s nim_chat_poc.nimble $@
|
ln -s logos_chat.nimble $@
|
||||||
|
|
||||||
update: | update-common
|
update: | update-common
|
||||||
rm -rf nim_chat_poc.nims && \
|
rm -rf logos_chat.nims && \
|
||||||
$(MAKE) nim_chat_poc.nims $(HANDLE_OUTPUT)
|
$(MAKE) logos_chat.nims $(HANDLE_OUTPUT)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf build
|
rm -rf build
|
||||||
|
cd vendor/libchat && cargo clean
|
||||||
|
|
||||||
# must be included after the default target
|
# must be included after the default target
|
||||||
-include $(BUILD_SYSTEM_DIR)/makefiles/targets.mk
|
-include $(BUILD_SYSTEM_DIR)/makefiles/targets.mk
|
||||||
@ -86,11 +87,17 @@ build-waku-nat:
|
|||||||
@echo "Start building waku nat-libs"
|
@echo "Start building waku nat-libs"
|
||||||
$(MAKE) -C vendor/nwaku nat-libs
|
$(MAKE) -C vendor/nwaku nat-libs
|
||||||
@echo "Completed building 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
|
.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/$@" && \
|
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
|
# 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/$@" && \
|
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 ##
|
## Library ##
|
||||||
@ -118,9 +125,9 @@ endif
|
|||||||
LIBLOGOSCHAT := build/liblogoschat.$(LIBLOGOSCHAT_EXT)
|
LIBLOGOSCHAT := build/liblogoschat.$(LIBLOGOSCHAT_EXT)
|
||||||
|
|
||||||
.PHONY: liblogoschat
|
.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)" && \
|
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 -e "\n\x1B[92mLibrary built successfully:\x1B[39m" && \
|
||||||
echo " $(shell pwd)/$(LIBLOGOSCHAT)"
|
echo " $(shell pwd)/$(LIBLOGOSCHAT)"
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
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)
|
||||||
|
|
||||||
|
|||||||
@ -5,3 +5,6 @@ for dir in walkDir(thisDir() / "vendor"):
|
|||||||
if dir.kind == pcDir:
|
if dir.kind == pcDir:
|
||||||
switch("path", dir.path)
|
switch("path", dir.path)
|
||||||
switch("path", dir.path / "src")
|
switch("path", dir.path / "src")
|
||||||
|
|
||||||
|
switch("path", thisDir() / "vendor/libchat/nim-bindings")
|
||||||
|
switch("path", thisDir() / "vendor/libchat/nim-bindings/src")
|
||||||
@ -24,7 +24,7 @@ proc main() {.async.} =
|
|||||||
await chatClient.start()
|
await chatClient.start()
|
||||||
|
|
||||||
info "EchoBot started"
|
info "EchoBot started"
|
||||||
info "Invite", link=chatClient.createIntroBundle().toLink()
|
info "Invite", link=chatClient.createIntroBundle()
|
||||||
|
|
||||||
when isMainModule:
|
when isMainModule:
|
||||||
waitFor main()
|
waitFor main()
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
CC = gcc
|
CC = gcc
|
||||||
CFLAGS = -Wall -Wextra -I../../library -pthread
|
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
|
BUILD_DIR = ../../build
|
||||||
TARGET = $(BUILD_DIR)/cbindings_chat_tui
|
TARGET = $(BUILD_DIR)/cbindings_chat_tui
|
||||||
|
|||||||
@ -30,9 +30,7 @@ static const size_t MAX_INPUT_LEN = 2048;
|
|||||||
// Application state structures
|
// Application state structures
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char current_convo[128];
|
char current_convo[128];
|
||||||
char inbox_id[128];
|
|
||||||
char my_name[64];
|
char my_name[64];
|
||||||
char my_address[128];
|
|
||||||
void *ctx;
|
void *ctx;
|
||||||
} ChatState;
|
} 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) {
|
static void identity_callback(int ret, const char *msg, size_t len, void *userData) {
|
||||||
(void)userData; (void)len;
|
(void)userData; (void)len;
|
||||||
if (ret == RET_OK) {
|
if (ret == RET_OK) {
|
||||||
const char *keys[] = {"name", "address"};
|
const char *keys[] = {"name"};
|
||||||
char *values[] = {g_app.chat.my_name, g_app.chat.my_address};
|
char *values[] = {g_app.chat.my_name};
|
||||||
size_t sizes[] = {sizeof(g_app.chat.my_name), sizeof(g_app.chat.my_address)};
|
size_t sizes[] = {sizeof(g_app.chat.my_name)};
|
||||||
json_extract(msg, keys, values, sizes, 2);
|
json_extract(msg, keys, values, sizes, 1);
|
||||||
|
|
||||||
char buf[256];
|
|
||||||
snprintf(buf, sizeof(buf), "Identity: %s (%.24s...)", g_app.chat.my_name, g_app.chat.my_address);
|
|
||||||
add_log(buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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];
|
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);
|
add_log(buf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -713,7 +701,6 @@ int main(int argc, char *argv[]) {
|
|||||||
add_log("Starting client...");
|
add_log("Starting client...");
|
||||||
chat_start(g_app.chat.ctx, general_callback, NULL);
|
chat_start(g_app.chat.ctx, general_callback, NULL);
|
||||||
chat_get_identity(g_app.chat.ctx, identity_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("Welcome to Chat TUI!");
|
||||||
add_message("Type /help for commands, /quit to exit");
|
add_message("Type /help for commands, /quit to exit");
|
||||||
|
|||||||
@ -5,8 +5,6 @@ import strformat
|
|||||||
import chat
|
import chat
|
||||||
import content_types
|
import content_types
|
||||||
|
|
||||||
# TEsting
|
|
||||||
import ../src/chat/crypto
|
|
||||||
|
|
||||||
|
|
||||||
proc getContent(content: ContentFrame): string =
|
proc getContent(content: ContentFrame): string =
|
||||||
@ -35,39 +33,38 @@ proc main() {.async.} =
|
|||||||
var saro = newClient(waku_saro, Identity(name: "saro", privateKey: sKey))
|
var saro = newClient(waku_saro, Identity(name: "saro", privateKey: sKey))
|
||||||
var raya = newClient(waku_raya, Identity(name: "raya", privateKey: rKey))
|
var raya = newClient(waku_raya, Identity(name: "raya", privateKey: rKey))
|
||||||
|
|
||||||
var ri = 0
|
# Wire Saro Callbacks
|
||||||
# Wire Callbacks
|
saro.onNewMessage(proc(convo: Conversation, msg: ReceivedMessage) {.async, closure.} =
|
||||||
saro.onNewMessage(proc(convo: Conversation, msg: ReceivedMessage) {.async.} =
|
|
||||||
let contentFrame = msg.content.fromBytes()
|
let contentFrame = msg.content.fromBytes()
|
||||||
echo " Saro <------ :: " & getContent(contentFrame)
|
notice " Saro <------ ", content = getContent(contentFrame)
|
||||||
await sleepAsync(5000.milliseconds)
|
await sleepAsync(1000.milliseconds)
|
||||||
discard await convo.sendMessage(initTextFrame("Ping").toContentFrame().toBytes())
|
discard await convo.sendMessage(initTextFrame("Ping").toContentFrame().toBytes())
|
||||||
|
)
|
||||||
)
|
|
||||||
|
|
||||||
saro.onDeliveryAck(proc(convo: Conversation, msgId: string) {.async.} =
|
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.} =
|
raya.onNewMessage(proc(convo: Conversation,msg: ReceivedMessage) {.async.} =
|
||||||
let contentFrame = msg.content.fromBytes()
|
let contentFrame = msg.content.fromBytes()
|
||||||
echo fmt" ------> Raya :: from:{msg.sender} " & getContent(contentFrame)
|
notice " ------> Raya :: from: ", content= getContent(contentFrame)
|
||||||
await sleepAsync(500.milliseconds)
|
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)
|
await sleepAsync(800.milliseconds)
|
||||||
discard await convo.sendMessage(initTextFrame("Pong" & $ri).toContentFrame().toBytes())
|
discard await convo.sendMessage(initTextFrame("Pang" & $i).toContentFrame().toBytes())
|
||||||
await sleepAsync(500.milliseconds)
|
inc i
|
||||||
discard await convo.sendMessage(initTextFrame("Pong" & $ri).toContentFrame().toBytes())
|
|
||||||
inc ri
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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.} =
|
raya.onDeliveryAck(proc(convo: Conversation, msgId: string) {.async.} =
|
||||||
echo " raya -- Read Receipt for " & msgId
|
echo " raya -- Read Receipt for " & msgId
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
import chronos
|
|
||||||
|
|
||||||
import tui/tui
|
|
||||||
|
|
||||||
|
|
||||||
when isMainModule:
|
|
||||||
waitFor main()
|
|
||||||
@ -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
|
|
||||||
)
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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")
|
|
||||||
@ -7,7 +7,6 @@ import chronos
|
|||||||
import ffi
|
import ffi
|
||||||
|
|
||||||
import src/chat
|
import src/chat
|
||||||
import src/chat/proto_types
|
|
||||||
import src/chat/delivery/waku_client
|
import src/chat/delivery/waku_client
|
||||||
import src/chat/identity
|
import src/chat/identity
|
||||||
import library/utils
|
import library/utils
|
||||||
@ -119,15 +118,6 @@ proc chat_get_id(
|
|||||||
let clientId = ctx.myLib[].getId()
|
let clientId = ctx.myLib[].getId()
|
||||||
return ok(clientId)
|
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
|
# Conversation List Operations
|
||||||
#################################################
|
#################################################
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
## Conversation API - FFI bindings for conversation operations
|
## Conversation API - FFI bindings for conversation operations
|
||||||
## Uses the {.ffi.} pragma for async request handling
|
## Uses the {.ffi.} pragma for async request handling
|
||||||
|
|
||||||
import std/[json, options]
|
import std/options
|
||||||
import chronicles
|
import chronicles
|
||||||
import chronos
|
import chronos
|
||||||
import ffi
|
import ffi
|
||||||
import stew/byteutils
|
import stew/byteutils
|
||||||
|
|
||||||
import src/chat
|
import src/chat
|
||||||
import src/chat/proto_types
|
|
||||||
import library/utils
|
import library/utils
|
||||||
|
|
||||||
logScope:
|
logScope:
|
||||||
@ -22,32 +21,24 @@ proc chat_new_private_conversation(
|
|||||||
ctx: ptr FFIContext[ChatClient],
|
ctx: ptr FFIContext[ChatClient],
|
||||||
callback: FFICallBack,
|
callback: FFICallBack,
|
||||||
userData: pointer,
|
userData: pointer,
|
||||||
introBundleJson: cstring,
|
introBundleStr: cstring,
|
||||||
contentHex: cstring
|
contentHex: cstring
|
||||||
) {.ffi.} =
|
) {.ffi.} =
|
||||||
## Create a new private conversation with the given IntroBundle
|
## 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
|
## contentHex: Initial message content as hex-encoded string
|
||||||
try:
|
try:
|
||||||
let bundleJson = parseJson($introBundleJson)
|
# Convert bundle string to seq[byte]
|
||||||
|
let bundle = toBytes($introBundleStr)
|
||||||
# Parse IntroBundle from JSON
|
|
||||||
let identBytes = hexToSeqByte(bundleJson["ident"].getStr())
|
|
||||||
let ephemeralBytes = hexToSeqByte(bundleJson["ephemeral"].getStr())
|
|
||||||
|
|
||||||
let introBundle = IntroBundle(
|
|
||||||
ident: identBytes,
|
|
||||||
ephemeral: ephemeralBytes
|
|
||||||
)
|
|
||||||
|
|
||||||
# Convert hex content to bytes
|
# Convert hex content to bytes
|
||||||
let content = hexToSeqByte($contentHex)
|
let content = hexToSeqByte($contentHex)
|
||||||
|
|
||||||
# Create the conversation
|
# Create the conversation
|
||||||
let errOpt = await ctx.myLib[].newPrivateConversation(introBundle, content)
|
let errOpt = await ctx.myLib[].newPrivateConversation(bundle, content)
|
||||||
if errOpt.isSome():
|
if errOpt.isSome():
|
||||||
return err("failed to create conversation: " & $errOpt.get())
|
return err("failed to create conversation: " & $errOpt.get())
|
||||||
|
|
||||||
return ok("")
|
return ok("")
|
||||||
except CatchableError as e:
|
except CatchableError as e:
|
||||||
error "chat_new_private_conversation failed", error = e.msg
|
error "chat_new_private_conversation failed", error = e.msg
|
||||||
|
|||||||
@ -8,8 +8,6 @@ import ffi
|
|||||||
import stew/byteutils
|
import stew/byteutils
|
||||||
|
|
||||||
import src/chat
|
import src/chat
|
||||||
import src/chat/crypto
|
|
||||||
import src/chat/proto_types
|
|
||||||
import library/utils
|
import library/utils
|
||||||
|
|
||||||
logScope:
|
logScope:
|
||||||
@ -25,12 +23,9 @@ proc chat_get_identity(
|
|||||||
userData: pointer
|
userData: pointer
|
||||||
) {.ffi.} =
|
) {.ffi.} =
|
||||||
## Get the client identity
|
## Get the client identity
|
||||||
## Returns JSON string: {"name": "...", "address": "...", "pubkey": "hex..."}
|
## Returns JSON string: {"name": "..."}
|
||||||
let ident = ctx.myLib[].identity()
|
|
||||||
let identJson = %*{
|
let identJson = %*{
|
||||||
"name": ident.getName(),
|
"name": ctx.myLib[].getId()
|
||||||
"address": ident.getAddr(),
|
|
||||||
"pubkey": ident.getPubkey().toHex()
|
|
||||||
}
|
}
|
||||||
return ok($identJson)
|
return ok($identJson)
|
||||||
|
|
||||||
@ -44,11 +39,7 @@ proc chat_create_intro_bundle(
|
|||||||
userData: pointer
|
userData: pointer
|
||||||
) {.ffi.} =
|
) {.ffi.} =
|
||||||
## Create an IntroBundle for initiating private conversations
|
## 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 bundle = ctx.myLib[].createIntroBundle()
|
||||||
let bundleJson = %*{
|
return ok(string.fromBytes(bundle))
|
||||||
"ident": bundle.ident.toHex(),
|
|
||||||
"ephemeral": bundle.ephemeral.toHex()
|
|
||||||
}
|
|
||||||
return ok($bundleJson)
|
|
||||||
|
|
||||||
|
|||||||
@ -58,9 +58,6 @@ void set_event_callback(void *ctx, FFICallBack callback, void *userData);
|
|||||||
// Get the client's identifier
|
// Get the client's identifier
|
||||||
int chat_get_id(void *ctx, FFICallBack callback, void *userData);
|
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
|
// Conversation Operations
|
||||||
//////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
@ -75,10 +72,10 @@ int chat_get_conversation(void *ctx, FFICallBack callback, void *userData,
|
|||||||
const char *convoId);
|
const char *convoId);
|
||||||
|
|
||||||
// Create a new private conversation with the given IntroBundle
|
// 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
|
// contentHex: Initial message content as hex-encoded string
|
||||||
int chat_new_private_conversation(void *ctx, FFICallBack callback,
|
int chat_new_private_conversation(void *ctx, FFICallBack callback,
|
||||||
void *userData, const char *introBundleJson,
|
void *userData, const char *introBundleStr,
|
||||||
const char *contentHex);
|
const char *contentHex);
|
||||||
|
|
||||||
// Send a message to a conversation
|
// Send a message to a conversation
|
||||||
@ -93,11 +90,11 @@ int chat_send_message(void *ctx, FFICallBack callback, void *userData,
|
|||||||
//////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// Get the client identity
|
// Get the client identity
|
||||||
// Returns JSON: {"name": "...", "address": "...", "pubkey": "hex..."}
|
// Returns JSON: {"name": "..."}
|
||||||
int chat_get_identity(void *ctx, FFICallBack callback, void *userData);
|
int chat_get_identity(void *ctx, FFICallBack callback, void *userData);
|
||||||
|
|
||||||
// Create an IntroBundle for initiating private conversations
|
// 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);
|
int chat_create_intro_bundle(void *ctx, FFICallBack callback, void *userData);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
## liblogoschat - C bindings for the Chat SDK
|
## liblogoschat - C bindings for Logos-Chat
|
||||||
## Main entry point for the shared library
|
## 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.
|
## It uses nim-ffi for thread-safe async request handling.
|
||||||
|
|
||||||
import std/[json, options]
|
import std/[json, options]
|
||||||
@ -10,10 +10,8 @@ import stew/byteutils
|
|||||||
|
|
||||||
import
|
import
|
||||||
src/chat/client,
|
src/chat/client,
|
||||||
src/chat/conversations,
|
|
||||||
src/chat/identity,
|
src/chat/identity,
|
||||||
src/chat/delivery/waku_client,
|
src/chat/delivery/waku_client,
|
||||||
src/chat/proto_types,
|
|
||||||
library/declare_lib,
|
library/declare_lib,
|
||||||
library/utils
|
library/utils
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
# Package
|
# Package
|
||||||
|
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
author = "jazzz"
|
author = "Logos.co"
|
||||||
description = "An example of the chat sdk in Nim"
|
description = "LogosChat is a decentralized permissionless messaging protocol."
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
srcDir = "src"
|
srcDir = "src"
|
||||||
bin = @["nim_chat_poc"]
|
bin = @["logos_chat"]
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
|
|
||||||
@ -15,12 +15,11 @@ requires "nim >= 2.2.4",
|
|||||||
"blake2",
|
"blake2",
|
||||||
"chronicles",
|
"chronicles",
|
||||||
"libp2p",
|
"libp2p",
|
||||||
"nimchacha20poly1305", # TODO: remove
|
|
||||||
"confutils",
|
"confutils",
|
||||||
"eth",
|
"eth",
|
||||||
"regex",
|
"regex",
|
||||||
"web3",
|
"web3",
|
||||||
"https://github.com/jazzz/nim-sds#exports",
|
"libchat",
|
||||||
"waku",
|
"waku",
|
||||||
"ffi"
|
"ffi"
|
||||||
|
|
||||||
@ -77,6 +76,6 @@ task pingpong, "Build the Pingpong example":
|
|||||||
let name = "pingpong"
|
let name = "pingpong"
|
||||||
buildBinary name, "examples/", "-d:chronicles_log_level='INFO' -d:chronicles_disabled_topics='waku node' "
|
buildBinary name, "examples/", "-d:chronicles_log_level='INFO' -d:chronicles_disabled_topics='waku node' "
|
||||||
|
|
||||||
task liblogoschat, "Build the Chat SDK shared library (C bindings)":
|
task liblogoschat, "Build the Logos-Chat shared library (C bindings)":
|
||||||
buildLibrary "logoschat", "library/",
|
buildLibrary "logoschat", "library/",
|
||||||
"-d:chronicles_log_level='INFO' -d:chronicles_enabled=on --path:src --path:vendor/nim-ffi"
|
"-d:chronicles_log_level='INFO' -d:chronicles_enabled=on --path:src --path:vendor/nim-ffi"
|
||||||
@ -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;
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
syntax = "proto3";
|
|
||||||
|
|
||||||
package wap.envelope;
|
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Payload Framing Messages
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
message WapEnvelopeV1 {
|
|
||||||
|
|
||||||
string conversation_hint = 1;
|
|
||||||
uint64 salt = 2;
|
|
||||||
|
|
||||||
bytes payload = 5;
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
// ....
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -1,17 +1,13 @@
|
|||||||
import chat/[
|
import chat/[
|
||||||
client,
|
client,
|
||||||
crypto,
|
|
||||||
conversations,
|
|
||||||
delivery/waku_client,
|
delivery/waku_client,
|
||||||
identity,
|
identity,
|
||||||
links,
|
|
||||||
types
|
types
|
||||||
]
|
]
|
||||||
|
|
||||||
export client, conversations, identity, links, waku_client
|
export client, identity, waku_client
|
||||||
|
export identity.`$`
|
||||||
|
|
||||||
#export specific frames need by applications
|
#export specific frames need by applications
|
||||||
export MessageId
|
export MessageId
|
||||||
|
|
||||||
export toHex
|
|
||||||
export crypto.`$`
|
|
||||||
|
|||||||
@ -6,24 +6,15 @@
|
|||||||
import # Foreign
|
import # Foreign
|
||||||
chronicles,
|
chronicles,
|
||||||
chronos,
|
chronos,
|
||||||
sds,
|
libchat,
|
||||||
sequtils,
|
std/options,
|
||||||
std/tables,
|
|
||||||
std/sequtils,
|
|
||||||
strformat,
|
strformat,
|
||||||
strutils,
|
|
||||||
tables,
|
|
||||||
types
|
types
|
||||||
|
|
||||||
import #local
|
import #local
|
||||||
conversations,
|
|
||||||
conversations/convo_impl,
|
|
||||||
crypto,
|
|
||||||
delivery/waku_client,
|
delivery/waku_client,
|
||||||
errors,
|
errors,
|
||||||
identity,
|
identity,
|
||||||
inbox,
|
|
||||||
proto_types,
|
|
||||||
types,
|
types,
|
||||||
utils
|
utils
|
||||||
|
|
||||||
@ -35,6 +26,27 @@ logScope:
|
|||||||
# Definitions
|
# 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
|
type
|
||||||
MessageCallback* = proc(conversation: Conversation, msg: ReceivedMessage): Future[void] {.async.}
|
MessageCallback* = proc(conversation: Conversation, msg: ReceivedMessage): Future[void] {.async.}
|
||||||
NewConvoCallback* = proc(conversation: Conversation): Future[void] {.async.}
|
NewConvoCallback* = proc(conversation: Conversation): Future[void] {.async.}
|
||||||
@ -48,13 +60,11 @@ type KeyEntry* = object
|
|||||||
timestamp: int64
|
timestamp: int64
|
||||||
|
|
||||||
type ChatClient* = ref object
|
type ChatClient* = ref object
|
||||||
ident: Identity
|
libchatCtx: LibChat
|
||||||
ds*: WakuClient
|
ds*: WakuClient
|
||||||
keyStore: Table[string, KeyEntry] # Keyed by HexEncoded Public Key
|
id: string
|
||||||
conversations: Table[string, Conversation] # Keyed by conversation ID
|
|
||||||
inboundQueue: QueueRef
|
inboundQueue: QueueRef
|
||||||
isRunning: bool
|
isRunning: bool
|
||||||
inbox: Inbox
|
|
||||||
|
|
||||||
newMessageCallbacks: seq[MessageCallback]
|
newMessageCallbacks: seq[MessageCallback]
|
||||||
newConvoCallbacks: seq[NewConvoCallback]
|
newConvoCallbacks: seq[NewConvoCallback]
|
||||||
@ -64,30 +74,24 @@ type ChatClient* = ref object
|
|||||||
# Constructors
|
# Constructors
|
||||||
#################################################
|
#################################################
|
||||||
|
|
||||||
proc newClient*(ds: WakuClient, ident: Identity): ChatClient {.raises: [IOError,
|
proc newClient*(ds: WakuClient, ident: Identity): ChatClient {.raises: [IOError, ValueError].} =
|
||||||
ValueError, SerializationError].} =
|
|
||||||
## Creates new instance of a `ChatClient` with a given `WakuConfig`
|
## 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:
|
try:
|
||||||
let rm = newReliabilityManager().valueOr:
|
|
||||||
raise newException(ValueError, fmt"SDS InitializationError")
|
|
||||||
|
|
||||||
let defaultInbox = initInbox(ident)
|
|
||||||
|
|
||||||
var q = QueueRef(queue: newAsyncQueue[ChatPayload](10))
|
var q = QueueRef(queue: newAsyncQueue[ChatPayload](10))
|
||||||
var c = ChatClient(ident: ident,
|
var c = ChatClient(
|
||||||
|
libchatCtx: newConversationsContext(),
|
||||||
ds: ds,
|
ds: ds,
|
||||||
keyStore: initTable[string, KeyEntry](),
|
id: ident.getName(),
|
||||||
conversations: initTable[string, Conversation](),
|
|
||||||
inboundQueue: q,
|
inboundQueue: q,
|
||||||
isRunning: false,
|
isRunning: false,
|
||||||
inbox: defaultInbox,
|
|
||||||
newMessageCallbacks: @[],
|
newMessageCallbacks: @[],
|
||||||
newConvoCallbacks: @[])
|
newConvoCallbacks: @[])
|
||||||
|
|
||||||
c.conversations[defaultInbox.id()] = defaultInbox
|
|
||||||
|
|
||||||
notice "Client started", client = c.ident.getName(),
|
notice "Client started", client = c.id
|
||||||
defaultInbox = defaultInbox, inTopic= topic_inbox(c.ident.get_addr())
|
|
||||||
result = c
|
result = c
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error "newCLient", err = e.msg
|
error "newCLient", err = e.msg
|
||||||
@ -97,27 +101,12 @@ proc newClient*(ds: WakuClient, ident: Identity): ChatClient {.raises: [IOError,
|
|||||||
#################################################
|
#################################################
|
||||||
|
|
||||||
proc getId*(client: ChatClient): string =
|
proc getId*(client: ChatClient): string =
|
||||||
result = client.ident.getName()
|
result = client.id
|
||||||
|
|
||||||
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]))
|
|
||||||
|
|
||||||
|
|
||||||
proc listConversations*(client: ChatClient): seq[Conversation] =
|
proc listConversations*(client: ChatClient): seq[Conversation] =
|
||||||
result = toSeq(client.conversations.values())
|
# TODO: (P1) Implement list conversations
|
||||||
|
result = @[]
|
||||||
|
|
||||||
#################################################
|
#################################################
|
||||||
# Callback Handling
|
# Callback Handling
|
||||||
@ -135,6 +124,7 @@ proc onNewConversation*(client: ChatClient, callback: NewConvoCallback) =
|
|||||||
|
|
||||||
proc notifyNewConversation(client: ChatClient, convo: Conversation) =
|
proc notifyNewConversation(client: ChatClient, convo: Conversation) =
|
||||||
for cb in client.newConvoCallbacks:
|
for cb in client.newConvoCallbacks:
|
||||||
|
debug "calling OnConvo CB", client=client.getId(), len = client.newConvoCallbacks.len()
|
||||||
discard cb(convo)
|
discard cb(convo)
|
||||||
|
|
||||||
proc onDeliveryAck*(client: ChatClient, callback: DeliveryAckCallback) =
|
proc onDeliveryAck*(client: ChatClient, callback: DeliveryAckCallback) =
|
||||||
@ -149,51 +139,47 @@ proc notifyDeliveryAck(client: ChatClient, convo: Conversation,
|
|||||||
# Functional
|
# Functional
|
||||||
#################################################
|
#################################################
|
||||||
|
|
||||||
proc createIntroBundle*(self: var ChatClient): IntroBundle =
|
proc createIntroBundle*(self: ChatClient): seq[byte] =
|
||||||
## Generates an IntroBundle for the client, which includes
|
## Generates an IntroBundle for the client, which includes
|
||||||
## the required information to send a message.
|
## the required information to send a message.
|
||||||
|
result = self.libchatCtx.createIntroductionBundle().valueOr:
|
||||||
# Create Ephemeral keypair, save it in the key store
|
error "could not create bundle",error=error, client = self.getId()
|
||||||
let ephemeralKey = generateKey()
|
return
|
||||||
|
|
||||||
self.keyStore[ephemeralKey.getPublicKey().bytes().bytesToHex()] = KeyEntry(
|
|
||||||
keyType: "ephemeral",
|
|
||||||
privateKey: ephemeralKey,
|
|
||||||
timestamp: getCurrentTimestamp()
|
|
||||||
)
|
|
||||||
|
|
||||||
result = IntroBundle(
|
|
||||||
ident: @(self.ident.getPubkey().bytes()),
|
|
||||||
ephemeral: @(ephemeralKey.getPublicKey().bytes()),
|
|
||||||
)
|
|
||||||
|
|
||||||
notice "IntroBundleCreated", client = self.getId(),
|
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
|
# 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 =
|
proc getConversation*(client: ChatClient, convoId: string): Conversation =
|
||||||
notice "Get conversation", client = client.getId(), convoId = convoId
|
result = Conversation(ctx:client.libchatCtx, convoId:convoId, ds: client.ds, convo_type: PrivateV1)
|
||||||
result = client.conversations[convoId]
|
|
||||||
|
|
||||||
proc newPrivateConversation*(client: ChatClient,
|
proc newPrivateConversation*(client: ChatClient,
|
||||||
introBundle: IntroBundle, content: Content): Future[Option[ChatError]] {.async.} =
|
introBundle: seq[byte], 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()
|
|
||||||
|
|
||||||
let convo = await client.inbox.inviteToPrivateConversation(client.ds,remote_pubkey, remote_ephemeralkey, content )
|
let res = client.libchatCtx.createNewPrivateConvo(introBundle, content)
|
||||||
client.addConversation(convo) # TODO: Fix re-entrantancy bug. Convo needs to be saved before payload is sent.
|
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)
|
return none(ChatError)
|
||||||
|
|
||||||
|
|
||||||
@ -202,31 +188,37 @@ proc newPrivateConversation*(client: ChatClient,
|
|||||||
# Receives a incoming payload, decodes it, and processes it.
|
# Receives a incoming payload, decodes it, and processes it.
|
||||||
#################################################
|
#################################################
|
||||||
|
|
||||||
proc parseMessage(client: ChatClient, msg: ChatPayload) {.raises: [ValueError,
|
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
|
|
||||||
|
|
||||||
try:
|
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:
|
except Exception as e:
|
||||||
error "HandleFrame Failed", error = e.msg
|
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
|
# Async Tasks
|
||||||
#################################################
|
#################################################
|
||||||
@ -237,20 +229,9 @@ proc messageQueueConsumer(client: ChatClient) {.async.} =
|
|||||||
|
|
||||||
while client.isRunning:
|
while client.isRunning:
|
||||||
let message = await client.inboundQueue.queue.get()
|
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))
|
client.parseMessage(message)
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
#################################################
|
#################################################
|
||||||
@ -271,5 +252,6 @@ proc start*(client: ChatClient) {.async.} =
|
|||||||
proc stop*(client: ChatClient) {.async.} =
|
proc stop*(client: ChatClient) {.async.} =
|
||||||
## Stop the client.
|
## Stop the client.
|
||||||
await client.ds.stop()
|
await client.ds.stop()
|
||||||
|
client.libchatCtx.destroy()
|
||||||
client.isRunning = false
|
client.isRunning = false
|
||||||
notice "Client stopped", client = client.getId()
|
notice "Client stopped", client = client.getId()
|
||||||
|
|||||||
@ -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)
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
import
|
|
||||||
./conversations/[convo_type, private_v1, message]
|
|
||||||
|
|
||||||
|
|
||||||
export private_v1, convo_type, message
|
|
||||||
@ -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)
|
|
||||||
|
|
||||||
|
|
||||||
@ -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")
|
|
||||||
@ -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]
|
|
||||||
|
|
||||||
|
|
||||||
@ -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)
|
|
||||||
@ -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]}"
|
|
||||||
@ -20,7 +20,6 @@ import
|
|||||||
waku_filter_v2/client,
|
waku_filter_v2/client,
|
||||||
]
|
]
|
||||||
|
|
||||||
import ../proto_types
|
|
||||||
|
|
||||||
logScope:
|
logScope:
|
||||||
topics = "chat waku"
|
topics = "chat waku"
|
||||||
@ -72,9 +71,9 @@ proc DefaultConfig*(): WakuConfig =
|
|||||||
shardId: @[shardId], pubsubTopic: &"/waku/2/rs/{clusterId}/{shardId}",
|
shardId: @[shardId], pubsubTopic: &"/waku/2/rs/{clusterId}/{shardId}",
|
||||||
staticPeers: @[])
|
staticPeers: @[])
|
||||||
|
|
||||||
proc sendPayload*(client: WakuClient, contentTopic: string,
|
|
||||||
env: WapEnvelopeV1) {.async.} =
|
proc sendBytes*(client: WakuClient, contentTopic: string,
|
||||||
let bytes = encode(env)
|
bytes: seq[byte]) {.async.} =
|
||||||
|
|
||||||
let msg = WakuMessage(contentTopic: contentTopic, payload: bytes)
|
let msg = WakuMessage(contentTopic: contentTopic, payload: bytes)
|
||||||
let res = await client.node.publish(some(PubsubTopic(client.cfg.pubsubTopic)), msg)
|
let res = await client.node.publish(some(PubsubTopic(client.cfg.pubsubTopic)), msg)
|
||||||
|
|||||||
@ -9,6 +9,7 @@ type
|
|||||||
errTypeError
|
errTypeError
|
||||||
errWrapped
|
errWrapped
|
||||||
errTopic
|
errTopic
|
||||||
|
errLibChat
|
||||||
|
|
||||||
proc `$`*(x: ChatError): string =
|
proc `$`*(x: ChatError): string =
|
||||||
fmt"ChatError(code={$x.code}, context: {x.context})"
|
fmt"ChatError(code={$x.code}, context: {x.context})"
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
|
|
||||||
import crypto
|
import crypto/ecdh
|
||||||
import results
|
import results
|
||||||
|
import strformat
|
||||||
|
import utils
|
||||||
|
|
||||||
|
export PublicKey, PrivateKey, loadPrivateKeyFromBytes, loadPublicKeyFromBytes
|
||||||
|
|
||||||
|
|
||||||
type
|
type
|
||||||
@ -28,6 +32,13 @@ proc getPubkey*(self: Identity): PublicKey =
|
|||||||
proc getAddr*(self: Identity): string =
|
proc getAddr*(self: Identity): string =
|
||||||
result = get_addr(self.getPubKey())
|
result = get_addr(self.getPubKey())
|
||||||
|
|
||||||
|
|
||||||
proc getName*(self: Identity): string =
|
proc getName*(self: Identity): string =
|
||||||
result = self.name
|
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]}"
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
|
||||||
@ -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}"
|
|
||||||
@ -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")
|
|
||||||
@ -6,7 +6,6 @@ import strutils
|
|||||||
proc getCurrentTimestamp*(): Timestamp =
|
proc getCurrentTimestamp*(): Timestamp =
|
||||||
result = waku_core.getNanosecondTime(getTime().toUnix())
|
result = waku_core.getNanosecondTime(getTime().toUnix())
|
||||||
|
|
||||||
|
|
||||||
proc hash_func*(s: string | seq[byte]): string =
|
proc hash_func*(s: string | seq[byte]): string =
|
||||||
# This should be Blake2s but it does not exist so substituting with Blake2b
|
# This should be Blake2s but it does not exist so substituting with Blake2b
|
||||||
result = getBlake2b(s, 4, "")
|
result = getBlake2b(s, 4, "")
|
||||||
@ -17,17 +16,3 @@ proc bytesToHex*[T](bytes: openarray[T], lowercase: bool = false): string =
|
|||||||
for b in bytes:
|
for b in bytes:
|
||||||
let hex = b.toHex(2)
|
let hex = b.toHex(2)
|
||||||
result.add(if lowercase: hex.toLower() else: hex)
|
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)
|
|
||||||
|
|||||||
@ -6,8 +6,6 @@ import protobuf_serialization/proto_parser
|
|||||||
import results
|
import results
|
||||||
import strformat
|
import strformat
|
||||||
|
|
||||||
import ../chat/proto_types
|
|
||||||
|
|
||||||
|
|
||||||
export protobuf_serialization
|
export protobuf_serialization
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
import naxolotl/[
|
|
||||||
naxolotl,
|
|
||||||
curve25519,
|
|
||||||
errors
|
|
||||||
]
|
|
||||||
|
|
||||||
export naxolotl, curve25519, NaxolotlError
|
|
||||||
@ -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)
|
|
||||||
|
|
||||||
@ -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
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
|
|
||||||
type
|
|
||||||
NaxolotlError* = object of CatchableError
|
|
||||||
code*: ErrorCode
|
|
||||||
context*: string
|
|
||||||
|
|
||||||
ErrorCode* = enum
|
|
||||||
errDecryption
|
|
||||||
errMessageAuthentication
|
|
||||||
errInvalidInput
|
|
||||||
errProgram
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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))
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
# import individual test suites
|
# import individual test suites
|
||||||
|
|
||||||
import ./test_curve25519
|
import ./test_curve25519
|
||||||
import ./test_naxolotl
|
|
||||||
|
|||||||
@ -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
vendor/constantine
vendored
@ -1 +0,0 @@
|
|||||||
Subproject commit d6aae1eca3775d6317e11b169edef0249162ce22
|
|
||||||
1
vendor/illwill
vendored
1
vendor/illwill
vendored
@ -1 +0,0 @@
|
|||||||
Subproject commit 99a120f7f69868b94f5d35ce7e21dd12535de70c
|
|
||||||
1
vendor/libchat
vendored
Submodule
1
vendor/libchat
vendored
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit a9ca4ffb7de90ea4cd269350c189c19fb78a2589
|
||||||
2
vendor/nim-ffi
vendored
2
vendor/nim-ffi
vendored
@ -1 +1 @@
|
|||||||
Subproject commit 06111de155253b34e47ed2aaed1d61d08d62cc1b
|
Subproject commit c2c03f1f0f300ec3aab8b733fe97575f452f6133
|
||||||
1
vendor/nim-sds
vendored
1
vendor/nim-sds
vendored
@ -1 +0,0 @@
|
|||||||
Subproject commit 6a05cfd2c954886ebbe46adb222ecc1dc9117fd0
|
|
||||||
1
vendor/nim_chacha20_poly1305
vendored
1
vendor/nim_chacha20_poly1305
vendored
@ -1 +0,0 @@
|
|||||||
Subproject commit ad06aff319bdb5d61cebd56d2d0e31c3516112c6
|
|
||||||
Loading…
x
Reference in New Issue
Block a user