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