ap_client/traits.rs
1use ap_noise::MultiDeviceTransport;
2use ap_proxy_protocol::{IdentityFingerprint, IdentityKeyPair};
3use async_trait::async_trait;
4
5use crate::error::ClientError;
6
7/// A cached connection record containing all connection data.
8#[derive(Debug, Clone)]
9pub struct ConnectionInfo {
10 pub fingerprint: IdentityFingerprint,
11 pub name: Option<String>,
12 pub cached_at: u64,
13 pub last_connected_at: u64,
14 pub transport_state: Option<MultiDeviceTransport>,
15}
16
17/// Lightweight update for an existing connection (no full read needed).
18#[derive(Debug, Clone, Copy)]
19pub struct ConnectionUpdate {
20 pub fingerprint: IdentityFingerprint,
21 pub last_connected_at: u64,
22}
23
24/// Trait for connection cache storage implementations.
25///
26/// Provides an abstraction for storing and retrieving approved remote connections.
27/// Implementations must be thread-safe for use in async contexts.
28#[async_trait]
29pub trait ConnectionStore: Send + Sync {
30 /// Get a connection by fingerprint, returning `None` if not found.
31 async fn get(&self, fingerprint: &IdentityFingerprint) -> Option<ConnectionInfo>;
32
33 /// Save a connection (insert or replace).
34 async fn save(&mut self, connection: ConnectionInfo) -> Result<(), ClientError>;
35
36 /// Update only the `last_connected_at` timestamp for an existing connection.
37 async fn update(&mut self, update: ConnectionUpdate) -> Result<(), ClientError>;
38
39 /// List all cached connections.
40 async fn list(&self) -> Vec<ConnectionInfo>;
41}
42
43/// Provides a cryptographic identity for the current client.
44///
45/// For the device group, this should be one shared identity, for the single-device, a unique identity.
46/// This should be generated on first run and stored persistently, in secure storage where possible.
47#[async_trait]
48pub trait IdentityProvider: Send + Sync {
49 /// Get the identity keypair
50 async fn identity(&self) -> IdentityKeyPair;
51
52 /// Get the fingerprint of this identity
53 async fn fingerprint(&self) -> IdentityFingerprint {
54 self.identity().await.identity().fingerprint()
55 }
56}
57
58/// An [`IdentityProvider`] that generates a random ephemeral identity on creation.
59///
60/// Useful for tests, examples, and consumers that don't need persistent identity.
61/// The keypair lives only in memory and is lost when the provider is dropped.
62///
63/// ```
64/// use ap_client::MemoryIdentityProvider;
65///
66/// let identity = MemoryIdentityProvider::new();
67/// ```
68///
69/// To wrap an existing keypair:
70///
71/// ```
72/// use ap_client::MemoryIdentityProvider;
73/// use ap_proxy_protocol::IdentityKeyPair;
74///
75/// let keypair = IdentityKeyPair::generate();
76/// let identity = MemoryIdentityProvider::from_keypair(keypair);
77/// ```
78#[derive(Clone)]
79pub struct MemoryIdentityProvider {
80 keypair: IdentityKeyPair,
81}
82
83impl MemoryIdentityProvider {
84 /// Generate a new random ephemeral identity.
85 pub fn new() -> Self {
86 Self {
87 keypair: IdentityKeyPair::generate(),
88 }
89 }
90
91 /// Wrap an existing keypair as an ephemeral identity provider.
92 pub fn from_keypair(keypair: IdentityKeyPair) -> Self {
93 Self { keypair }
94 }
95}
96
97impl Default for MemoryIdentityProvider {
98 fn default() -> Self {
99 Self::new()
100 }
101}
102
103#[async_trait]
104impl IdentityProvider for MemoryIdentityProvider {
105 async fn identity(&self) -> IdentityKeyPair {
106 self.keypair.clone()
107 }
108}
109
110/// How a new connection was established between devices.
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub enum AuditConnectionType {
113 /// Paired using a 9-character rendezvous code exchanged out-of-band.
114 /// Requires explicit fingerprint verification before the connection is trusted.
115 Rendezvous,
116 /// Paired using a pre-shared key (PSK) token.
117 /// Trust is established through the shared secret — no fingerprint verification needed.
118 Psk,
119}
120
121/// Describes which credential fields were included in an approved response.
122///
123/// Contains only presence flags, never actual credential values.
124/// Useful for audit trails that need to record *what kind* of data was shared
125/// without logging sensitive material.
126#[derive(Debug, Clone, Default)]
127pub struct CredentialFieldSet {
128 pub has_username: bool,
129 pub has_password: bool,
130 pub has_totp: bool,
131 pub has_uri: bool,
132 pub has_notes: bool,
133}
134
135/// Audit events emitted by the [`UserClient`] (trusted device) for security-relevant actions.
136///
137/// Each variant represents a discrete, auditable action in the access protocol.
138/// Implementations of [`AuditLog`] receive these events and can persist them to files,
139/// databases, or external services.
140///
141/// ## Field conventions
142///
143/// - `remote_identity` — the [`IdentityFingerprint`] of the remote (untrusted) device.
144/// This is a stable 32-byte identifier derived from the device's persistent public key.
145/// - `remote_name` — optional human-friendly label assigned by the user when pairing
146/// (e.g., "Work Laptop"). Only available on connection events.
147/// - `request_id` — unique per-request correlation token generated by the remote client.
148/// Use this to correlate `CredentialRequested` → `CredentialApproved`/`CredentialDenied`.
149/// - `query` — the [`CredentialQuery`](crate::CredentialQuery) that triggered the lookup.
150/// - `domain` — the credential's domain (from the matched vault item), if available.
151///
152/// This enum is `#[non_exhaustive]` — new variants may be added in future versions.
153/// Implementations should include a `_ => {}` catch-all arm when matching.
154#[derive(Debug, Clone)]
155#[non_exhaustive]
156pub enum AuditEvent<'a> {
157 /// A new remote device completed the Noise handshake and was accepted as trusted.
158 ///
159 /// Emitted once per new pairing, after the session is cached. For rendezvous connections,
160 /// this fires only after the user has explicitly approved the fingerprint verification.
161 /// For PSK connections, trust is implicit via the shared secret.
162 ConnectionEstablished {
163 remote_identity: &'a IdentityFingerprint,
164 remote_name: Option<&'a str>,
165 connection_type: AuditConnectionType,
166 },
167
168 /// A previously-paired device reconnected and refreshed its transport keys.
169 ///
170 /// This is a routine reconnection of an already-trusted device — no user approval
171 /// is needed. The Noise handshake runs again to derive fresh encryption keys,
172 /// but the device was already verified during the original pairing.
173 SessionRefreshed {
174 remote_identity: &'a IdentityFingerprint,
175 },
176
177 /// A new connection attempt was rejected during fingerprint verification.
178 ///
179 /// The user was shown the handshake fingerprint and chose to reject it,
180 /// meaning the remote device was not added to the trusted session cache.
181 /// Only applies to rendezvous connections (PSK connections skip verification).
182 ConnectionRejected {
183 remote_identity: &'a IdentityFingerprint,
184 },
185
186 /// A remote device sent a request for credentials.
187 ///
188 /// Emitted when the encrypted request is received and decrypted. At this point
189 /// the request is pending user approval — no credential data has been shared yet.
190 CredentialRequested {
191 query: &'a crate::CredentialQuery,
192 remote_identity: &'a IdentityFingerprint,
193 request_id: &'a str,
194 },
195
196 /// A credential request was approved and the credential was sent to the remote device.
197 ///
198 /// The `fields` indicate which credential fields were included (e.g., username,
199 /// password, TOTP) without revealing the actual values.
200 CredentialApproved {
201 query: &'a crate::CredentialQuery,
202 domain: Option<&'a str>,
203 remote_identity: &'a IdentityFingerprint,
204 request_id: &'a str,
205 credential_id: Option<&'a str>,
206 fields: CredentialFieldSet,
207 },
208
209 /// A credential request was denied by the user.
210 ///
211 /// No credential data was sent to the remote device.
212 CredentialDenied {
213 query: &'a crate::CredentialQuery,
214 domain: Option<&'a str>,
215 remote_identity: &'a IdentityFingerprint,
216 request_id: &'a str,
217 credential_id: Option<&'a str>,
218 },
219}
220
221/// Persistent audit logging for security-relevant events on the UserClient.
222///
223/// Implementations may write to files, databases, or external services.
224/// All methods receive `&self` (interior mutability is the implementor's
225/// responsibility). Implementations should handle errors internally
226/// (e.g., log a warning via `tracing`). Timestamps are the implementor's
227/// responsibility.
228#[async_trait]
229pub trait AuditLog: Send + Sync {
230 /// Write an audit event
231 async fn write(&self, event: AuditEvent<'_>);
232}
233
234/// No-op audit logger that discards all events.
235/// Used as the default when no audit logging is configured.
236pub struct NoOpAuditLog;
237
238#[async_trait]
239impl AuditLog for NoOpAuditLog {
240 async fn write(&self, _event: AuditEvent<'_>) {}
241}