mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-02-09 16:33:10 +00:00
PrivateV1 Convo Initialization via Inbox (#13)
* Load orginal protofiles * Change package name * Add prost generation * Remove placeholders * Add generated files + imports * replace with chat-proto * Add XK0 * auto formatting * Initial implementation of PrivateV1 initialization * Add ConvoFactory trait * Hook up indentity placeholder * Remove RemoteInbox until it’s needed * Simplify Identity ownership * Clean up x3handshake * Move inbox encryption * Simplify inbox encryption * Cleanup warnings * Add todos * Update chat-proto crate * Publickey Handling * Reorg Inbox handshake * Update Inbox convoId * Remove file structure headers * Update ConvoID * Add Domain Separator trait * Remove Convo trait functions * Rename Context * Add SecretKey * Add workspace dependency * update KE name * Update comments for clarity * Remove Xk0 references * Bump chat_proto version and relock
This commit is contained in:
parent
58392841cd
commit
fe23c39321
217
Cargo.lock
generated
217
Cargo.lock
generated
@ -9,9 +9,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"generic-array",
|
||||
"generic-array 0.14.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
version = "0.10.6"
|
||||
@ -27,9 +39,15 @@ version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"generic-array 0.14.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
@ -60,6 +78,14 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chat-proto"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/logos-messaging/chat_proto#21cd52b0649c959f43eec19e1edad12451ccc382"
|
||||
dependencies = [
|
||||
"prost",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
@ -71,6 +97,12 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
@ -80,13 +112,27 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ed25519-dalek",
|
||||
"generic-array 1.3.5",
|
||||
"hkdf",
|
||||
"rand_core",
|
||||
"sha2",
|
||||
"x25519-dalek",
|
||||
"xeddsa",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"generic-array 0.14.7",
|
||||
"rand_core",
|
||||
"typenum",
|
||||
]
|
||||
@ -100,6 +146,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"curve25519-dalek-derive",
|
||||
"digest",
|
||||
"fiat-crypto",
|
||||
"rustc_version",
|
||||
"subtle",
|
||||
@ -117,6 +164,27 @@ dependencies = [
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@ -143,6 +211,36 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
||||
dependencies = [
|
||||
"pkcs8",
|
||||
"signature",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519-dalek"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
|
||||
dependencies = [
|
||||
"curve25519-dalek",
|
||||
"ed25519",
|
||||
"serde",
|
||||
"sha2",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@ -200,6 +298,16 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "1.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eaf57c49a95fd1fe24b90b3033bee6dc7e8f1288d51494cb44e627c295e38542"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.16"
|
||||
@ -217,6 +325,12 @@ version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
version = "0.12.4"
|
||||
@ -251,7 +365,7 @@ version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"generic-array 0.14.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -263,6 +377,15 @@ dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.178"
|
||||
@ -273,7 +396,14 @@ checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
|
||||
name = "logos-chat"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"blake2",
|
||||
"chat-proto",
|
||||
"crypto",
|
||||
"hex",
|
||||
"prost",
|
||||
"rand_core",
|
||||
"thiserror",
|
||||
"x25519-dalek",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -310,6 +440,16 @@ version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "pkcs8"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
||||
dependencies = [
|
||||
"der",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "poly1305"
|
||||
version = "0.8.0"
|
||||
@ -358,6 +498,29 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"prost-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-derive"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.42"
|
||||
@ -486,12 +649,42 @@ dependencies = [
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2-const-stable"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9"
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stabby"
|
||||
version = "36.2.2"
|
||||
@ -695,6 +888,22 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xeddsa"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2460c9a9c9d1331ff6801e87badb517faa6b6758e5fb585eb27daf7622c6d5ad"
|
||||
dependencies = [
|
||||
"curve25519-dalek",
|
||||
"derive_more",
|
||||
"ed25519",
|
||||
"ed25519-dalek",
|
||||
"rand",
|
||||
"sha2",
|
||||
"x25519-dalek",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.31"
|
||||
|
||||
@ -4,5 +4,9 @@ resolver = "3"
|
||||
|
||||
members = [
|
||||
"conversations",
|
||||
"crypto",
|
||||
"double-ratchets",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
blake2 = "0.10"
|
||||
|
||||
@ -7,4 +7,11 @@ edition = "2024"
|
||||
crate-type = ["staticlib"]
|
||||
|
||||
[dependencies]
|
||||
blake2.workspace = true
|
||||
chat-proto = { git = "https://github.com/logos-messaging/chat_proto" }
|
||||
crypto = { path = "../crypto" }
|
||||
hex = "0.4.3"
|
||||
prost = "0.14.1"
|
||||
rand_core = { version = "0.6" }
|
||||
thiserror = "2.0.17"
|
||||
x25519-dalek = { version = "2.0.1", features = ["static_secrets", "reusable_secrets", "getrandom"] }
|
||||
|
||||
@ -8,9 +8,9 @@ pub enum ErrorCode {
|
||||
BadConvoId = -2,
|
||||
}
|
||||
|
||||
use crate::context::Ctx;
|
||||
use crate::context::Context;
|
||||
|
||||
pub type ContextHandle = *mut Ctx;
|
||||
pub type ContextHandle = *mut Context;
|
||||
|
||||
/// Creates a new libchat Ctx
|
||||
///
|
||||
@ -18,7 +18,7 @@ pub type ContextHandle = *mut Ctx;
|
||||
/// Opaque handle to the store. Must be freed with conversation_store_destroy()
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn create_context() -> ContextHandle {
|
||||
let store = Box::new(Ctx::new());
|
||||
let store = Box::new(Context::new());
|
||||
Box::into_raw(store) // Leak the box, return raw pointer
|
||||
}
|
||||
|
||||
|
||||
@ -1,29 +1,46 @@
|
||||
use crate::conversation::{ConversationId, ConversationIdOwned, ConversationStore, PrivateV1Convo};
|
||||
use std::rc::Rc;
|
||||
|
||||
pub struct PayloadData {
|
||||
pub delivery_address: String,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
use crypto::PrekeyBundle;
|
||||
|
||||
pub struct ContentData {
|
||||
pub conversation_id: String,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
use crate::{
|
||||
conversation::{ConversationId, ConversationIdOwned, ConversationStore},
|
||||
identity::Identity,
|
||||
inbox::Inbox,
|
||||
types::{ContentData, PayloadData},
|
||||
};
|
||||
|
||||
pub struct Ctx {
|
||||
// This is the main entry point to the conversations api.
|
||||
// Ctx manages lifetimes of objects to process and generate payloads.
|
||||
pub struct Context {
|
||||
_identity: Rc<Identity>,
|
||||
store: ConversationStore,
|
||||
inbox: Inbox,
|
||||
}
|
||||
|
||||
impl Ctx {
|
||||
impl Context {
|
||||
pub fn new() -> Self {
|
||||
let identity = Rc::new(Identity::new());
|
||||
let inbox = Inbox::new(Rc::clone(&identity)); //
|
||||
Self {
|
||||
_identity: identity,
|
||||
store: ConversationStore::new(),
|
||||
inbox,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_private_convo(&mut self, _content: &[u8]) -> ConversationIdOwned {
|
||||
let new_convo = PrivateV1Convo::new();
|
||||
self.store.insert(new_convo)
|
||||
pub fn create_private_convo(
|
||||
&mut self,
|
||||
remote_bundle: &PrekeyBundle,
|
||||
content: String,
|
||||
) -> ConversationIdOwned {
|
||||
let (convo, _payloads) = self
|
||||
.inbox
|
||||
.invite_to_private_convo(remote_bundle, content)
|
||||
.unwrap_or_else(|_| todo!("Log/Surface Error"));
|
||||
|
||||
self.store.insert_convo(convo)
|
||||
|
||||
// TODO: Change return type to handle outbout packets.
|
||||
}
|
||||
|
||||
pub fn send_content(&mut self, _convo_id: ConversationId, _content: &[u8]) -> Vec<PayloadData> {
|
||||
@ -42,3 +59,21 @@ impl Ctx {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::conversation::GroupTestConvo;
|
||||
|
||||
#[test]
|
||||
fn convo_store_get() {
|
||||
let mut store: ConversationStore = ConversationStore::new();
|
||||
|
||||
let new_convo = GroupTestConvo::new();
|
||||
let convo_id = store.insert_convo(new_convo);
|
||||
|
||||
let convo = store.get_mut(&convo_id).ok_or_else(|| 0);
|
||||
convo.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,46 +3,55 @@ use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use crate::errors::ChatError;
|
||||
|
||||
/////////////////////////////////////////////////
|
||||
// Type Definitions
|
||||
/////////////////////////////////////////////////
|
||||
use crate::types::ContentData;
|
||||
|
||||
pub type ConversationId<'a> = &'a str;
|
||||
pub type ConversationIdOwned = Arc<str>;
|
||||
|
||||
/////////////////////////////////////////////////
|
||||
// Trait Definitions
|
||||
/////////////////////////////////////////////////
|
||||
|
||||
pub trait Convo: Debug {
|
||||
pub trait Id: Debug {
|
||||
fn id(&self) -> ConversationId;
|
||||
fn send_frame(&mut self, message: &[u8]) -> Result<(), ChatError>;
|
||||
fn handle_frame(&mut self, message: &[u8]) -> Result<(), ChatError>;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////
|
||||
// Structs
|
||||
/////////////////////////////////////////////////
|
||||
pub trait ConvoFactory: Id + Debug {
|
||||
fn handle_frame(
|
||||
&mut self,
|
||||
encoded_payload: &[u8],
|
||||
) -> Result<(Box<dyn Convo>, Vec<ContentData>), ChatError>;
|
||||
}
|
||||
|
||||
pub trait Convo: Id + Debug {
|
||||
fn send_message(&mut self, content: &[u8]) -> Result<Vec<EncryptedPayload>, ChatError>;
|
||||
}
|
||||
|
||||
pub struct ConversationStore {
|
||||
conversations: HashMap<Arc<str>, Box<dyn Convo>>,
|
||||
factories: HashMap<Arc<str>, Box<dyn ConvoFactory>>,
|
||||
}
|
||||
|
||||
impl ConversationStore {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
conversations: HashMap::new(),
|
||||
factories: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, conversation: impl Convo + 'static) -> ConversationIdOwned {
|
||||
pub fn insert_convo(&mut self, conversation: impl Convo + Id + 'static) -> ConversationIdOwned {
|
||||
let key: ConversationIdOwned = Arc::from(conversation.id());
|
||||
self.conversations
|
||||
.insert(key.clone(), Box::new(conversation));
|
||||
key
|
||||
}
|
||||
|
||||
pub fn register_factory(
|
||||
&mut self,
|
||||
handler: impl ConvoFactory + Id + 'static,
|
||||
) -> ConversationIdOwned {
|
||||
let key: ConversationIdOwned = Arc::from(handler.id());
|
||||
self.factories.insert(key.clone(), Box::new(handler));
|
||||
key
|
||||
}
|
||||
|
||||
pub fn get(&self, id: ConversationId) -> Option<&(dyn Convo + '_)> {
|
||||
self.conversations.get(id).map(|c| c.as_ref())
|
||||
}
|
||||
@ -51,17 +60,22 @@ impl ConversationStore {
|
||||
Some(self.conversations.get_mut(id)?.as_mut())
|
||||
}
|
||||
|
||||
pub fn get_factory(&mut self, id: ConversationId) -> Option<&mut (dyn ConvoFactory + '_)> {
|
||||
Some(self.factories.get_mut(id)?.as_mut())
|
||||
}
|
||||
|
||||
pub fn conversation_ids(&self) -> impl Iterator<Item = ConversationIdOwned> + '_ {
|
||||
self.conversations.keys().cloned()
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////
|
||||
// Modules
|
||||
/////////////////////////////////////////////////
|
||||
pub fn factory_ids(&self) -> impl Iterator<Item = ConversationIdOwned> + '_ {
|
||||
self.factories.keys().cloned()
|
||||
}
|
||||
}
|
||||
|
||||
mod group_test;
|
||||
mod privatev1;
|
||||
|
||||
use crate::proto::EncryptedPayload;
|
||||
pub use group_test::GroupTestConvo;
|
||||
pub use privatev1::PrivateV1Convo;
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
use crate::conversation::{ChatError, ConversationId, Convo};
|
||||
use chat_proto::logoschat::encryption::EncryptedPayload;
|
||||
|
||||
use crate::conversation::{ChatError, ConversationId, Convo, Id};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GroupTestConvo {}
|
||||
@ -9,19 +11,15 @@ impl GroupTestConvo {
|
||||
}
|
||||
}
|
||||
|
||||
impl Convo for GroupTestConvo {
|
||||
impl Id for GroupTestConvo {
|
||||
fn id(&self) -> ConversationId {
|
||||
// implementation
|
||||
"grouptest"
|
||||
}
|
||||
}
|
||||
|
||||
fn send_frame(&mut self, _message: &[u8]) -> Result<(), ChatError> {
|
||||
// todo!("Not Implemented")
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_frame(&mut self, _message: &[u8]) -> Result<(), ChatError> {
|
||||
// todo!("Not Implemented")
|
||||
Ok(())
|
||||
impl Convo for GroupTestConvo {
|
||||
fn send_message(&mut self, _content: &[u8]) -> Result<Vec<EncryptedPayload>, ChatError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,25 +1,56 @@
|
||||
use crate::conversation::{ChatError, ConversationId, Convo};
|
||||
use chat_proto::logoschat::{
|
||||
convos::private_v1::{PrivateV1Frame, private_v1_frame::FrameType},
|
||||
encryption::{Doubleratchet, EncryptedPayload, encrypted_payload::Encryption},
|
||||
};
|
||||
use crypto::SecretKey;
|
||||
use prost::{Message, bytes::Bytes};
|
||||
|
||||
use crate::{
|
||||
conversation::{ChatError, ConversationId, Convo, Id},
|
||||
utils::timestamp_millis,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PrivateV1Convo {}
|
||||
|
||||
impl PrivateV1Convo {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(_seed_key: SecretKey) -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
fn encrypt(&self, frame: PrivateV1Frame) -> EncryptedPayload {
|
||||
// TODO: Integrate DR
|
||||
|
||||
EncryptedPayload {
|
||||
encryption: Some(Encryption::Doubleratchet(Doubleratchet {
|
||||
dh: Bytes::from(vec![]),
|
||||
msg_num: 0,
|
||||
prev_chain_len: 1,
|
||||
ciphertext: Bytes::from(frame.encode_to_vec()),
|
||||
aux: "".into(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Id for PrivateV1Convo {
|
||||
fn id(&self) -> ConversationId {
|
||||
// TODO: implementation
|
||||
"private_v1_convo_id"
|
||||
}
|
||||
}
|
||||
|
||||
impl Convo for PrivateV1Convo {
|
||||
fn id(&self) -> ConversationId {
|
||||
// implementation
|
||||
"private_v1_convo_id"
|
||||
}
|
||||
fn send_message(&mut self, content: &[u8]) -> Result<Vec<EncryptedPayload>, ChatError> {
|
||||
let frame = PrivateV1Frame {
|
||||
conversation_id: self.id().into(),
|
||||
sender: "delete".into(),
|
||||
timestamp: timestamp_millis(),
|
||||
frame_type: Some(FrameType::Content(content.to_vec().into())),
|
||||
};
|
||||
|
||||
fn send_frame(&mut self, _message: &[u8]) -> Result<(), ChatError> {
|
||||
todo!("Not Implemented")
|
||||
}
|
||||
let ef = self.encrypt(frame);
|
||||
|
||||
fn handle_frame(&mut self, _message: &[u8]) -> Result<(), ChatError> {
|
||||
todo!("Not Implemented")
|
||||
Ok(vec![ef])
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
pub use blake2::Digest;
|
||||
use blake2::{Blake2b, digest};
|
||||
use prost::bytes::Bytes;
|
||||
pub use x25519_dalek::{PublicKey, StaticSecret};
|
||||
|
||||
pub trait CopyBytes {
|
||||
fn copy_to_bytes(&self) -> Bytes;
|
||||
}
|
||||
|
||||
impl CopyBytes for PublicKey {
|
||||
fn copy_to_bytes(&self) -> Bytes {
|
||||
Bytes::copy_from_slice(self.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub type Blake2b128 = Blake2b<digest::consts::U16>;
|
||||
@ -4,4 +4,12 @@ pub use thiserror::Error;
|
||||
pub enum ChatError {
|
||||
#[error("protocol error: {0:?}")]
|
||||
Protocol(String),
|
||||
#[error("Failed to decode payload: {0}")]
|
||||
DecodeError(#[from] prost::DecodeError),
|
||||
#[error("incorrect bundle value: {0:?}")]
|
||||
UnexpectedPayload(String),
|
||||
#[error("unexpected payload contents: {0}")]
|
||||
BadBundleValue(String),
|
||||
#[error("handshake initiated with a unknown ephemeral key")]
|
||||
UnknownEphemeralKey(),
|
||||
}
|
||||
|
||||
43
conversations/src/identity.rs
Normal file
43
conversations/src/identity.rs
Normal file
@ -0,0 +1,43 @@
|
||||
use blake2::{Blake2b512, Digest};
|
||||
use std::fmt;
|
||||
|
||||
use crate::crypto::{PublicKey, StaticSecret};
|
||||
|
||||
pub struct Identity {
|
||||
secret: StaticSecret,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Identity {
|
||||
// Manually implement debug to not reveal secret key material
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Identity")
|
||||
.field("public_key", &self.public_key())
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Identity {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
secret: StaticSecret::random(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn address(&self) -> String {
|
||||
hex::encode(Blake2b512::digest(self.public_key()))
|
||||
}
|
||||
|
||||
pub fn public_key(&self) -> PublicKey {
|
||||
PublicKey::from(&self.secret)
|
||||
}
|
||||
|
||||
pub fn secret(&self) -> &StaticSecret {
|
||||
&self.secret
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Identity {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@ -1,31 +1,4 @@
|
||||
use crate::conversation::{ChatError, ConversationId, Convo};
|
||||
mod handshake;
|
||||
mod inbox;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Inbox {
|
||||
address: String,
|
||||
}
|
||||
|
||||
impl Inbox {
|
||||
pub fn new(address: impl Into<String>) -> Self {
|
||||
Self {
|
||||
address: address.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Convo for Inbox {
|
||||
fn id(&self) -> ConversationId {
|
||||
self.address.as_ref()
|
||||
}
|
||||
|
||||
fn send_frame(&mut self, _message: &[u8]) -> Result<(), ChatError> {
|
||||
todo!("Not Implemented")
|
||||
}
|
||||
|
||||
fn handle_frame(&mut self, message: &[u8]) -> Result<(), ChatError> {
|
||||
if message.len() == 0 {
|
||||
return Err(ChatError::Protocol("Example error".into()));
|
||||
}
|
||||
todo!("Not Implemented")
|
||||
}
|
||||
}
|
||||
pub use inbox::Inbox;
|
||||
|
||||
120
conversations/src/inbox/handshake.rs
Normal file
120
conversations/src/inbox/handshake.rs
Normal file
@ -0,0 +1,120 @@
|
||||
use blake2::{
|
||||
Blake2bMac,
|
||||
digest::{FixedOutput, consts::U32},
|
||||
};
|
||||
use crypto::{DomainSeparator, PrekeyBundle, SecretKey, X3Handshake};
|
||||
use rand_core::{CryptoRng, RngCore};
|
||||
|
||||
use crate::crypto::{PublicKey, StaticSecret};
|
||||
|
||||
type Blake2bMac256 = Blake2bMac<U32>;
|
||||
|
||||
pub struct InboxDomain;
|
||||
impl DomainSeparator for InboxDomain {
|
||||
const BYTES: &'static [u8] = b"logos_chat_inbox";
|
||||
}
|
||||
|
||||
type InboxKeyExchange = X3Handshake<InboxDomain>;
|
||||
|
||||
pub struct InboxHandshake {}
|
||||
|
||||
impl InboxHandshake {
|
||||
/// Performs
|
||||
pub fn perform_as_initiator<R: RngCore + CryptoRng>(
|
||||
identity_keypair: &StaticSecret,
|
||||
recipient_bundle: &PrekeyBundle,
|
||||
rng: &mut R,
|
||||
) -> (SecretKey, PublicKey) {
|
||||
// Perform X3DH handshake to get shared secret
|
||||
let (shared_secret, ephemeral_public) =
|
||||
InboxKeyExchange::initator(identity_keypair, recipient_bundle, rng);
|
||||
|
||||
let seed_key = Self::derive_keys_from_shared_secret(shared_secret);
|
||||
(seed_key, ephemeral_public)
|
||||
}
|
||||
|
||||
/// Perform the Inbox Handshake after receiving a keyBundle
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `identity_keypair` - Your long-term identity key pair
|
||||
/// * `signed_prekey` - Your signed prekey (private)
|
||||
/// * `onetime_prekey` - Your one-time prekey (private, if used)
|
||||
/// * `initiator_identity` - Initiator's identity public key
|
||||
/// * `initiator_ephemeral` - Initiator's ephemeral public key
|
||||
pub fn perform_as_responder(
|
||||
identity_keypair: &StaticSecret,
|
||||
signed_prekey: &StaticSecret,
|
||||
onetime_prekey: Option<&StaticSecret>,
|
||||
initiator_identity: &PublicKey,
|
||||
initiator_ephemeral: &PublicKey,
|
||||
) -> SecretKey {
|
||||
// Perform X3DH to get shared secret
|
||||
let shared_secret = InboxKeyExchange::responder(
|
||||
identity_keypair,
|
||||
signed_prekey,
|
||||
onetime_prekey,
|
||||
initiator_identity,
|
||||
initiator_ephemeral,
|
||||
);
|
||||
|
||||
Self::derive_keys_from_shared_secret(shared_secret)
|
||||
}
|
||||
|
||||
/// Derive keys from X3DH shared secret
|
||||
fn derive_keys_from_shared_secret(shared_secret: SecretKey) -> SecretKey {
|
||||
let seed_key: [u8; 32] = Blake2bMac256::new_with_salt_and_personal(
|
||||
shared_secret.as_bytes(),
|
||||
&[], // No salt - input already has high entropy
|
||||
b"InboxV1-Seed",
|
||||
)
|
||||
.unwrap()
|
||||
.finalize_fixed()
|
||||
.into(); // digest uses an incompatible version of GenericArray. use array as intermediary
|
||||
|
||||
seed_key.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand_core::OsRng;
|
||||
|
||||
#[test]
|
||||
fn test_inbox_encryption_initialization() {
|
||||
let mut rng = OsRng;
|
||||
|
||||
// Alice (initiator) generates her identity key
|
||||
let alice_identity = StaticSecret::random_from_rng(&mut rng);
|
||||
let alice_identity_pub = PublicKey::from(&alice_identity);
|
||||
|
||||
// Bob (responder) generates his keys
|
||||
let bob_identity = StaticSecret::random_from_rng(&mut rng);
|
||||
let bob_signed_prekey = StaticSecret::random_from_rng(&mut rng);
|
||||
let bob_signed_prekey_pub = PublicKey::from(&bob_signed_prekey);
|
||||
|
||||
// Create Bob's prekey bundle
|
||||
let bob_bundle = PrekeyBundle {
|
||||
identity_key: PublicKey::from(&bob_identity),
|
||||
signed_prekey: bob_signed_prekey_pub,
|
||||
signature: [0u8; 64],
|
||||
onetime_prekey: None,
|
||||
};
|
||||
|
||||
// Alice performs handshake
|
||||
let (alice_secret, alice_ephemeral_pub) =
|
||||
InboxHandshake::perform_as_initiator(&alice_identity, &bob_bundle, &mut rng);
|
||||
|
||||
// Bob performs handshake
|
||||
let bob_secret = InboxHandshake::perform_as_responder(
|
||||
&bob_identity,
|
||||
&bob_signed_prekey,
|
||||
None,
|
||||
&alice_identity_pub,
|
||||
&alice_ephemeral_pub,
|
||||
);
|
||||
|
||||
// Both should derive the same root key
|
||||
assert_eq!(alice_secret, bob_secret);
|
||||
}
|
||||
}
|
||||
247
conversations/src/inbox/inbox.rs
Normal file
247
conversations/src/inbox/inbox.rs
Normal file
@ -0,0 +1,247 @@
|
||||
use chat_proto::logoschat::inbox::InboxV1Frame;
|
||||
use hex;
|
||||
use prost::Message;
|
||||
use prost::bytes::Bytes;
|
||||
use rand_core::OsRng;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crypto::{PrekeyBundle, SecretKey};
|
||||
|
||||
use crate::conversation::{ChatError, ConversationId, Convo, ConvoFactory, Id, PrivateV1Convo};
|
||||
use crate::crypto::{Blake2b128, CopyBytes, Digest, PublicKey, StaticSecret};
|
||||
use crate::identity::Identity;
|
||||
use crate::inbox::handshake::InboxHandshake;
|
||||
use crate::proto::{self};
|
||||
use crate::types::ContentData;
|
||||
|
||||
pub struct Inbox {
|
||||
ident: Rc<Identity>,
|
||||
local_convo_id: String,
|
||||
ephemeral_keys: HashMap<String, StaticSecret>,
|
||||
}
|
||||
|
||||
impl<'a> std::fmt::Debug for Inbox {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Inbox")
|
||||
.field("ident", &self.ident)
|
||||
.field("convo_id", &self.local_convo_id)
|
||||
.field(
|
||||
"ephemeral_keys",
|
||||
&format!("<{} keys>", self.ephemeral_keys.len()),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Inbox {
|
||||
pub fn new(ident: Rc<Identity>) -> Self {
|
||||
let local_convo_id = ident.address();
|
||||
Self {
|
||||
ident,
|
||||
local_convo_id,
|
||||
ephemeral_keys: HashMap::<String, StaticSecret>::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_local_convo_id(addr: &str) -> String {
|
||||
let hash = Blake2b128::digest(format!("{}:{}:{}", "logoschat", "inboxV1", addr));
|
||||
hex::encode(hash)
|
||||
}
|
||||
|
||||
pub fn create_bundle(&mut self) -> PrekeyBundle {
|
||||
let ephemeral = StaticSecret::random();
|
||||
|
||||
let signed_prekey = PublicKey::from(&ephemeral);
|
||||
self.ephemeral_keys
|
||||
.insert(hex::encode(signed_prekey.as_bytes()), ephemeral);
|
||||
|
||||
PrekeyBundle {
|
||||
identity_key: self.ident.public_key(),
|
||||
signed_prekey: signed_prekey,
|
||||
signature: [0u8; 64],
|
||||
onetime_prekey: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invite_to_private_convo(
|
||||
&self,
|
||||
remote_bundle: &PrekeyBundle,
|
||||
initial_message: String,
|
||||
) -> Result<(PrivateV1Convo, Vec<proto::EncryptedPayload>), ChatError> {
|
||||
let mut rng = OsRng;
|
||||
|
||||
let (seed_key, ephemeral_pub) =
|
||||
InboxHandshake::perform_as_initiator(&self.ident.secret(), remote_bundle, &mut rng);
|
||||
|
||||
let mut convo = PrivateV1Convo::new(seed_key);
|
||||
|
||||
let mut initial_payloads = convo.send_message(initial_message.as_bytes())?;
|
||||
|
||||
// Wrap First payload in Invite
|
||||
if let Some(first_message) = initial_payloads.get_mut(0) {
|
||||
let old = first_message.clone();
|
||||
let frame = Self::wrap_in_invite(old);
|
||||
|
||||
// TODO: Encrypt frame
|
||||
let ciphertext = frame.encode_to_vec();
|
||||
|
||||
let header = proto::InboxHeaderV1 {
|
||||
initiator_static: self.ident.public_key().copy_to_bytes(),
|
||||
initiator_ephemeral: ephemeral_pub.copy_to_bytes(),
|
||||
responder_static: remote_bundle.identity_key.copy_to_bytes(),
|
||||
responder_ephemeral: remote_bundle.signed_prekey.copy_to_bytes(),
|
||||
};
|
||||
|
||||
let handshake = proto::InboxHandshakeV1 {
|
||||
header: Some(header),
|
||||
payload: Bytes::from_owner(ciphertext),
|
||||
};
|
||||
|
||||
*first_message = proto::EncryptedPayload {
|
||||
encryption: Some(proto::Encryption::InboxHandshake(handshake)),
|
||||
};
|
||||
}
|
||||
|
||||
Ok((convo, initial_payloads))
|
||||
}
|
||||
|
||||
fn wrap_in_invite(payload: proto::EncryptedPayload) -> proto::InboxV1Frame {
|
||||
let invite = proto::InvitePrivateV1 {
|
||||
discriminator: "default".into(),
|
||||
initial_message: Some(payload),
|
||||
};
|
||||
|
||||
proto::InboxV1Frame {
|
||||
frame_type: Some(
|
||||
chat_proto::logoschat::inbox::inbox_v1_frame::FrameType::InvitePrivateV1(invite),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn perform_handshake(
|
||||
&self,
|
||||
payload: proto::EncryptedPayload,
|
||||
) -> Result<(SecretKey, proto::InboxV1Frame), ChatError> {
|
||||
let handshake = Self::extract_payload(payload)?;
|
||||
let header = handshake
|
||||
.header
|
||||
.ok_or(ChatError::UnexpectedPayload("InboxV1Header".into()))?;
|
||||
let pubkey_hex = hex::encode(header.responder_ephemeral.as_ref());
|
||||
|
||||
let ephemeral_key = self.lookup_ephemeral_key(&pubkey_hex)?;
|
||||
|
||||
let initator_static = PublicKey::from(
|
||||
<[u8; 32]>::try_from(header.initiator_static.as_ref())
|
||||
.map_err(|_| ChatError::BadBundleValue("wrong size - initator static".into()))?,
|
||||
);
|
||||
|
||||
let initator_ephemeral = PublicKey::from(
|
||||
<[u8; 32]>::try_from(header.initiator_ephemeral.as_ref())
|
||||
.map_err(|_| ChatError::BadBundleValue("wrong size - initator ephemeral".into()))?,
|
||||
);
|
||||
|
||||
let seed_key = InboxHandshake::perform_as_responder(
|
||||
self.ident.secret(),
|
||||
ephemeral_key,
|
||||
None,
|
||||
&initator_static,
|
||||
&initator_ephemeral,
|
||||
);
|
||||
|
||||
// TODO: Decrypt Content
|
||||
let frame = InboxV1Frame::decode(handshake.payload)?;
|
||||
Ok((seed_key, frame))
|
||||
}
|
||||
|
||||
fn extract_payload(
|
||||
payload: proto::EncryptedPayload,
|
||||
) -> Result<proto::InboxHandshakeV1, ChatError> {
|
||||
let Some(proto::Encryption::InboxHandshake(handshake)) = payload.encryption else {
|
||||
return Err(ChatError::Protocol(
|
||||
"Expected inboxhandshake encryption".into(),
|
||||
));
|
||||
};
|
||||
|
||||
Ok(handshake)
|
||||
}
|
||||
|
||||
fn decrypt_frame(
|
||||
enc_payload: proto::InboxHandshakeV1,
|
||||
) -> Result<proto::InboxV1Frame, ChatError> {
|
||||
let frame_bytes = enc_payload.payload;
|
||||
// TODO: decrypt payload
|
||||
let frame = proto::InboxV1Frame::decode(frame_bytes)?;
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
fn lookup_ephemeral_key(&self, key: &str) -> Result<&StaticSecret, ChatError> {
|
||||
self.ephemeral_keys
|
||||
.get(key)
|
||||
.ok_or_else(|| return ChatError::UnknownEphemeralKey())
|
||||
}
|
||||
}
|
||||
|
||||
impl Id for Inbox {
|
||||
fn id(&self) -> ConversationId {
|
||||
&self.local_convo_id
|
||||
}
|
||||
}
|
||||
|
||||
impl ConvoFactory for Inbox {
|
||||
fn handle_frame(
|
||||
&mut self,
|
||||
message: &[u8],
|
||||
) -> Result<(Box<dyn Convo>, Vec<ContentData>), ChatError> {
|
||||
if message.len() == 0 {
|
||||
return Err(ChatError::Protocol("Example error".into()));
|
||||
}
|
||||
|
||||
let ep = proto::EncryptedPayload::decode(message)?;
|
||||
let (seed_key, frame) = self.perform_handshake(ep)?;
|
||||
|
||||
match frame.frame_type.unwrap() {
|
||||
chat_proto::logoschat::inbox::inbox_v1_frame::FrameType::InvitePrivateV1(
|
||||
_invite_private_v1,
|
||||
) => {
|
||||
let convo = PrivateV1Convo::new(seed_key);
|
||||
// TODO: Update PrivateV1 Constructor with DR, initial_message
|
||||
Ok((Box::new(convo), vec![]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_invite_privatev1_roundtrip() {
|
||||
let saro_ident = Identity::new();
|
||||
let saro_inbox = Inbox::new(saro_ident.into());
|
||||
|
||||
let raya_ident = Identity::new();
|
||||
let mut raya_inbox = Inbox::new(raya_ident.into());
|
||||
|
||||
let bundle = raya_inbox.create_bundle();
|
||||
let (_, payloads) = saro_inbox
|
||||
.invite_to_private_convo(&bundle, "hello".into())
|
||||
.unwrap();
|
||||
|
||||
let encrypted_payload = payloads
|
||||
.get(0)
|
||||
.expect("RemoteInbox::invite_to_private_convo did not generate any payloads");
|
||||
|
||||
let mut buf = Vec::new();
|
||||
encrypted_payload.encode(&mut buf).unwrap();
|
||||
|
||||
// Test handle_frame with valid payload
|
||||
let result = raya_inbox.handle_frame(&buf);
|
||||
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"handle_frame should accept valid encrypted payloads"
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,15 +1,17 @@
|
||||
mod api;
|
||||
mod context;
|
||||
mod conversation;
|
||||
mod crypto;
|
||||
mod errors;
|
||||
mod identity;
|
||||
mod inbox;
|
||||
mod proto;
|
||||
mod types;
|
||||
mod utils;
|
||||
|
||||
pub use api::*;
|
||||
|
||||
#[cfg(test)]
|
||||
use conversation::{ConversationStore, GroupTestConvo, PrivateV1Convo};
|
||||
use inbox::Inbox;
|
||||
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
@ -113,41 +115,4 @@ mod tests {
|
||||
|
||||
assert_eq!(result, -1, "Should return ERR_BAD_PTR for null pointer");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convo_store_get() {
|
||||
let mut store: ConversationStore = ConversationStore::new();
|
||||
|
||||
let new_convo = GroupTestConvo::new();
|
||||
let convo_id = store.insert(new_convo);
|
||||
|
||||
let convo = store.get_mut(&convo_id).ok_or_else(|| 0);
|
||||
convo.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_convo_example() {
|
||||
let mut store: ConversationStore = ConversationStore::new();
|
||||
|
||||
let raya = Inbox::new("Raya");
|
||||
let saro = PrivateV1Convo::new();
|
||||
let pax = GroupTestConvo::new();
|
||||
|
||||
store.insert(raya);
|
||||
store.insert(saro);
|
||||
let convo_id = store.insert(pax);
|
||||
|
||||
for id in store.conversation_ids().collect::<Vec<_>>() {
|
||||
let a = store.get_mut(&id).unwrap();
|
||||
a.send_frame(b"test message").unwrap();
|
||||
println!("Conversation ID: {} :: {:?}", id, a);
|
||||
}
|
||||
|
||||
for id in store.conversation_ids().collect::<Vec<_>>() {
|
||||
let a = store.get_mut(&id).unwrap();
|
||||
let _ = a.handle_frame(&[0x1, 0x2]);
|
||||
}
|
||||
|
||||
println!("ID -> {}", store.get(&convo_id).unwrap().id());
|
||||
}
|
||||
}
|
||||
|
||||
5
conversations/src/proto.rs
Normal file
5
conversations/src/proto.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub use chat_proto::logoschat::encryption::encrypted_payload::Encryption;
|
||||
pub use chat_proto::logoschat::encryption::inbox_handshake_v1::InboxHeaderV1;
|
||||
pub use chat_proto::logoschat::encryption::{EncryptedPayload, InboxHandshakeV1};
|
||||
pub use chat_proto::logoschat::inbox::InboxV1Frame;
|
||||
pub use chat_proto::logoschat::invite::InvitePrivateV1;
|
||||
13
conversations/src/types.rs
Normal file
13
conversations/src/types.rs
Normal file
@ -0,0 +1,13 @@
|
||||
// This struct represents Outbound data.
|
||||
// It wraps an encoded payload with a delivery address, so it can be handled by the delivery service.
|
||||
pub struct PayloadData {
|
||||
pub delivery_address: String,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
// This struct represents the result of processed inbound data.
|
||||
// It wraps content payload with a conversation_id
|
||||
pub struct ContentData {
|
||||
pub conversation_id: String,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
8
conversations/src/utils.rs
Normal file
8
conversations/src/utils.rs
Normal file
@ -0,0 +1,8 @@
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub fn timestamp_millis() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as i64
|
||||
}
|
||||
14
crypto/Cargo.toml
Normal file
14
crypto/Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "crypto"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
x25519-dalek = { version = "2.0.1", features = ["static_secrets"] }
|
||||
hkdf = "0.12"
|
||||
sha2 = "0.10"
|
||||
rand_core = { version = "0.6", features = ["getrandom"] }
|
||||
ed25519-dalek = "2.2.0"
|
||||
xeddsa = "1.0.2"
|
||||
zeroize = {version = "1.8.2", features= ["derive"]}
|
||||
generic-array = "1.3.5"
|
||||
31
crypto/src/keys.rs
Normal file
31
crypto/src/keys.rs
Normal file
@ -0,0 +1,31 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
pub use generic_array::{GenericArray, typenum::U32};
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
#[derive(Clone, Zeroize, ZeroizeOnDrop, PartialEq)]
|
||||
pub struct SecretKey([u8; 32]);
|
||||
|
||||
impl SecretKey {
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.0.as_slice()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; 32]> for SecretKey {
|
||||
fn from(value: [u8; 32]) -> Self {
|
||||
SecretKey(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GenericArray<u8, U32>> for SecretKey {
|
||||
fn from(value: GenericArray<u8, U32>) -> Self {
|
||||
SecretKey(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for SecretKey {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("SecretKey").field(&"<32 bytes>").finish()
|
||||
}
|
||||
}
|
||||
5
crypto/src/lib.rs
Normal file
5
crypto/src/lib.rs
Normal file
@ -0,0 +1,5 @@
|
||||
mod keys;
|
||||
mod x3dh;
|
||||
|
||||
pub use keys::{GenericArray, SecretKey};
|
||||
pub use x3dh::{DomainSeparator, PrekeyBundle, X3Handshake};
|
||||
214
crypto/src/x3dh.rs
Normal file
214
crypto/src/x3dh.rs
Normal file
@ -0,0 +1,214 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use hkdf::Hkdf;
|
||||
use rand_core::{CryptoRng, RngCore};
|
||||
use sha2::Sha256;
|
||||
use x25519_dalek::{PublicKey, SharedSecret, StaticSecret};
|
||||
|
||||
use crate::keys::SecretKey;
|
||||
|
||||
/// A prekey bundle containing the public keys needed to initiate an X3DH key exchange.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PrekeyBundle {
|
||||
pub identity_key: PublicKey,
|
||||
pub signed_prekey: PublicKey,
|
||||
pub signature: [u8; 64],
|
||||
pub onetime_prekey: Option<PublicKey>,
|
||||
}
|
||||
|
||||
pub trait DomainSeparator {
|
||||
const BYTES: &'static [u8];
|
||||
}
|
||||
|
||||
pub struct X3Handshake<D: DomainSeparator> {
|
||||
_phantom: PhantomData<D>,
|
||||
}
|
||||
|
||||
impl<D: DomainSeparator> X3Handshake<D> {
|
||||
fn domain_separator() -> &'static [u8] {
|
||||
D::BYTES
|
||||
}
|
||||
|
||||
/// Derive the shared secret from DH outputs using HKDF-SHA256
|
||||
fn derive_shared_secret(
|
||||
dh1: &SharedSecret,
|
||||
dh2: &SharedSecret,
|
||||
dh3: &SharedSecret,
|
||||
dh4: Option<&SharedSecret>,
|
||||
) -> SecretKey {
|
||||
// Concatenate all DH outputs
|
||||
let mut km = Vec::new();
|
||||
km.extend_from_slice(dh1.as_bytes());
|
||||
km.extend_from_slice(dh2.as_bytes());
|
||||
km.extend_from_slice(dh3.as_bytes());
|
||||
if let Some(dh4) = dh4 {
|
||||
km.extend_from_slice(dh4.as_bytes());
|
||||
}
|
||||
|
||||
// Use HKDF to derive the shared secret
|
||||
// Using "X3DH" as the info parameter as per Signal protocol
|
||||
let hk = Hkdf::<Sha256>::new(None, &km);
|
||||
let mut output = [0u8; 32];
|
||||
hk.expand(Self::domain_separator(), &mut output)
|
||||
.expect("32 bytes is valid HKDF output length");
|
||||
|
||||
// Move into SecretKey so it gets zeroized on drop.
|
||||
output.into()
|
||||
}
|
||||
|
||||
/// Perform X3DH key agreement as the initiator
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `identity_keypair` - Initiator's long-term identity key pair
|
||||
/// * `recipient_bundle` - Recipient's prekey bundle
|
||||
/// * `rng` - Cryptographically secure random number generator
|
||||
///
|
||||
/// # Returns
|
||||
/// A tuple of (shared secret bytes, ephemeral public key)
|
||||
pub fn initator<R: RngCore + CryptoRng>(
|
||||
identity_keypair: &StaticSecret,
|
||||
recipient_bundle: &PrekeyBundle,
|
||||
rng: &mut R,
|
||||
) -> (SecretKey, PublicKey) {
|
||||
// Generate ephemeral key for this handshake (using StaticSecret for multiple DH operations)
|
||||
let ephemeral_secret = StaticSecret::random_from_rng(rng);
|
||||
let ephemeral_public = PublicKey::from(&ephemeral_secret);
|
||||
|
||||
// Perform the 4 Diffie-Hellman operations
|
||||
let dh1 = identity_keypair.diffie_hellman(&recipient_bundle.signed_prekey);
|
||||
let dh2 = ephemeral_secret.diffie_hellman(&recipient_bundle.identity_key);
|
||||
let dh3 = ephemeral_secret.diffie_hellman(&recipient_bundle.signed_prekey);
|
||||
let dh4 = recipient_bundle
|
||||
.onetime_prekey
|
||||
.as_ref()
|
||||
.map(|opk| ephemeral_secret.diffie_hellman(opk));
|
||||
|
||||
// Combine all DH outputs into shared secret
|
||||
let shared_secret = Self::derive_shared_secret(&dh1, &dh2, &dh3, dh4.as_ref());
|
||||
|
||||
(shared_secret, ephemeral_public)
|
||||
}
|
||||
|
||||
/// Perform X3DH key agreement as the responder
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `identity_keypair` - Responder's long-term identity key pair
|
||||
/// * `signed_prekey` - Responder's signed prekey (private)
|
||||
/// * `onetime_prekey` - Responder's one-time prekey (private, if used)
|
||||
/// * `initiator_identity` - Initiator's identity public key
|
||||
/// * `initiator_ephemeral` - Initiator's ephemeral public key
|
||||
///
|
||||
/// # Returns
|
||||
/// The derived shared secret bytes
|
||||
pub fn responder(
|
||||
identity_keypair: &StaticSecret,
|
||||
signed_prekey: &StaticSecret,
|
||||
onetime_prekey: Option<&StaticSecret>,
|
||||
initiator_identity: &PublicKey,
|
||||
initiator_ephemeral: &PublicKey,
|
||||
) -> SecretKey {
|
||||
let dh1 = signed_prekey.diffie_hellman(initiator_identity);
|
||||
let dh2 = identity_keypair.diffie_hellman(initiator_ephemeral);
|
||||
let dh3 = signed_prekey.diffie_hellman(initiator_ephemeral);
|
||||
let dh4 = onetime_prekey.map(|opk| opk.diffie_hellman(initiator_ephemeral));
|
||||
|
||||
// Combine all DH outputs into shared secret
|
||||
Self::derive_shared_secret(&dh1, &dh2, &dh3, dh4.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand_core::OsRng;
|
||||
|
||||
pub struct TestProtocol;
|
||||
impl DomainSeparator for TestProtocol {
|
||||
const BYTES: &'static [u8] = b"x3dh_tests_v1";
|
||||
}
|
||||
|
||||
type X3DH = X3Handshake<TestProtocol>;
|
||||
|
||||
#[test]
|
||||
fn test_x3dh_with_onetime_key() {
|
||||
let mut rng = OsRng;
|
||||
|
||||
// Alice (initiator) generates her identity key
|
||||
let alice_identity = StaticSecret::random_from_rng(&mut rng);
|
||||
let alice_identity_pub = PublicKey::from(&alice_identity);
|
||||
|
||||
// Bob (responder) generates his keys
|
||||
let bob_identity = StaticSecret::random_from_rng(&mut rng);
|
||||
let bob_identity_pub = PublicKey::from(&bob_identity);
|
||||
|
||||
let bob_signed_prekey = StaticSecret::random_from_rng(&mut rng);
|
||||
let bob_signed_prekey_pub = PublicKey::from(&bob_signed_prekey);
|
||||
|
||||
let bob_onetime_prekey = StaticSecret::random_from_rng(&mut rng);
|
||||
let bob_onetime_prekey_pub = PublicKey::from(&bob_onetime_prekey);
|
||||
|
||||
// Create Bob's prekey bundle (with one-time prekey)
|
||||
let bob_bundle = PrekeyBundle {
|
||||
identity_key: bob_identity_pub,
|
||||
signed_prekey: bob_signed_prekey_pub,
|
||||
signature: [0u8; 64], // Placeholder for signature
|
||||
onetime_prekey: Some(bob_onetime_prekey_pub),
|
||||
};
|
||||
|
||||
// Alice performs X3DH
|
||||
let (alice_shared_secret, alice_ephemeral_pub) =
|
||||
X3DH::initator(&alice_identity, &bob_bundle, &mut rng);
|
||||
|
||||
// Bob performs X3DH
|
||||
let bob_shared_secret = X3DH::responder(
|
||||
&bob_identity,
|
||||
&bob_signed_prekey,
|
||||
Some(&bob_onetime_prekey),
|
||||
&alice_identity_pub,
|
||||
&alice_ephemeral_pub,
|
||||
);
|
||||
|
||||
// Both should derive the same shared secret
|
||||
assert_eq!(alice_shared_secret, bob_shared_secret);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_x3dh_without_onetime_key() {
|
||||
let mut rng = OsRng;
|
||||
|
||||
// Alice (initiator) generates her identity key
|
||||
let alice_identity = StaticSecret::random_from_rng(&mut rng);
|
||||
let alice_identity_pub = PublicKey::from(&alice_identity);
|
||||
|
||||
// Bob (responder) generates his keys
|
||||
let bob_identity = StaticSecret::random_from_rng(&mut rng);
|
||||
let bob_identity_pub = PublicKey::from(&bob_identity);
|
||||
|
||||
let bob_signed_prekey = StaticSecret::random_from_rng(&mut rng);
|
||||
let bob_signed_prekey_pub = PublicKey::from(&bob_signed_prekey);
|
||||
|
||||
// Create Bob's prekey bundle (without one-time prekey)
|
||||
let bob_bundle = PrekeyBundle {
|
||||
identity_key: bob_identity_pub,
|
||||
signed_prekey: bob_signed_prekey_pub,
|
||||
signature: [0u8; 64], // Placeholder for signature
|
||||
onetime_prekey: None,
|
||||
};
|
||||
|
||||
// Alice performs X3DH
|
||||
let (alice_shared_secret, alice_ephemeral_pub) =
|
||||
X3DH::initator(&alice_identity, &bob_bundle, &mut rng);
|
||||
|
||||
// Bob performs X3DH
|
||||
let bob_shared_secret = X3DH::responder(
|
||||
&bob_identity,
|
||||
&bob_signed_prekey,
|
||||
None,
|
||||
&alice_identity_pub,
|
||||
&alice_ephemeral_pub,
|
||||
);
|
||||
|
||||
// Both should derive the same shared secret
|
||||
assert_eq!(alice_shared_secret, bob_shared_secret);
|
||||
}
|
||||
}
|
||||
3
rust_toolchain.toml
Normal file
3
rust_toolchain.toml
Normal file
@ -0,0 +1,3 @@
|
||||
[toolchain]
|
||||
channel = "stable"
|
||||
components = ["rustfmt", "clippy"]
|
||||
Loading…
x
Reference in New Issue
Block a user