Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,13 @@
url = https://github.com/johnnovak/illwill.git
ignore = untracked
branch = master
[submodule "vendor/nim_chacha20_poly1305"]
path = vendor/nim_chacha20_poly1305
url = https://github.com/lantos-lgtm/nim_chacha20_poly1305.git
ignore = untracked
branch = master
[submodule "vendor/constantine"]
path = vendor/constantine
url = https://github.com/mratsim/constantine.git
ignore = untracked
branch = master
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ nim_chat_poc: | build-waku-librln build-waku-nat nim_chat_poc.nims
$(ENV_SCRIPT) nim nim_chat_poc $(NIM_PARAMS) nim_chat_poc.nims

# Ensure there is a nimble task with a name that matches the target
tui bot_echo: | build-waku-librln build-waku-nat nim_chat_poc.nims
tui bot_echo pingpong: | build-waku-librln build-waku-nat nim_chat_poc.nims
echo -e $(BUILD_MSG) "build/$@" && \
$(ENV_SCRIPT) nim $@ $(NIM_PARAMS) --path:src nim_chat_poc.nims

Expand Down
39 changes: 30 additions & 9 deletions examples/pingpong.nim
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import chronicles
import chronos
import strformat

import chat_sdk
import content_types

# TEsting
import ../src/chat_sdk/crypto


proc getContent(content: ContentFrame): string =
# Skip type checks and assume its a TextFrame
let m = decode(content.bytes, TextFrame).valueOr:
raise newException(ValueError, fmt"Badly formed Content")
return fmt"{m}"
raise newException(ValueError, fmt"Badly formed Content")
return fmt"{m}"

proc main() {.async.} =

Expand All @@ -21,15 +25,22 @@ proc main() {.async.} =
cfg_saro.staticPeers.add(cfg_raya.getMultiAddr())
cfg_raya.staticPeers.add(cfg_saro.getMultiAddr())

let sKey = loadPrivateKeyFromBytes(@[45u8, 216, 160, 24, 19, 207, 193, 214, 98, 92, 153, 145, 222, 247, 101, 99, 96, 131, 149, 185, 33, 187, 229, 251, 100, 158, 20, 131, 111, 97, 181, 210]).get()
let rKey = loadPrivateKeyFromBytes(@[43u8, 12, 160, 51, 212, 90, 199, 160, 154, 164, 129, 229, 147, 69, 151, 17, 239, 51, 190, 33, 86, 164, 50, 105, 39, 250, 182, 116, 138, 132, 114, 234]).get()

let sIdent = Identity(name: "saro", privateKey: sKey)

# Create Clients
var saro = newClient(cfg_saro, createIdentity("Saro"))
var raya = newClient(cfg_raya, createIdentity("Raya"))
var saro = newClient(cfg_saro, sIdent)
var raya = newClient(cfg_raya, Identity(name: "raya", privateKey: rKey))

var ri = 0
# Wire Callbacks
saro.onNewMessage(proc(convo: Conversation, msg: ContentFrame) {.async.} =
echo " Saro <------ :: " & getContent(msg)
await sleepAsync(10000)
await sleepAsync(5000)
discard await convo.sendMessage(saro.ds, initTextFrame("Ping").toContentFrame())

)

saro.onDeliveryAck(proc(convo: Conversation, msgId: string) {.async.} =
Expand All @@ -41,8 +52,13 @@ proc main() {.async.} =

raya.onNewMessage(proc(convo: Conversation, msg: ContentFrame) {.async.} =
echo " ------> Raya :: " & getContent(msg)
await sleepAsync(10000)
sdiscard await convo.sendMessage(raya.ds, initTextFrame("Pong").toContentFrame())
await sleepAsync(500)
discard await convo.sendMessage(raya.ds, initTextFrame("Pong" & $ri).toContentFrame())
await sleepAsync(800)
discard await convo.sendMessage(raya.ds, initTextFrame("Pong" & $ri).toContentFrame())
await sleepAsync(500)
discard await convo.sendMessage(raya.ds, initTextFrame("Pong" & $ri).toContentFrame())
inc ri
)

raya.onNewConversation(proc(convo: Conversation) {.async.} =
Expand All @@ -63,7 +79,12 @@ proc main() {.async.} =
let raya_bundle = raya.createIntroBundle()
discard await saro.newPrivateConversation(raya_bundle)

await sleepAsync(10000) # Run for some time
await sleepAsync(20000) # Run for some time

saro.stop()
raya.stop()
raya.stop()


when isMainModule:
waitFor main()
notice "Shutdown"
4 changes: 4 additions & 0 deletions nim_chat_poc.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,7 @@ task tui, "Build Waku based simple example":
task bot_echo, "Build the EchoBot example":
let name = "bot_echo"
buildBinary name, "examples/", "-d:chronicles_log_level='INFO' -d:chronicles_disabled_topics='waku node' "

task pingpong, "Build the Pingpong example":
let name = "pingpong"
buildBinary name, "examples/", "-d:chronicles_log_level='INFO' -d:chronicles_disabled_topics='waku node' "
12 changes: 7 additions & 5 deletions protos/encryption.proto
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@ message EncryptedPayload {

oneof encryption {
encryption.Plaintext plaintext = 1;
encryption.Ecies ecies = 2;
encryption.Doubleratchet doubleratchet = 2;
}
}

message Plaintext {
bytes payload=1;
}

message Ecies {
bytes encrypted_bytes=1;
bytes ephemeral_pubkey = 2;
bytes tag = 3;
message Doubleratchet {
bytes dh = 1; // 32 byte array
uint32 msgNum = 2;
uint32 prevChainLen = 3;
bytes ciphertext = 4;
string aux = 5;
}
7 changes: 6 additions & 1 deletion src/chat_sdk/client.nim
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import #local
conversations/convo_impl,
crypto,
delivery/waku_client,
errors,
identity,
inbox,
proto_types,
Expand Down Expand Up @@ -215,7 +216,11 @@ proc newPrivateConversation*(client: Client,
msgId: string): Future[void] {.async.} =
client.notifyDeliveryAck(conversation, msgId)

let convo = initPrivateV1(client.identity(), destPubkey, "default", deliveryAckCb)
# TODO: remove placeholder key
var key : array[32, byte]
key[2]=2

var convo = initPrivateV1Sender(client.identity(), destPubkey, key, deliveryAckCb)
client.addConversation(convo)

# TODO: Subscribe to new content topic
Expand Down
64 changes: 51 additions & 13 deletions src/chat_sdk/conversations/private_v1.nim
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@ import ../delivery/waku_client

import ../[
identity,
errors,
proto_types,
types,
utils
]
import convo_type

import ../../naxolotl as nax


type
PrivateV1* = ref object of Conversation
Expand All @@ -29,6 +32,7 @@ type
topic: string
participants: seq[PublicKey]
discriminator: string
doubleratchet: naxolotl.Doubleratchet

proc getTopic*(self: PrivateV1): string =
## Returns the topic for the PrivateV1 conversation.
Expand Down Expand Up @@ -56,11 +60,32 @@ proc calcMsgId(self: PrivateV1, msgBytes: seq[byte]): string =
result = getBlake2b(s, 16, "")


proc encrypt*(convo: PrivateV1, frame: PrivateV1Frame): EncryptedPayload =
result = EncryptedPayload(plaintext: Plaintext(payload: encode(frame)))
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 decrypt*(convo: PrivateV1, enc: EncryptedPayload): PrivateV1Frame =
result = decode(enc.plaintext.payload, PrivateV1Frame).get()


proc wireCallbacks(convo: PrivateV1, deliveryAckCb: proc(
Expand Down Expand Up @@ -91,8 +116,8 @@ proc wireCallbacks(convo: PrivateV1, deliveryAckCb: proc(



proc initPrivateV1*(owner: Identity, participant: PublicKey,
discriminator: string = "default", deliveryAckCb: proc(
proc initPrivateV1*(owner: Identity, participant: PublicKey, seedKey: array[32, byte],
discriminator: string = "default", isSender: bool, deliveryAckCb: proc(
conversation: Conversation,
msgId: string): Future[void] {.async.} = nil):
PrivateV1 =
Expand All @@ -107,27 +132,37 @@ proc initPrivateV1*(owner: Identity, participant: PublicKey,
owner: owner,
topic: derive_topic(participants, discriminator),
participants: participants,
discriminator: discriminator
discriminator: discriminator,
doubleratchet: initDoubleratchet(seedKey, owner.privateKey.bytes, participant.bytes, isSender)
)

result.wireCallbacks(deliveryAckCb)

result.sdsClient.ensureChannel(result.getConvoId()).isOkOr:
raise newException(ValueError, "bad sds channel")


proc initPrivateV1Sender*(owner:Identity, participant: PublicKey, seedKey: array[32, byte], deliveryAckCb: proc(
conversation: Conversation, msgId: string): Future[void] {.async.} = nil): PrivateV1 =
initPrivateV1(owner, participant, seedKey, "default", true, deliveryAckCb)

proc initPrivateV1Recipient*(owner:Identity, participant: PublicKey, seedKey: array[32, byte], deliveryAckCb: proc(
conversation: Conversation, msgId: string): Future[void] {.async.} = nil): PrivateV1 =
initPrivateV1(owner, participant, seedKey, "default", false, deliveryAckCb)


proc sendFrame(self: PrivateV1, ds: WakuClient,
msg: PrivateV1Frame): Future[MessageId]{.async.} =

let frameBytes = encode(msg)
let msgId = self.calcMsgId(frameBytes)
let sdsPayload = self.sdsClient.wrapOutgoingMessage(frameBytes, msgId,
var sdsPayload = self.sdsClient.wrapOutgoingMessage(frameBytes, msgId,
self.getConvoId()).valueOr:
raise newException(ValueError, fmt"sds wrapOutgoingMessage failed: {repr(error)}")

let encryptedBytes = EncryptedPayload(plaintext: Plaintext(
payload: sdsPayload))
let encryptedPayload = self.encrypt(sdsPayload)

discard ds.sendPayload(self.getTopic(), encryptedBytes.toEnvelope(
discard ds.sendPayload(self.getTopic(), encryptedPayload.toEnvelope(
self.getConvoId()))

result = msgId
Expand All @@ -144,9 +179,12 @@ proc handleFrame*[T: ConversationStore](convo: PrivateV1, client: T,
let enc = decode(bytes, EncryptedPayload).valueOr:
raise newException(ValueError, fmt"Failed to decode EncryptedPayload: {repr(error)}")

# TODO: Decrypt the payload
let plaintext = convo.decrypt(enc).valueOr:
error "decryption failed", error = error
return

let (frameData, missingDeps, channelId) = convo.sdsClient.unwrapReceivedMessage(
enc.plaintext.payload).valueOr:
plaintext).valueOr:
raise newException(ValueError, fmt"Failed to unwrap SDS message:{repr(error)}")

debug "sds unwrap", convo = convo.id(), missingDeps = missingDeps,
Expand Down
14 changes: 14 additions & 0 deletions src/chat_sdk/errors.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import strformat

type
ChatError* = object of CatchableError
code*: ErrorCode
context*: string

ErrorCode* = enum
errTypeError
errWrapped


proc `$`*(x: ChatError): string =
fmt"ChatError(code={$x.code}, context: {x.context})"
6 changes: 5 additions & 1 deletion src/chat_sdk/inbox.nim
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,11 @@ proc createPrivateV1FromInvite*[T: ConversationStore](client: T,
msgId: string): Future[void] {.async.} =
client.notifyDeliveryAck(conversation, msgId)

let convo = initPrivateV1(client.identity(), destPubkey, "default", deliveryAckCb)
# TODO: remove placeholder key
var key : array[32, byte]
key[2]=2

let convo = initPrivateV1Recipient(client.identity(), destPubkey, key, deliveryAckCb)
notice "Creating PrivateV1 conversation", client = client.getId(),
topic = convo.getConvoId()
client.addConversation(convo)
Expand Down
3 changes: 0 additions & 3 deletions src/chat_sdk/types.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
type ChatError* = string


type MessageId* = string
8 changes: 8 additions & 0 deletions src/naxolotl.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import naxolotl/[
naxolotl,
curve25519,
types,
errors
]

export naxolotl, curve25519, NaxolotlError
64 changes: 64 additions & 0 deletions src/naxolotl/chacha.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import nim_chacha20_poly1305/[common, chacha20_poly1305, streaming, helpers, poly1305]
import std/[sysrand]
import results
import strformat
import chronicles

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)

Loading