mirror of
https://github.com/logos-messaging/nim-chat-poc.git
synced 2026-01-05 15:43:13 +00:00
Initial commit
This commit is contained in:
parent
9a01b8c6fa
commit
db47ffd5a4
@ -26,4 +26,4 @@ requires "nimchacha20poly1305" # TODO: remove
|
||||
requires "confutils >= 0.1.0"
|
||||
requires "eth >= 0.8.0"
|
||||
requires "regex >= 0.26.3"
|
||||
requires "web3 >= 0.7.0"
|
||||
requires "web3 >= 0.7.0"
|
||||
|
||||
9
protos/common_frames.proto
Normal file
9
protos/common_frames.proto
Normal file
@ -0,0 +1,9 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package umbra.common_frames;
|
||||
|
||||
message ContentFrame {
|
||||
uint32 domain = 1;
|
||||
uint32 tag = 2;
|
||||
bytes bytes = 3;
|
||||
}
|
||||
28
protos/conversations/group_v1.proto
Normal file
28
protos/conversations/group_v1.proto
Normal file
@ -0,0 +1,28 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package umbra.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;
|
||||
// ...
|
||||
}
|
||||
}
|
||||
22
protos/conversations/private_v1.proto
Normal file
22
protos/conversations/private_v1.proto
Normal file
@ -0,0 +1,22 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package umbra.convos.private_v1;
|
||||
|
||||
import "common_frames.proto";
|
||||
|
||||
|
||||
|
||||
|
||||
message Placeholder {
|
||||
uint32 counter = 1;
|
||||
}
|
||||
|
||||
message PrivateV1Frame {
|
||||
string conversation_id = 1;
|
||||
|
||||
oneof frame_type {
|
||||
common_frames.ContentFrame content = 10;
|
||||
Placeholder placeholder = 11;
|
||||
// ....
|
||||
}
|
||||
}
|
||||
24
protos/encryption.proto
Normal file
24
protos/encryption.proto
Normal file
@ -0,0 +1,24 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package umbra.encryption;
|
||||
|
||||
|
||||
// TODO: This also encompasses plaintexts, is there a better name?
|
||||
// Alternatives: ???
|
||||
message EncryptedPayload {
|
||||
|
||||
oneof encryption {
|
||||
encryption.Plaintext plaintext = 1;
|
||||
encryption.Ecies ecies = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message Plaintext {
|
||||
bytes payload=1;
|
||||
}
|
||||
|
||||
message Ecies {
|
||||
bytes encrypted_bytes=1;
|
||||
bytes ephemeral_pubkey = 2;
|
||||
bytes tag = 3;
|
||||
}
|
||||
16
protos/envelope.proto
Normal file
16
protos/envelope.proto
Normal file
@ -0,0 +1,16 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package umbra.envelope;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Payload Framing Messages
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
message UmbraEnvelopeV1 {
|
||||
|
||||
string conversation_hint = 1;
|
||||
uint64 salt = 2;
|
||||
|
||||
bytes payload = 5;
|
||||
}
|
||||
12
protos/inbox.proto
Normal file
12
protos/inbox.proto
Normal file
@ -0,0 +1,12 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package umbra.inbox;
|
||||
|
||||
import "invite.proto";
|
||||
|
||||
message InboxV1Frame {
|
||||
string recipient = 1;
|
||||
oneof frame_type {
|
||||
invite.InvitePrivateV1 invite_private_v1 = 10;
|
||||
}
|
||||
}
|
||||
7
protos/invite.proto
Normal file
7
protos/invite.proto
Normal file
@ -0,0 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package umbra.invite;
|
||||
|
||||
message InvitePrivateV1 {
|
||||
repeated string participants = 1;
|
||||
}
|
||||
23
protos/reliability.proto
Normal file
23
protos/reliability.proto
Normal file
@ -0,0 +1,23 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package umbra.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;
|
||||
}
|
||||
175
src/client.nim
Normal file
175
src/client.nim
Normal file
@ -0,0 +1,175 @@
|
||||
import tables
|
||||
import identity
|
||||
import crypto
|
||||
import proto_types
|
||||
import std/times
|
||||
import utils
|
||||
import dev
|
||||
import inbox
|
||||
|
||||
import conversations/private_v1
|
||||
|
||||
import secp256k1
|
||||
import chronicles
|
||||
|
||||
type KeyEntry* = object
|
||||
keytype: string
|
||||
keypair: SkKeyPair
|
||||
timestamp: int64
|
||||
|
||||
|
||||
type SupportedConvoTypes* = Inbox | PrivateV1
|
||||
|
||||
type
|
||||
ConvoType* = enum
|
||||
InboxV1Type, PrivateV1Type
|
||||
|
||||
ConvoWrapper* = object
|
||||
case convo_type*: ConvoType
|
||||
of InboxV1Type:
|
||||
inboxV1*: Inbox
|
||||
of PrivateV1Type:
|
||||
privateV1*: PrivateV1
|
||||
|
||||
|
||||
type
|
||||
Client* = object
|
||||
ident: Identity
|
||||
key_store: Table[string, KeyEntry] # Keyed by HexEncoded Public Key
|
||||
conversations: Table[string, ConvoWrapper] # Keyed by conversation ID
|
||||
|
||||
|
||||
proc process_invite*(self: Client, invite: InvitePrivateV1)
|
||||
|
||||
|
||||
#################################################
|
||||
# Constructors
|
||||
#################################################
|
||||
|
||||
proc initClient*(name: string): Client =
|
||||
|
||||
|
||||
var c = Client(ident: createIdentity(name),
|
||||
key_store: initTable[string, KeyEntry](),
|
||||
conversations: initTable[string, ConvoWrapper]())
|
||||
|
||||
let default_inbox = initInbox(c.ident.getAddr(), proc(
|
||||
x: InvitePrivateV1) = c.process_invite(x))
|
||||
|
||||
c.conversations[conversation_id_for(c.ident.getPubkey(
|
||||
))] = ConvoWrapper(convo_type: InboxV1Type, inboxV1: default_inbox)
|
||||
|
||||
result = c
|
||||
|
||||
|
||||
#################################################
|
||||
# Parameter Access
|
||||
#################################################
|
||||
|
||||
proc getClientAddr*(self: Client): string =
|
||||
result = self.ident.getAddr()
|
||||
|
||||
proc default_inbox_conversation_id*(self: Client): string =
|
||||
## Returns the default inbox address for the client.
|
||||
result = conversation_id_for(self.ident.getPubkey())
|
||||
|
||||
#################################################
|
||||
# Methods
|
||||
#################################################
|
||||
|
||||
# proc addConversation*(c: var Client, convo: Conversation) =
|
||||
# if not c.conversations.hasKey(convo.getConvoId()):
|
||||
# c.conversations[convo.getConvoId()] = convo
|
||||
# else:
|
||||
# echo "Conversation with ID ", convo.getConvoId(), " already exists."
|
||||
|
||||
|
||||
proc createIntroBundle*(self: var Client): IntroBundle =
|
||||
## Generates an IntroBundle for the client, which includes
|
||||
## the required information to send a message.
|
||||
|
||||
# Create Ephemeral keypair, save it in the key store
|
||||
let ephemeral_keypair = generate_keypair()
|
||||
self.key_store[ephemeral_keypair.pubkey.toHexCompressed()] = KeyEntry(
|
||||
keytype: "ephemeral",
|
||||
keypair: ephemeral_keypair,
|
||||
timestamp: getTime().toUnix(),
|
||||
)
|
||||
|
||||
result = IntroBundle(
|
||||
ident: @(self.ident.getPubkey().toRawCompressed()),
|
||||
ephemeral: @(ephemeral_keypair.pubkey.toRawCompressed()),
|
||||
)
|
||||
|
||||
proc createPrivateConvo*(self: Client, intro_bundle: IntroBundle): TransportMessage =
|
||||
## Creates a private conversation with the given Invitebundle.
|
||||
|
||||
|
||||
|
||||
let res_pubkey = SkPublicKey.fromRaw(intro_bundle.ident)
|
||||
if res_pubkey.isErr:
|
||||
raise newException(ValueError, "Invalid public key in intro bundle.")
|
||||
let dest_pubkey = res_pubkey.get()
|
||||
let convo_id = "/convo/inbox/" & dest_pubkey.getAddr()
|
||||
|
||||
let dst_convo_topic = topic_inbox(dest_pubkey.get_addr())
|
||||
|
||||
let invite = InvitePrivateV1(
|
||||
participants: @[self.ident.getAddr(), dest_pubkey.get_addr()],
|
||||
)
|
||||
let env = wrap_env(encrypt(InboxV1Frame(invite_private_v1: invite,
|
||||
recipient: "")), convo_id)
|
||||
|
||||
# Create a new conversation
|
||||
# let convo_id = self.ident.getAddr() & "-" & raya_bundle.ident.toHexCompressed()
|
||||
# let new_convo = Conversation(id: convo_id, participants: @[self.ident,
|
||||
# Identity(name: "Raya", keypair: SkKeyPair.fromRawCompressed(
|
||||
# raya_bundle.ident))])
|
||||
|
||||
# Add the conversation to the client's store
|
||||
# self.addConversation(new_convo)
|
||||
|
||||
# Return the invite (or any other relevant data)
|
||||
# return new_convo
|
||||
|
||||
return sendTo(dst_convo_topic, encode(env))
|
||||
|
||||
proc get_conversation(self: Client,
|
||||
conversation_hint: string): Result[Option[ConvoWrapper], string] =
|
||||
|
||||
# TODO: Implementing Hinting
|
||||
if not self.conversations.hasKey(conversation_hint):
|
||||
ok(none(ConvoWrapper))
|
||||
else:
|
||||
ok(some(self.conversations[conversation_hint]))
|
||||
|
||||
|
||||
proc recv*(self: var Client, transport_message: TransportMessage): seq[
|
||||
TransportMessage] =
|
||||
## Reveives a incomming payload, decodes it, and processes it.
|
||||
let res_env = decode(transport_message.payload, UmbraEnvelopeV1)
|
||||
if res_env.isErr:
|
||||
raise newException(ValueError, "Failed to decode UmbraEnvelopeV1: " & res_env.error)
|
||||
let env = res_env.get()
|
||||
|
||||
let res_convo = self.get_conversation(env.conversation_hint)
|
||||
if res_convo.isErr:
|
||||
raise newException(ValueError, "Failed to get conversation: " &
|
||||
res_convo.error)
|
||||
|
||||
let convo = res_convo.get()
|
||||
if not convo.isSome:
|
||||
debug "No conversation found", hint = env.conversation_hint
|
||||
return
|
||||
|
||||
let inbox = convo.get().inboxV1
|
||||
|
||||
let res = inbox.handle_incomming_frame(transport_message.topic, env.payload)
|
||||
if res.isErr:
|
||||
warn "Failed to handle incoming frame: ", error = res.error
|
||||
return @[]
|
||||
|
||||
|
||||
|
||||
proc process_invite*(self: Client, invite: InvitePrivateV1) =
|
||||
debug "Callback Invoked", invite = invite
|
||||
4
src/conversations/private_v1.nim
Normal file
4
src/conversations/private_v1.nim
Normal file
@ -0,0 +1,4 @@
|
||||
type
|
||||
PrivateV1* = object
|
||||
# Placeholder for PrivateV1 conversation type
|
||||
name*: string
|
||||
33
src/crypto.nim
Normal file
33
src/crypto.nim
Normal file
@ -0,0 +1,33 @@
|
||||
import proto_types
|
||||
|
||||
import secp256k1
|
||||
import std/[sysrand]
|
||||
export secp256k1
|
||||
|
||||
|
||||
type KeyPair* = SkKeyPair
|
||||
|
||||
|
||||
|
||||
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_keypair*(): KeyPair =
|
||||
var rng: Rng = urandom
|
||||
let res = SkKeyPair.random(rng)
|
||||
if res.isErr:
|
||||
raise newException(ValueError, "Failed to generate keypair: ")
|
||||
|
||||
result = res.get()
|
||||
|
||||
6
src/dev.nim
Normal file
6
src/dev.nim
Normal file
@ -0,0 +1,6 @@
|
||||
## Utilties for development and debugging
|
||||
|
||||
proc dir*[T](obj: T) =
|
||||
echo "Object of type: ", T
|
||||
for name, value in fieldPairs(obj):
|
||||
echo " ", name, ": ", value
|
||||
30
src/identity.nim
Normal file
30
src/identity.nim
Normal file
@ -0,0 +1,30 @@
|
||||
|
||||
import crypto
|
||||
import utils
|
||||
|
||||
|
||||
type
|
||||
Identity* = object
|
||||
name: string
|
||||
keypair: KeyPair
|
||||
|
||||
|
||||
#################################################
|
||||
# Constructors
|
||||
#################################################
|
||||
|
||||
proc createIdentity*(name: string): Identity =
|
||||
let keypair = generate_keypair()
|
||||
result = Identity(name: name, keypair: keypair)
|
||||
|
||||
|
||||
#################################################
|
||||
# Parameter Access
|
||||
#################################################
|
||||
|
||||
proc getAddr*(self: Identity): string =
|
||||
result = get_addr(self.keypair.pubkey)
|
||||
|
||||
|
||||
proc getPubkey*(self: Identity): SkPublicKey =
|
||||
result = self.keypair.pubkey
|
||||
65
src/inbox.nim
Normal file
65
src/inbox.nim
Normal file
@ -0,0 +1,65 @@
|
||||
import crypto
|
||||
import proto_types
|
||||
import utils
|
||||
import dev
|
||||
|
||||
import chronicles
|
||||
|
||||
|
||||
type InviteCallback* = proc(invite: InvitePrivateV1): void
|
||||
|
||||
|
||||
type
|
||||
Inbox* = object
|
||||
inbox_addr: string
|
||||
invite_callback: InviteCallback
|
||||
|
||||
proc initInbox*(inbox_addr: string, invite_callback: InviteCallback): Inbox =
|
||||
## Initializes an Inbox object with the given address and invite callback.
|
||||
return Inbox(inbox_addr: inbox_addr, invite_callback: invite_callback)
|
||||
|
||||
|
||||
proc encrypt*(frame: InboxV1Frame): EncryptedPayload =
|
||||
return encrypt_plain(frame)
|
||||
|
||||
proc wrap_env*(payload: EncryptedPayload, convo_id: string): UmbraEnvelopeV1 =
|
||||
let bytes = encode(payload)
|
||||
let salt = generateSalt()
|
||||
|
||||
return UmbraEnvelopeV1(
|
||||
payload: bytes,
|
||||
salt: salt,
|
||||
conversation_hint: convo_id,
|
||||
)
|
||||
|
||||
proc conversation_id_for*(pubkey: SkPublicKey): string =
|
||||
## Generates a conversation ID based on the public key.
|
||||
return "/convo/inbox/" & pubkey.get_addr()
|
||||
|
||||
# TODO derive this from instance of Inbox
|
||||
proc topic_inbox*(client_addr: string): string =
|
||||
return "/inbox/v1/" & client_addr
|
||||
|
||||
|
||||
proc handle_incomming_frame*(self: Inbox, topic: string, bytes: seq[
|
||||
byte]): Result[int, string] =
|
||||
# TODO: Can this fail?
|
||||
let res = decode(bytes, EncryptedPayload)
|
||||
if res.isErr:
|
||||
return err("Failed to decode payload: " & res.error)
|
||||
|
||||
let encbytes = res.get()
|
||||
|
||||
# NOTE! in nim_protobuf_serializaiton OneOf fields are not exclusive, and all fields are default initialized.
|
||||
if encbytes.plaintext == Plaintext():
|
||||
return err("Incorrect Encryption Type")
|
||||
|
||||
let res_frame = decrypt_plain(encbytes.plaintext, InboxV1Frame)
|
||||
if res_frame.isErr:
|
||||
return err("Failed to decrypt frame: " & res_frame.error)
|
||||
let frame = res_frame.get()
|
||||
|
||||
self.invite_callback(frame.invite_private_v1)
|
||||
ok(0)
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
|
||||
import
|
||||
chronicles,
|
||||
chronos,
|
||||
@ -28,3 +29,39 @@ proc main(): Future[void] {.async.} =
|
||||
|
||||
when isMainModule:
|
||||
waitFor main()
|
||||
|
||||
# import client
|
||||
# import chronicles
|
||||
# import proto_types
|
||||
|
||||
# proc log(transport_message: TransportMessage) =
|
||||
# ## Log the transport message
|
||||
# info "Transport Message:", topic = transport_message.topic,
|
||||
# payload = transport_message.payload
|
||||
|
||||
# proc demo() =
|
||||
|
||||
# # Initalize Clients
|
||||
# var saro = initClient("Saro")
|
||||
# var raya = initClient("Raya")
|
||||
|
||||
# # # Exchange Contact Info
|
||||
# let raya_bundle = raya.createIntroBundle()
|
||||
|
||||
# # Create Conversation
|
||||
# let invite = saro.createPrivateConvo(raya_bundle)
|
||||
# invite.log()
|
||||
# let msgs = raya.recv(invite)
|
||||
|
||||
# # raya.convos()[0].sendText("Hello Saro, this is Raya!")
|
||||
|
||||
|
||||
# when isMainModule:
|
||||
# echo("Starting ChatPOC...")
|
||||
|
||||
# try:
|
||||
# demo()
|
||||
# except Exception as e:
|
||||
# error "Crashed ", error = e.msg
|
||||
|
||||
# echo("Finished...")
|
||||
|
||||
57
src/proto_types.nim
Normal file
57
src/proto_types.nim
Normal file
@ -0,0 +1,57 @@
|
||||
# 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
|
||||
|
||||
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/encryption.proto"
|
||||
import_proto3 "../protos/envelope.proto"
|
||||
|
||||
type EncryptableTypes = InboxV1Frame | EncryptedPayload
|
||||
|
||||
export EncryptedPayload
|
||||
export InboxV1Frame
|
||||
|
||||
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]
|
||||
|
||||
|
||||
export IntroBundle
|
||||
|
||||
|
||||
type
|
||||
TransportMessage {.proto3.} = object
|
||||
topic* {.fieldNumber: 1.}: string
|
||||
payload* {.fieldNumber: 2.}: seq[byte]
|
||||
|
||||
|
||||
proc sendTo*(topic: string, payload: seq[byte]): TransportMessage =
|
||||
result = TransportMessage(topic: topic, payload: payload)
|
||||
|
||||
export TransportMessage
|
||||
@ -1,6 +1,18 @@
|
||||
import std/times
|
||||
import waku/waku_core
|
||||
|
||||
import std/[random, times]
|
||||
import crypto
|
||||
import blake2
|
||||
|
||||
proc getTimestamp*(): Timestamp =
|
||||
result = waku_core.getNanosecondTime(getTime().toUnix())
|
||||
|
||||
proc generateSalt*(): uint64 =
|
||||
randomize()
|
||||
result = 0
|
||||
for i in 0 ..< 8:
|
||||
result = result or (uint64(rand(255)) shl (i * 8))
|
||||
|
||||
proc get_addr*(pubkey: SkPublicKey): string =
|
||||
# TODO: Needs Spec
|
||||
result = getBlake2b(pubkey.toHexCompressed(), 4, "")
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user