diff --git a/.gitignore b/.gitignore index 1dc0b45..7e66fdf 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,8 @@ target # Temporary data folder tmp +# Auto-generated C headers (regenerated by flake.nix build) +libchat.h +double_ratchet.h + .DS_Store diff --git a/core/conversations/Cargo.toml b/core/conversations/Cargo.toml index 4ea9408..7df95b2 100644 --- a/core/conversations/Cargo.toml +++ b/core/conversations/Cargo.toml @@ -4,7 +4,14 @@ version = "0.1.0" edition = "2024" [lib] -crate-type = ["rlib","staticlib","dylib"] +crate-type = ["rlib","staticlib","cdylib"] + +[features] +headers = ["safer-ffi/headers"] + +[[bin]] +name = "generate-libchat-headers" +required-features = ["headers"] [dependencies] base64 = "0.22" diff --git a/core/conversations/src/api.rs b/core/conversations/src/api.rs index bd1e300..3041b1f 100644 --- a/core/conversations/src/api.rs +++ b/core/conversations/src/api.rs @@ -49,9 +49,9 @@ pub struct ContextHandle(pub(crate) Context); /// # Returns /// Opaque handle to the store. Must be freed with destroy_context() #[ffi_export] -pub fn create_context(name: repr_c::String) -> repr_c::Box { - // Deference name to to `str` and then borrow to &str - Box::new(ContextHandle(Context::new_with_name(&*name))).into() +pub fn create_context(name: c_slice::Ref<'_, u8>) -> repr_c::Box { + let name_str = std::str::from_utf8(name.as_slice()).unwrap_or("default"); + Box::new(ContextHandle(Context::new_with_name(name_str))).into() } /// Returns the friendly name of the contexts installation. @@ -61,6 +61,13 @@ pub fn installation_name(ctx: &ContextHandle) -> repr_c::String { ctx.0.installation_name().to_string().into() } +/// Returns the local delivery address (hex-encoded Blake2b-512 of the installation public key). +/// This is the address other installations use to send messages to this context. +#[ffi_export] +pub fn local_delivery_address(ctx: &ContextHandle) -> repr_c::String { + ctx.0.local_delivery_address().to_string().into() +} + /// Destroys a conversation store and frees its memory /// /// # Safety @@ -182,11 +189,12 @@ pub fn list_conversations(ctx: &mut ContextHandle) -> ListConvoResult { #[ffi_export] pub fn send_content( ctx: &mut ContextHandle, - convo_id: repr_c::String, + convo_id: c_slice::Ref<'_, u8>, content: c_slice::Ref<'_, u8>, out: &mut SendContentResult, ) { - let payloads = match ctx.0.send_content(&convo_id, &content) { + let convo_id_str = std::str::from_utf8(convo_id.as_slice()).unwrap_or(""); + let payloads = match ctx.0.send_content(convo_id_str, &content) { Ok(p) => p, Err(_) => { *out = SendContentResult { diff --git a/core/conversations/src/bin/generate-libchat-headers.rs b/core/conversations/src/bin/generate-libchat-headers.rs new file mode 100644 index 0000000..ac480ce --- /dev/null +++ b/core/conversations/src/bin/generate-libchat-headers.rs @@ -0,0 +1,3 @@ +fn main() -> std::io::Result<()> { + libchat::generate_headers() +} diff --git a/core/conversations/src/context.rs b/core/conversations/src/context.rs index bec37a5..70a52cd 100644 --- a/core/conversations/src/context.rs +++ b/core/conversations/src/context.rs @@ -65,6 +65,10 @@ impl Context { self._identity.get_name() } + pub fn local_delivery_address(&self) -> &str { + self.inbox.id() + } + pub fn create_private_convo( &mut self, remote_bundle: &Introduction, diff --git a/core/conversations/src/conversation/privatev1.rs b/core/conversations/src/conversation/privatev1.rs index 0b8042e..0289be5 100644 --- a/core/conversations/src/conversation/privatev1.rs +++ b/core/conversations/src/conversation/privatev1.rs @@ -55,11 +55,12 @@ impl BaseConvoId { pub struct PrivateV1Convo { local_convo_id: String, remote_convo_id: String, + remote_delivery_address: String, dr_state: RatchetState, } impl PrivateV1Convo { - pub fn new_initiator(seed_key: SymmetricKey32, remote: PublicKey) -> Self { + pub fn new_initiator(seed_key: SymmetricKey32, remote: PublicKey, remote_delivery_address: String) -> Self { let base_convo_id = BaseConvoId::new(&seed_key); let local_convo_id = base_convo_id.id_for_participant(Role::Initiator); let remote_convo_id = base_convo_id.id_for_participant(Role::Responder); @@ -73,11 +74,12 @@ impl PrivateV1Convo { Self { local_convo_id, remote_convo_id, + remote_delivery_address, dr_state, } } - pub fn new_responder(seed_key: SymmetricKey32, dh_self: &PrivateKey) -> Self { + pub fn new_responder(seed_key: SymmetricKey32, dh_self: &PrivateKey, remote_delivery_address: String) -> Self { let base_convo_id = BaseConvoId::new(&seed_key); let local_convo_id = base_convo_id.id_for_participant(Role::Responder); let remote_convo_id = base_convo_id.id_for_participant(Role::Initiator); @@ -92,6 +94,7 @@ impl PrivateV1Convo { Self { local_convo_id, remote_convo_id, + remote_delivery_address, dr_state, } } @@ -179,7 +182,7 @@ impl Convo for PrivateV1Convo { let data = self.encrypt(frame); Ok(vec![AddressedEncryptedPayload { - delivery_address: "delivery_address".into(), + delivery_address: self.remote_delivery_address.clone(), data, }]) } @@ -236,8 +239,8 @@ mod tests { let seed_key_saro = SymmetricKey32::from(seed_key); let seed_key_raya = SymmetricKey32::from(seed_key); let send_content_bytes = vec![0, 2, 4, 6, 8]; - let mut sr_convo = PrivateV1Convo::new_initiator(seed_key_saro, pub_raya); - let mut rs_convo = PrivateV1Convo::new_responder(seed_key_raya, &raya); + let mut sr_convo = PrivateV1Convo::new_initiator(seed_key_saro, pub_raya, "test_addr".into()); + let mut rs_convo = PrivateV1Convo::new_responder(seed_key_raya, &raya, "test_addr".into()); let send_frame = PrivateV1Frame { conversation_id: "_".into(), diff --git a/core/conversations/src/inbox/handler.rs b/core/conversations/src/inbox/handler.rs index 278ae16..6752b3a 100644 --- a/core/conversations/src/inbox/handler.rs +++ b/core/conversations/src/inbox/handler.rs @@ -16,12 +16,6 @@ use crate::inbox::handshake::InboxHandshake; use crate::proto; use crate::types::{AddressedEncryptedPayload, ContentData}; -/// Compute the deterministic Delivery_address for an installation -fn delivery_address_for_installation(_: PublicKey) -> String { - // TODO: Implement Delivery Address - "delivery_address".into() -} - pub struct Inbox { ident: Rc, local_convo_id: String, @@ -78,13 +72,12 @@ impl Inbox { let (seed_key, ephemeral_pub) = InboxHandshake::perform_as_initiator(self.ident.secret(), &pkb, &mut rng); - let mut convo = PrivateV1Convo::new_initiator(seed_key, *remote_bundle.ephemeral_key()); + let remote_delivery_addr = Inbox::inbox_identifier_for_key(*remote_bundle.installation_key()); + let mut convo = PrivateV1Convo::new_initiator(seed_key, *remote_bundle.ephemeral_key(), remote_delivery_addr.clone()); let mut payloads = convo.send_message(initial_message)?; - // Wrap First payload in Invite if let Some(first_message) = payloads.get_mut(0) { - // Take the the value of .data - it's being replaced at the end of this block let frame = Self::wrap_in_invite(std::mem::take(&mut first_message.data)); // TODO: Encrypt frame @@ -102,10 +95,7 @@ impl Inbox { payload: Bytes::from_owner(ciphertext), }; - // Update the address field with the Inbox delivery_Address - first_message.delivery_address = - delivery_address_for_installation(*remote_bundle.installation_key()); - // Update the data field with new Payload + first_message.delivery_address = remote_delivery_addr; first_message.data = proto::EncryptedPayload { encryption: Some(proto::Encryption::InboxHandshake(handshake)), }; @@ -128,12 +118,18 @@ impl Inbox { let key_index = hex::encode(header.responder_ephemeral.as_ref()); let ephemeral_key = self.lookup_ephemeral_key(&key_index)?; + // Extract initiator's identity key for delivery address before header is consumed + let initiator_static_bytes: [u8; 32] = header.initiator_static.as_ref() + .try_into() + .map_err(|_| ChatError::BadBundleValue("wrong size - initiator static".into()))?; + let remote_delivery_addr = Inbox::inbox_identifier_for_key(PublicKey::from(initiator_static_bytes)); + // Perform handshake and decrypt frame let (seed_key, frame) = self.perform_handshake(ephemeral_key, header, handshake.payload)?; match frame.frame_type.unwrap() { proto::inbox_v1_frame::FrameType::InvitePrivateV1(_invite_private_v1) => { - let mut convo = PrivateV1Convo::new_responder(seed_key, ephemeral_key); + let mut convo = PrivateV1Convo::new_responder(seed_key, ephemeral_key, remote_delivery_addr); let Some(enc_payload) = _invite_private_v1.initial_message else { return Err(ChatError::Protocol("missing initial encpayload".into())); diff --git a/core/conversations/src/lib.rs b/core/conversations/src/lib.rs index de0c023..0cb6bf6 100644 --- a/core/conversations/src/lib.rs +++ b/core/conversations/src/lib.rs @@ -14,6 +14,13 @@ pub use api::*; pub use context::{Context, Introduction}; pub use errors::ChatError; +#[cfg(feature = "headers")] +pub fn generate_headers() -> std::io::Result<()> { + safer_ffi::headers::builder() + .to_file("libchat.h")? + .generate() +} + #[cfg(test)] mod tests { @@ -24,8 +31,8 @@ mod tests { #[test] fn test_message_roundtrip() { - let mut saro = create_context("saro".into()); - let mut raya = create_context("raya".into()); + let mut saro = create_context(b"saro".as_slice().into()); + let mut raya = create_context(b"raya".as_slice().into()); // Raya Creates Bundle and Sends to Saro let mut intro_result = CreateIntroResult { diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..7653b8c --- /dev/null +++ b/flake.nix @@ -0,0 +1,95 @@ +{ + description = "libchat - Logos Chat cryptographic library"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { self, nixpkgs, rust-overlay }: + let + systems = [ "aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux" ]; + forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f { + pkgs = import nixpkgs { + inherit system; + overlays = [ rust-overlay.overlays.default ]; + }; + }); + in + { + packages = forAllSystems ({ pkgs }: + let + rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust_toolchain.toml; + rustPlatform = pkgs.makeRustPlatform { + cargo = rustToolchain; + rustc = rustToolchain; + }; + in + { + default = rustPlatform.buildRustPackage { + pname = "libchat"; + version = "0.1.0"; + src = pkgs.lib.cleanSourceWith { + src = ./.; + filter = path: type: + let base = builtins.baseNameOf path; + in !(builtins.elem base [ "target" "nim-bindings" ".git" ".github" "tmp" ]); + }; + + cargoLock = { + lockFile = ./Cargo.lock; + outputHashes = { + "chat-proto-0.1.0" = "sha256-aCl80VOIkd/GK3gnmRuFoSAvPBfeE/FKCaNlLt5AbUU="; + }; + }; + + # perl: required by openssl-sys (transitive dep) + nativeBuildInputs = [ pkgs.perl pkgs.pkg-config pkgs.cmake ]; + doCheck = false; # tests require network access unavailable in nix sandbox + + postBuild = '' + cargo run --release --bin generate-libchat-headers --features headers + ''; + + installPhase = '' + runHook preInstall + mkdir -p $out/lib $out/include + + # Copy shared library + cp target/release/liblibchat.so $out/lib/ 2>/dev/null || true + cp target/release/liblibchat.dylib $out/lib/ 2>/dev/null || true + cp target/release/liblibchat.a $out/lib/ 2>/dev/null || true + + # Copy generated header + cp libchat.h $out/include/ + + runHook postInstall + ''; + + meta = with pkgs.lib; { + description = "Logos Chat cryptographic library (C FFI)"; + platforms = platforms.unix; + }; + }; + } + ); + + devShells = forAllSystems ({ pkgs }: + let + rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust_toolchain.toml; + in + { + default = pkgs.mkShell { + nativeBuildInputs = [ + rustToolchain + pkgs.pkg-config + pkgs.cmake + ]; + }; + } + ); + }; +}