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