feat: prepare for logos module consumption

- add flake.nix for Nix builds (cdylib + header generation)
- switch crate-type from dylib to cdylib for C FFI
- replace placeholder delivery addresses with real derived addresses
- switch FFI inputs from owned to borrowed
- expose local_delivery_address() so caller knows where to subscribe
This commit is contained in:
pablo 2026-03-27 10:11:57 +01:00
parent 8cddd9ddcf
commit ecd1be3b9e
No known key found for this signature in database
GPG Key ID: 78F35FCC60FDC63A
9 changed files with 154 additions and 27 deletions

4
.gitignore vendored
View File

@ -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

View File

@ -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"

View File

@ -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<ContextHandle> {
// 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<ContextHandle> {
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 {

View File

@ -0,0 +1,3 @@
fn main() -> std::io::Result<()> {
libchat::generate_headers()
}

View File

@ -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,

View File

@ -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(),

View File

@ -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<Identity>,
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()));

View File

@ -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 {

95
flake.nix Normal file
View File

@ -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
];
};
}
);
};
}