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