mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-05-18 07:59:50 +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"
|
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crypto-common",
|
"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]]
|
[[package]]
|
||||||
name = "blake2"
|
name = "blake2"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
@ -27,9 +39,15 @@ version = "0.10.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||||
dependencies = [
|
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]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@ -60,6 +78,14 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chat-proto"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/logos-messaging/chat_proto#21cd52b0649c959f43eec19e1edad12451ccc382"
|
||||||
|
dependencies = [
|
||||||
|
"prost",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cipher"
|
name = "cipher"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
@ -71,6 +97,12 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-oid"
|
||||||
|
version = "0.9.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@ -80,13 +112,27 @@ dependencies = [
|
|||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array",
|
"generic-array 0.14.7",
|
||||||
"rand_core",
|
"rand_core",
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
@ -100,6 +146,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
"curve25519-dalek-derive",
|
"curve25519-dalek-derive",
|
||||||
|
"digest",
|
||||||
"fiat-crypto",
|
"fiat-crypto",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"subtle",
|
"subtle",
|
||||||
@ -117,6 +164,27 @@ dependencies = [
|
|||||||
"syn 2.0.111",
|
"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]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
@ -143,6 +211,36 @@ dependencies = [
|
|||||||
"zeroize",
|
"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]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@ -200,6 +298,16 @@ dependencies = [
|
|||||||
"version_check",
|
"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]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
@ -217,6 +325,12 @@ version = "0.16.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hkdf"
|
name = "hkdf"
|
||||||
version = "0.12.4"
|
version = "0.12.4"
|
||||||
@ -251,7 +365,7 @@ version = "0.1.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array",
|
"generic-array 0.14.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -263,6 +377,15 @@ dependencies = [
|
|||||||
"rustversion",
|
"rustversion",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.178"
|
version = "0.2.178"
|
||||||
@ -273,7 +396,14 @@ checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
|
|||||||
name = "logos-chat"
|
name = "logos-chat"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"blake2",
|
||||||
|
"chat-proto",
|
||||||
|
"crypto",
|
||||||
|
"hex",
|
||||||
|
"prost",
|
||||||
|
"rand_core",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"x25519-dalek",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -310,6 +440,16 @@ version = "1.0.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
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]]
|
[[package]]
|
||||||
name = "poly1305"
|
name = "poly1305"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@ -358,6 +498,29 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"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]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.42"
|
version = "1.0.42"
|
||||||
@ -486,12 +649,42 @@ dependencies = [
|
|||||||
"syn 2.0.111",
|
"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]]
|
[[package]]
|
||||||
name = "sha2-const-stable"
|
name = "sha2-const-stable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9"
|
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]]
|
[[package]]
|
||||||
name = "stabby"
|
name = "stabby"
|
||||||
version = "36.2.2"
|
version = "36.2.2"
|
||||||
@ -695,6 +888,22 @@ dependencies = [
|
|||||||
"zeroize",
|
"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]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.31"
|
version = "0.8.31"
|
||||||
|
|||||||
@ -4,5 +4,9 @@ resolver = "3"
|
|||||||
|
|
||||||
members = [
|
members = [
|
||||||
"conversations",
|
"conversations",
|
||||||
|
"crypto",
|
||||||
"double-ratchets",
|
"double-ratchets",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
blake2 = "0.10"
|
||||||
|
|||||||
@ -7,4 +7,11 @@ edition = "2024"
|
|||||||
crate-type = ["staticlib"]
|
crate-type = ["staticlib"]
|
||||||
|
|
||||||
[dependencies]
|
[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"
|
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,
|
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
|
/// 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()
|
/// Opaque handle to the store. Must be freed with conversation_store_destroy()
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub extern "C" fn create_context() -> ContextHandle {
|
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
|
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 {
|
use crypto::PrekeyBundle;
|
||||||
pub delivery_address: String,
|
|
||||||
pub data: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ContentData {
|
use crate::{
|
||||||
pub conversation_id: String,
|
conversation::{ConversationId, ConversationIdOwned, ConversationStore},
|
||||||
pub data: Vec<u8>,
|
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,
|
store: ConversationStore,
|
||||||
|
inbox: Inbox,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ctx {
|
impl Context {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
let identity = Rc::new(Identity::new());
|
||||||
|
let inbox = Inbox::new(Rc::clone(&identity)); //
|
||||||
Self {
|
Self {
|
||||||
|
_identity: identity,
|
||||||
store: ConversationStore::new(),
|
store: ConversationStore::new(),
|
||||||
|
inbox,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_private_convo(&mut self, _content: &[u8]) -> ConversationIdOwned {
|
pub fn create_private_convo(
|
||||||
let new_convo = PrivateV1Convo::new();
|
&mut self,
|
||||||
self.store.insert(new_convo)
|
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> {
|
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;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub use crate::errors::ChatError;
|
pub use crate::errors::ChatError;
|
||||||
|
use crate::types::ContentData;
|
||||||
/////////////////////////////////////////////////
|
|
||||||
// Type Definitions
|
|
||||||
/////////////////////////////////////////////////
|
|
||||||
|
|
||||||
pub type ConversationId<'a> = &'a str;
|
pub type ConversationId<'a> = &'a str;
|
||||||
pub type ConversationIdOwned = Arc<str>;
|
pub type ConversationIdOwned = Arc<str>;
|
||||||
|
|
||||||
/////////////////////////////////////////////////
|
pub trait Id: Debug {
|
||||||
// Trait Definitions
|
|
||||||
/////////////////////////////////////////////////
|
|
||||||
|
|
||||||
pub trait Convo: Debug {
|
|
||||||
fn id(&self) -> ConversationId;
|
fn id(&self) -> ConversationId;
|
||||||
fn send_frame(&mut self, message: &[u8]) -> Result<(), ChatError>;
|
|
||||||
fn handle_frame(&mut self, message: &[u8]) -> Result<(), ChatError>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/////////////////////////////////////////////////
|
pub trait ConvoFactory: Id + Debug {
|
||||||
// Structs
|
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 {
|
pub struct ConversationStore {
|
||||||
conversations: HashMap<Arc<str>, Box<dyn Convo>>,
|
conversations: HashMap<Arc<str>, Box<dyn Convo>>,
|
||||||
|
factories: HashMap<Arc<str>, Box<dyn ConvoFactory>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConversationStore {
|
impl ConversationStore {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
conversations: HashMap::new(),
|
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());
|
let key: ConversationIdOwned = Arc::from(conversation.id());
|
||||||
self.conversations
|
self.conversations
|
||||||
.insert(key.clone(), Box::new(conversation));
|
.insert(key.clone(), Box::new(conversation));
|
||||||
key
|
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 + '_)> {
|
pub fn get(&self, id: ConversationId) -> Option<&(dyn Convo + '_)> {
|
||||||
self.conversations.get(id).map(|c| c.as_ref())
|
self.conversations.get(id).map(|c| c.as_ref())
|
||||||
}
|
}
|
||||||
@ -51,17 +60,22 @@ impl ConversationStore {
|
|||||||
Some(self.conversations.get_mut(id)?.as_mut())
|
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> + '_ {
|
pub fn conversation_ids(&self) -> impl Iterator<Item = ConversationIdOwned> + '_ {
|
||||||
self.conversations.keys().cloned()
|
self.conversations.keys().cloned()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////
|
pub fn factory_ids(&self) -> impl Iterator<Item = ConversationIdOwned> + '_ {
|
||||||
// Modules
|
self.factories.keys().cloned()
|
||||||
/////////////////////////////////////////////////
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mod group_test;
|
mod group_test;
|
||||||
mod privatev1;
|
mod privatev1;
|
||||||
|
|
||||||
|
use crate::proto::EncryptedPayload;
|
||||||
pub use group_test::GroupTestConvo;
|
pub use group_test::GroupTestConvo;
|
||||||
pub use privatev1::PrivateV1Convo;
|
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)]
|
#[derive(Debug)]
|
||||||
pub struct GroupTestConvo {}
|
pub struct GroupTestConvo {}
|
||||||
@ -9,19 +11,15 @@ impl GroupTestConvo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Convo for GroupTestConvo {
|
impl Id for GroupTestConvo {
|
||||||
fn id(&self) -> ConversationId {
|
fn id(&self) -> ConversationId {
|
||||||
// implementation
|
// implementation
|
||||||
"grouptest"
|
"grouptest"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn send_frame(&mut self, _message: &[u8]) -> Result<(), ChatError> {
|
impl Convo for GroupTestConvo {
|
||||||
// todo!("Not Implemented")
|
fn send_message(&mut self, _content: &[u8]) -> Result<Vec<EncryptedPayload>, ChatError> {
|
||||||
Ok(())
|
Ok(vec![])
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_frame(&mut self, _message: &[u8]) -> Result<(), ChatError> {
|
|
||||||
// todo!("Not Implemented")
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)]
|
#[derive(Debug)]
|
||||||
pub struct PrivateV1Convo {}
|
pub struct PrivateV1Convo {}
|
||||||
|
|
||||||
impl PrivateV1Convo {
|
impl PrivateV1Convo {
|
||||||
pub fn new() -> Self {
|
pub fn new(_seed_key: SecretKey) -> Self {
|
||||||
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 {
|
impl Convo for PrivateV1Convo {
|
||||||
fn id(&self) -> ConversationId {
|
fn send_message(&mut self, content: &[u8]) -> Result<Vec<EncryptedPayload>, ChatError> {
|
||||||
// implementation
|
let frame = PrivateV1Frame {
|
||||||
"private_v1_convo_id"
|
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> {
|
let ef = self.encrypt(frame);
|
||||||
todo!("Not Implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_frame(&mut self, _message: &[u8]) -> Result<(), ChatError> {
|
Ok(vec![ef])
|
||||||
todo!("Not Implemented")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
pub enum ChatError {
|
||||||
#[error("protocol error: {0:?}")]
|
#[error("protocol error: {0:?}")]
|
||||||
Protocol(String),
|
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 use inbox::Inbox;
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
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 api;
|
||||||
mod context;
|
mod context;
|
||||||
mod conversation;
|
mod conversation;
|
||||||
|
mod crypto;
|
||||||
mod errors;
|
mod errors;
|
||||||
|
mod identity;
|
||||||
mod inbox;
|
mod inbox;
|
||||||
|
mod proto;
|
||||||
|
mod types;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
pub use api::*;
|
pub use api::*;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use conversation::{ConversationStore, GroupTestConvo, PrivateV1Convo};
|
|
||||||
use inbox::Inbox;
|
|
||||||
|
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -113,41 +115,4 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(result, -1, "Should return ERR_BAD_PTR for null pointer");
|
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