mirror of
https://github.com/logos-messaging/nim-chat-poc.git
synced 2026-01-03 22:53:08 +00:00
234 lines
7.8 KiB
Nim
234 lines
7.8 KiB
Nim
|
|
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
|
|
|
|
|
|
type
|
|
ReceivedPrivateV1Message* = ref object of ReceivedMessage
|
|
|
|
proc initReceivedMessage(sender: PublicKey, timestamp: int64, content: ContentFrame) : ReceivedPrivateV1Message =
|
|
ReceivedPrivateV1Message(sender:sender, timestamp:timestamp, content:content)
|
|
|
|
|
|
type
|
|
PrivateV1* = ref object of Conversation
|
|
ds: WakuClient
|
|
sdsClient: ReliabilityManager
|
|
owner: Identity
|
|
topic: string
|
|
participant: PublicKey
|
|
discriminator: string
|
|
doubleratchet: naxolotl.Doubleratchet
|
|
|
|
proc getTopic*(self: PrivateV1): string =
|
|
## Returns the topic for the PrivateV1 conversation.
|
|
return self.topic
|
|
|
|
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 derive_topic(participants: seq[PublicKey], discriminator: string): string =
|
|
## Derives a topic from the participants' public keys.
|
|
return "/convo/private/" & getConvoIdRaw(participants, 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
|
|
|
|
if convo.doubleratchet.dhSelf.public == header.dhPublic:
|
|
info "outgoing message, no need to decrypt"
|
|
return err(ChatError(code: errDecryptOutgoing, context: "Attempted to decrypt outgoing message"))
|
|
|
|
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, cb = repr(deliveryAckCb)
|
|
|
|
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 participants = @[owner.getPubkey(), participant];
|
|
|
|
var rm = newReliabilityManager().valueOr:
|
|
raise newException(ValueError, fmt"sds initialization: {repr(error)}")
|
|
|
|
result = PrivateV1(
|
|
ds: ds,
|
|
sdsClient: rm,
|
|
owner: owner,
|
|
topic: derive_topic(participants, discriminator),
|
|
participant: participant,
|
|
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, 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", true, deliveryAckCb)
|
|
|
|
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)
|
|
|
|
|
|
proc sendFrame(self: PrivateV1, ds: WakuClient,
|
|
msg: PrivateV1Frame): Future[MessageId]{.async.} =
|
|
|
|
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)}")
|
|
|
|
let encryptedPayload = self.encrypt(sdsPayload)
|
|
|
|
discard ds.sendPayload(self.getTopic(), 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,
|
|
bytes: seq[byte]) =
|
|
## Dispatcher for Incoming `PrivateV1Frames`.
|
|
## Calls further processing depending on the kind of frame.
|
|
|
|
let enc = decode(bytes, EncryptedPayload).valueOr:
|
|
raise newException(ValueError, fmt"Failed to decode EncryptedPayload: {repr(error)}")
|
|
|
|
let plaintext = convo.decrypt(enc).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 typeContentFrame:
|
|
# 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
|
|
|
|
|
|
method sendMessage*(convo: PrivateV1, content_frame: ContentFrame) : 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"
|
|
|