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