Skip to main content

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