Skip to main content

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}