Add DelegateSigner

This commit is contained in:
Jazz Turner-Baggs 2026-06-19 01:58:11 -07:00
parent d99c09b57d
commit c5ee262e33
No known key found for this signature in database
6 changed files with 206 additions and 13 deletions

View File

@ -31,4 +31,4 @@ pub use service_traits::{DeliveryService, RegistrationService, WakeupService};
pub use shared_traits::{IdentId, IdentIdRef, IdentityProvider};
pub use storage::ConversationKind;
pub use types::AddressedEnvelope;
pub use utils::hex_trunc;
pub use utils::{hex_trunc, trunc};

View File

@ -71,3 +71,12 @@ pub fn hex_trunc(data: &[u8]) -> String {
)
}
}
#[allow(unused)]
pub fn trunc(data: &str) -> String {
if data.len() <= 8 {
data.to_string()
} else {
format!("{}..{}", &data[..4], &data[data.len() - 4..])
}
}

View File

@ -11,10 +11,13 @@ crate-type = ["rlib"]
chat-sqlite = { workspace = true }
components = { workspace = true }
crossbeam-channel = { workspace = true }
crypto = { workspace = true }
libchat = { workspace = true }
logos-account = { workspace = true, features = ["dev"]}
shared-traits = { workspace = true }
# External dependencies (sorted)
hex = "0.4.3"
parking_lot = "0.12"
thiserror = "2"
tracing = "0.1"

View File

@ -7,13 +7,13 @@ use libchat::{
ChatError, ChatStorage, ConversationId, ConvoOutcome, Core, DeliveryService, IdentId,
IdentIdRef, InboxOutcome, Introduction, PayloadOutcome, RegistrationService, StorageConfig,
};
use logos_account::TestLogosAccount;
use parking_lot::Mutex;
use crate::delegate::{self, DelegateCredential, DelegateSigner};
use crate::errors::ClientError;
use crate::event::Event;
type ClientCore<T, R> = Core<(TestLogosAccount, T, R, ThreadedWakeupService, ChatStorage)>;
type ClientCore<T, R> = Core<(DelegateSigner, T, R, ThreadedWakeupService, ChatStorage)>;
type AccountAddressRef<'a> = &'a str;
type LocalSignerId = IdentId;
@ -53,11 +53,12 @@ impl<T: Transport> ChatClient<T, EphemeralRegistry> {
/// Create an in-memory, ephemeral client. Identity is lost on drop.
pub fn new(name: impl Into<String>, mut transport: T) -> (Self, Receiver<Event>) {
let inbound = transport.inbound();
let ident = TestLogosAccount::new(name);
let delegate = DelegateSigner::random();
let (wakeup_tx, wakeup_rx) = crossbeam_channel::unbounded();
let wakeup_service = ThreadedWakeupService::new(wakeup_tx);
let core = Core::new_with_name(
ident,
delegate,
transport,
EphemeralRegistry::new(),
wakeup_service,
@ -78,11 +79,11 @@ impl<T: Transport> ChatClient<T, EphemeralRegistry> {
) -> Result<(Self, Receiver<Event>), ClientError> {
let store = ChatStorage::new(config).map_err(ChatError::from)?;
let inbound = transport.inbound();
let ident = TestLogosAccount::new(name);
let delegate = DelegateSigner::random();
let (wakeup_tx, wakeup_rx) = crossbeam_channel::unbounded();
let wakeup_service = ThreadedWakeupService::new(wakeup_tx);
let core = Core::new_from_store(
ident,
delegate,
transport,
EphemeralRegistry::new(),
wakeup_service,
@ -96,7 +97,7 @@ impl<T: Transport> ChatClient<T, EphemeralRegistry> {
impl<T, R> ChatClient<T, R>
where
T: DeliveryService + Send + 'static,
T: Transport + Send + 'static,
R: RegistrationService + Send + 'static,
{
/// Open or create a persistent client with a caller-supplied registration
@ -118,14 +119,35 @@ where
{
let store = ChatStorage::new(config).map_err(ChatError::from)?;
let inbound = transport.inbound();
let ident = TestLogosAccount::new(name);
let delegate = DelegateSigner::random();
let (wakeup_tx, wakeup_rx) = crossbeam_channel::unbounded();
let wakeup_service = ThreadedWakeupService::new(wakeup_tx);
let mut core = Core::new_from_store(ident, transport, registry, wakeup_service, store)?;
let mut core = Core::new_from_store(delegate, transport, registry, wakeup_service, store)?;
core.register_keypackage()?;
Ok(Self::spawn(core, inbound, wakeup_rx))
}
/// Create a client with ephemeral storage with the provided Transport and RegistrationService.
pub fn new_ephemeral(
delegate: DelegateSigner,
mut transport: T,
reg: R,
) -> (Self, Receiver<Event>) {
let inbound = transport.inbound();
let (wakeup_tx, wakeup_rx) = crossbeam_channel::unbounded();
let wakeup_service = ThreadedWakeupService::new(wakeup_tx);
let core = Core::new_with_name(
delegate,
transport,
reg,
wakeup_service,
ChatStorage::in_memory(),
)
.unwrap();
Self::spawn(core, inbound, wakeup_rx)
}
fn spawn(
core: ClientCore<T, R>,
inbound: Receiver<Vec<u8>>,
@ -290,9 +312,15 @@ fn events_from_inbound(result: PayloadOutcome) -> Vec<Event> {
fn convo_events(outcome: ConvoOutcome) -> Vec<Event> {
let ConvoOutcome { convo_id, content } = outcome;
content
.map(|c| Event::MessageReceived {
convo_id: Arc::from(convo_id),
content: c.bytes,
.map(|c| {
let data = hex::decode(c.encoded_credential).unwrap();
let delegate_cred = DelegateCredential::from(data);
println!("{:?}", delegate_cred);
Event::MessageReceived {
convo_id: Arc::from(convo_id),
content: c.bytes,
}
})
.into_iter()
.collect()

View File

@ -0,0 +1,151 @@
use crypto::{Ed25519SigningKey, Ed25519VerifyingKey};
use libchat::{IdentId, IdentityProvider};
use crate::ClientError;
type AccountAddr = String;
pub struct DelegateSigner {
signing_key: Ed25519SigningKey,
verifying_key: Ed25519VerifyingKey,
identifier: IdentId,
account_addr: Option<AccountAddr>,
}
impl DelegateSigner {
pub fn random() -> Self {
let signing_key = Ed25519SigningKey::generate();
let verifying_key = signing_key.verifying_key();
let identifier = DelegateCredential::unassociated(&verifying_key).into();
Self {
signing_key,
verifying_key,
identifier,
account_addr: None,
}
}
pub fn associate(&mut self, account_addr: AccountAddr) {
self.identifier =
DelegateCredential::associated(&self.verifying_key, account_addr.as_str()).into();
self.account_addr = Some(account_addr);
}
pub fn account_addr(&self) -> Option<&str> {
self.account_addr.as_deref()
}
}
impl IdentityProvider for DelegateSigner {
fn id(&self) -> libchat::IdentIdRef<'_> {
&self.identifier
}
fn display_name(&self) -> String {
trunc(self.identifier.as_str())
}
fn sign(&self, payload: &[u8]) -> crypto::Ed25519Signature {
self.signing_key.sign(payload)
}
fn public_key(&self) -> &Ed25519VerifyingKey {
&self.verifying_key
}
}
/// Represents the senders information for received frames.
#[derive(Debug)]
pub struct DelegateCredential {
delegate_id: Ed25519VerifyingKey,
account_addr: Option<AccountAddr>,
}
impl DelegateCredential {
const TAG_DELEGATE_ID: u8 = 0x01;
const TAG_ACCOUNT_ADDR: u8 = 0x02;
pub fn unassociated(delegate: &Ed25519VerifyingKey) -> Self {
Self {
delegate_id: delegate.clone(),
account_addr: None,
}
}
pub fn associated(delegate: &Ed25519VerifyingKey, account: &str) -> Self {
Self {
delegate_id: delegate.clone(),
account_addr: Some(account.to_string()),
}
}
pub fn to_vec(self) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&[0x23, 0x23]);
let key_bytes = self.delegate_id.as_ref();
data.extend_from_slice(&[Self::TAG_DELEGATE_ID, key_bytes.len() as u8]);
data.extend_from_slice(key_bytes);
if let Some(addr) = self.account_addr {
let addr_bytes = addr.as_bytes();
data.extend_from_slice(&[Self::TAG_ACCOUNT_ADDR, addr_bytes.len() as u8]);
data.extend_from_slice(addr_bytes);
}
data
}
}
impl From<DelegateCredential> for Vec<u8> {
fn from(value: DelegateCredential) -> Self {
value.to_vec()
}
}
impl From<Vec<u8>> for DelegateCredential {
fn from(value: Vec<u8>) -> Self {
assert_eq!(&value[..2], &[0x23, 0x23], "invalid magic bytes");
let mut delegate_id = None;
let mut account_addr = None;
let mut i = 2;
while i + 2 <= value.len() {
let tag = value[i];
let len = value[i + 1] as usize;
i += 2;
let v = &value[i..i + len];
i += len;
match tag {
DelegateCredential::TAG_DELEGATE_ID => {
let bytes: &[u8; 32] = v.try_into().expect("invalid delegate_id length");
delegate_id = Some(
Ed25519VerifyingKey::from_bytes(bytes).expect("invalid verifying key"),
);
}
DelegateCredential::TAG_ACCOUNT_ADDR => {
account_addr =
Some(String::from_utf8(v.to_vec()).expect("invalid account_addr utf8"));
}
_ => {}
}
}
Self {
delegate_id: delegate_id.expect("missing delegate_id TLV field"),
account_addr,
}
}
}
impl From<DelegateCredential> for IdentId {
fn from(value: DelegateCredential) -> Self {
IdentId::new(hex::encode(value.to_vec()))
}
}
impl TryFrom<IdentId> for DelegateCredential {
type Error = ClientError;
fn try_from(value: IdentId) -> Result<Self, Self::Error> {
Ok(hex::decode(value.as_str())
.map_err(|e| ClientError::BadlyFormedCredential)?
.into())
}
}

View File

@ -4,4 +4,6 @@ use libchat::ChatError;
pub enum ClientError {
#[error(transparent)]
Chat(#[from] ChatError),
#[error("received credential could not be parsed")]
BadlyFormedCredential,
}