Skip to main content

scp_platform/
traits.rs

1//! Platform abstraction traits for SCP.
2//!
3//! These four traits abstract device-specific capabilities behind Rust trait
4//! interfaces so that production implementations (Secure Enclave, Android
5//! Keystore) and testing implementations (in-memory) share the same API
6//! surface. See ADR-006 for the full platform adapter design.
7//!
8//! # Traits
9//!
10//! - [`KeyCustody`] — Cryptographic key management (generation, signing, ECDH, pseudonym derivation)
11//! - [`DeviceAttestation`] — Device-level attestation tokens
12//! - [`Push`] — Push notification registration and handling
13//! - [`Storage`] — Persistent key-value byte storage
14
15use serde::{Deserialize, Serialize};
16use zeroize::ZeroizeOnDrop;
17
18use crate::error::PlatformError;
19
20// ---------------------------------------------------------------------------
21// Supporting types
22// ---------------------------------------------------------------------------
23
24/// The type of cryptographic key managed by a [`KeyHandle`].
25///
26/// See ADR-006 for usage: Ed25519 keys are used for identity and signing,
27/// X25519 keys are used for key agreement (HPKE wrapping keys).
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
29pub enum KeyType {
30    /// Ed25519 signing key (identity keys, active signing keys, pseudonym keys).
31    Ed25519,
32    /// X25519 key agreement key (HPKE wrapping keys).
33    X25519,
34}
35
36/// Opaque handle to a cryptographic key managed by a [`KeyCustody`] implementation.
37///
38/// The handle is an integer identifier. Implementations map this to actual key
39/// material stored internally (e.g., in a `HashMap`, Secure Enclave slot, or
40/// Android Keystore alias). The raw private key never leaves the custody
41/// boundary.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
43pub struct KeyHandle(u64);
44
45impl KeyHandle {
46    /// Creates a new key handle from a raw identifier.
47    ///
48    /// This is intended for [`KeyCustody`] implementations that allocate
49    /// integer IDs for their managed keys.
50    #[must_use]
51    pub const fn new(id: u64) -> Self {
52        Self(id)
53    }
54
55    /// Returns the raw integer identifier for this handle.
56    #[must_use]
57    pub const fn id(&self) -> u64 {
58        self.0
59    }
60}
61
62/// A public key extracted from a [`KeyHandle`].
63///
64/// Contains the raw public key bytes — Ed25519 (32 bytes) or X25519 (32 bytes).
65/// The interpretation depends on the [`KeyType`] of the originating handle.
66#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
67pub struct PublicKey(Vec<u8>);
68
69impl PublicKey {
70    /// Creates a new public key from raw bytes.
71    #[must_use]
72    pub const fn new(bytes: Vec<u8>) -> Self {
73        Self(bytes)
74    }
75
76    /// Returns a reference to the raw public key bytes.
77    #[must_use]
78    pub fn as_bytes(&self) -> &[u8] {
79        &self.0
80    }
81
82    /// Consumes this value and returns the raw public key bytes.
83    #[must_use]
84    pub fn into_bytes(self) -> Vec<u8> {
85        self.0
86    }
87}
88
89/// An Ed25519 signature produced by [`KeyCustody::sign`].
90///
91/// Contains the raw 64-byte Ed25519 signature.
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93pub struct Signature(Vec<u8>);
94
95impl Signature {
96    /// Creates a new signature from raw bytes.
97    #[must_use]
98    pub const fn new(bytes: Vec<u8>) -> Self {
99        Self(bytes)
100    }
101
102    /// Returns a reference to the raw signature bytes.
103    #[must_use]
104    pub fn as_bytes(&self) -> &[u8] {
105        &self.0
106    }
107
108    /// Consumes this value and returns the raw signature bytes.
109    #[must_use]
110    pub fn into_bytes(self) -> Vec<u8> {
111        self.0
112    }
113}
114
115/// A 32-byte X25519 shared secret produced by [`KeyCustody::dh_agree`].
116///
117/// This type intentionally does **not** implement [`Clone`] or [`Serialize`] to
118/// prevent accidental duplication or serialization of secret material. Callers
119/// should consume the secret and then let it be dropped.
120///
121/// **Zeroization:** The inner bytes are automatically zeroed on drop via
122/// [`ZeroizeOnDrop`], ensuring key material is cleared from memory.
123#[derive(Debug, PartialEq, Eq, ZeroizeOnDrop)]
124pub struct SharedSecret([u8; 32]);
125
126impl SharedSecret {
127    /// Creates a new shared secret from a 32-byte array.
128    #[must_use]
129    pub const fn new(bytes: [u8; 32]) -> Self {
130        Self(bytes)
131    }
132
133    /// Returns a reference to the raw shared secret bytes.
134    #[must_use]
135    pub const fn as_bytes(&self) -> &[u8; 32] {
136        &self.0
137    }
138}
139
140/// A deterministic pseudonym keypair derived from an identity key and a context
141/// ID via [`KeyCustody::derive_pseudonym`].
142///
143/// The derivation algorithm is specified in ADR-006:
144///   1. `seed = HMAC-SHA256(identity_key_material, context_id || "scp-pseudonym")`
145///   2. `pseudonym_keypair = Ed25519_keygen(seed[0..32])`
146///
147/// The returned keypair is always software-managed regardless of whether the
148/// source identity key is hardware-backed.
149#[derive(Debug, Clone)]
150pub struct PseudonymKeypair {
151    /// The public key of the derived pseudonym.
152    pub public_key: PublicKey,
153    /// A handle to the derived pseudonym's signing key, managed by the
154    /// [`KeyCustody`] implementation.
155    pub key_handle: KeyHandle,
156}
157
158/// The custody type for a given key, indicating where the key material is
159/// stored and how it is protected.
160///
161/// See ADR-006 for the custody model: production adapters use hardware-backed
162/// custody, while the testing adapter uses [`CustodyType::InMemory`].
163#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
164pub enum CustodyType {
165    /// Key material is stored in memory only (testing adapter).
166    InMemory,
167    /// Key material is protected by a hardware security module (Secure Enclave,
168    /// Android Keystore, TPM).
169    Hardware,
170    /// Key material is stored in software (e.g., encrypted file on disk) but
171    /// not in a hardware security module.
172    Software,
173}
174
175/// A device attestation token produced by [`DeviceAttestation::attest`].
176///
177/// The token format is platform-specific (e.g., Apple App Attest, Android
178/// `SafetyNet`). The testing adapter returns a synthetic token. See ADR-006.
179#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
180pub struct DeviceAttestationToken(Vec<u8>);
181
182impl DeviceAttestationToken {
183    /// Creates a new attestation token from raw bytes.
184    #[must_use]
185    pub const fn new(bytes: Vec<u8>) -> Self {
186        Self(bytes)
187    }
188
189    /// Returns a reference to the raw token bytes.
190    #[must_use]
191    pub fn as_bytes(&self) -> &[u8] {
192        &self.0
193    }
194
195    /// Consumes this value and returns the raw token bytes.
196    #[must_use]
197    pub fn into_bytes(self) -> Vec<u8> {
198        self.0
199    }
200}
201
202/// A push notification token returned by [`Push::register`].
203///
204/// The token format is platform-specific (e.g., APNs device token, FCM
205/// registration token). The testing adapter returns a synthetic UUID. See ADR-006.
206#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
207pub struct PushToken(Vec<u8>);
208
209impl PushToken {
210    /// Creates a new push token from raw bytes.
211    #[must_use]
212    pub const fn new(bytes: Vec<u8>) -> Self {
213        Self(bytes)
214    }
215
216    /// Returns a reference to the raw token bytes.
217    #[must_use]
218    pub fn as_bytes(&self) -> &[u8] {
219        &self.0
220    }
221
222    /// Consumes this value and returns the raw token bytes.
223    #[must_use]
224    pub fn into_bytes(self) -> Vec<u8> {
225        self.0
226    }
227}
228
229/// A wake signal produced by [`Push::handle_notification`].
230///
231/// Indicates that the application should wake up and process pending messages.
232/// The payload carries transport-specific context (e.g., which context has new
233/// messages). See ADR-006.
234#[derive(Debug, Clone, PartialEq, Eq)]
235pub struct WakeSignal {
236    /// The raw notification payload that triggered this wake signal.
237    pub payload: Vec<u8>,
238}
239
240impl WakeSignal {
241    /// Creates a new wake signal from a notification payload.
242    #[must_use]
243    pub const fn new(payload: Vec<u8>) -> Self {
244        Self { payload }
245    }
246}
247
248// ---------------------------------------------------------------------------
249// Trait definitions
250// ---------------------------------------------------------------------------
251
252/// Cryptographic key management trait.
253///
254/// Abstracts key generation, signing, key agreement, and pseudonym derivation
255/// behind a uniform interface. Production implementations delegate to hardware
256/// security modules (Secure Enclave on iOS, Android Keystore on Android). The
257/// testing implementation ([`InMemoryKeyCustody`](ADR-006)) stores keys in a
258/// `HashMap`.
259///
260/// All methods that perform I/O or hardware interaction are `async`. The
261/// [`custody_type`](KeyCustody::custody_type) method is synchronous because it
262/// only inspects local state.
263///
264/// See ADR-006 for the full design rationale.
265pub trait KeyCustody: Send + Sync {
266    /// Generate a new keypair of the specified type.
267    ///
268    /// Ed25519 keys may be hardware-backed (Secure Enclave, Keystore).
269    /// X25519 wrapping keys are always software-managed but routed through
270    /// `KeyCustody` for API consistency.
271    ///
272    /// Returns an opaque [`KeyHandle`] that references the generated key.
273    fn generate_keypair(
274        &self,
275        key_type: KeyType,
276    ) -> impl Future<Output = Result<KeyHandle, PlatformError>> + Send;
277
278    /// Sign data with an Ed25519 key.
279    ///
280    /// # Errors
281    ///
282    /// Returns [`PlatformError::KeyNotFound`] if the handle is invalid.
283    /// Returns [`PlatformError::WrongKeyType`] if the handle refers to an
284    /// X25519 key.
285    fn sign(
286        &self,
287        key: &KeyHandle,
288        data: &[u8],
289    ) -> impl Future<Output = Result<Signature, PlatformError>> + Send;
290
291    /// Return the public key for a handle.
292    ///
293    /// Works for both Ed25519 and X25519 key handles.
294    ///
295    /// # Errors
296    ///
297    /// Returns [`PlatformError::KeyNotFound`] if the handle is invalid.
298    fn public_key(
299        &self,
300        key: &KeyHandle,
301    ) -> impl Future<Output = Result<PublicKey, PlatformError>> + Send;
302
303    /// Destroy key material associated with a handle.
304    ///
305    /// After this call, all subsequent operations with the same handle will
306    /// return [`PlatformError::KeyNotFound`].
307    ///
308    /// # Errors
309    ///
310    /// Returns [`PlatformError::KeyNotFound`] if the handle is already invalid.
311    fn destroy_key(
312        &self,
313        key: &KeyHandle,
314    ) -> impl Future<Output = Result<(), PlatformError>> + Send;
315
316    /// Perform X25519 Diffie-Hellman key agreement.
317    ///
318    /// Returns the 32-byte shared secret. The private key never leaves the
319    /// custody boundary — the scalar multiplication happens inside the adapter.
320    ///
321    /// # Errors
322    ///
323    /// Returns [`PlatformError::KeyNotFound`] if the handle is invalid.
324    /// Returns [`PlatformError::WrongKeyType`] if the handle refers to an
325    /// Ed25519 key.
326    fn dh_agree(
327        &self,
328        key: &KeyHandle,
329        peer_public: &[u8; 32],
330    ) -> impl Future<Output = Result<SharedSecret, PlatformError>> + Send;
331
332    /// Derive a deterministic, context-scoped pseudonym keypair (v1, non-rotatable).
333    ///
334    /// Algorithm (all implementations MUST produce identical output):
335    ///   1. `seed = HMAC-SHA256(identity_key_material, context_id || "scp-pseudonym")`
336    ///   2. `pseudonym_keypair = Ed25519_keygen(seed[0..32])`
337    ///
338    /// For hardware-backed keys: the HMAC is computed inside the HSM using an
339    /// associated symmetric key derived during [`generate_keypair`](KeyCustody::generate_keypair).
340    /// For software keys: the HMAC uses the raw Ed25519 public key bytes (ADR-027 amendment).
341    ///
342    /// The returned [`PseudonymKeypair`] is always software-managed (derived
343    /// output).
344    ///
345    /// For contexts that support pseudonym rotation (BLACK-001 mitigation),
346    /// use [`derive_rotatable_pseudonym`](KeyCustody::derive_rotatable_pseudonym) instead.
347    ///
348    /// # Errors
349    ///
350    /// Returns [`PlatformError::KeyNotFound`] if the handle is invalid.
351    /// Returns [`PlatformError::WrongKeyType`] if the handle refers to an
352    /// X25519 key.
353    fn derive_pseudonym(
354        &self,
355        key: &KeyHandle,
356        context_id: &[u8],
357    ) -> impl Future<Output = Result<PseudonymKeypair, PlatformError>> + Send;
358
359    /// Derive a rotatable, epoch-scoped pseudonym keypair (v2).
360    ///
361    /// Mitigates relay-side pseudonym correlation (BLACK-001) by including a
362    /// rotation epoch in the HMAC derivation, producing a different pseudonym
363    /// for each epoch within the same context.
364    ///
365    /// Algorithm (all implementations MUST produce identical output):
366    ///   1. `seed = HMAC-SHA256(identity_key_material, context_id || epoch_BE || "scp-pseudonym-v2")`
367    ///   2. `pseudonym_keypair = Ed25519_keygen(seed[0..32])`
368    ///
369    /// where `epoch_BE` is the `pseudonym_epoch` as an 8-byte big-endian u64.
370    ///
371    /// The domain separator `"scp-pseudonym-v2"` is intentionally different from
372    /// the v1 separator `"scp-pseudonym"` so that epoch 0 in v2 produces a
373    /// different pseudonym than the v1 derivation. This prevents accidental
374    /// domain confusion.
375    ///
376    /// # Errors
377    ///
378    /// Returns [`PlatformError::KeyNotFound`] if the handle is invalid.
379    /// Returns [`PlatformError::WrongKeyType`] if the handle refers to an
380    /// X25519 key.
381    fn derive_rotatable_pseudonym(
382        &self,
383        key: &KeyHandle,
384        context_id: &[u8],
385        pseudonym_epoch: u64,
386    ) -> impl Future<Output = Result<PseudonymKeypair, PlatformError>> + Send;
387
388    /// Returns the custody type for a given key handle.
389    ///
390    /// This is a synchronous query against local state — no I/O is required.
391    fn custody_type(&self, key: &KeyHandle) -> CustodyType;
392}
393
394/// Device attestation trait.
395///
396/// Abstracts platform-specific device attestation (Apple App Attest, Android
397/// `SafetyNet` / Play Integrity). The testing implementation returns synthetic
398/// attestation tokens that always verify. See ADR-006.
399pub trait DeviceAttestation: Send + Sync {
400    /// Generate a device attestation token.
401    ///
402    /// # Errors
403    ///
404    /// Returns [`PlatformError::AttestationError`] if the platform attestation
405    /// service is unavailable.
406    fn attest(&self) -> impl Future<Output = Result<DeviceAttestationToken, PlatformError>> + Send;
407
408    /// Verify a device attestation token.
409    ///
410    /// Returns `true` if the token is valid, `false` otherwise.
411    ///
412    /// # Errors
413    ///
414    /// Returns [`PlatformError::AttestationError`] if verification cannot be
415    /// completed (e.g., network error contacting the attestation service).
416    fn verify(
417        &self,
418        token: &DeviceAttestationToken,
419    ) -> impl Future<Output = Result<bool, PlatformError>> + Send;
420}
421
422/// Push notification trait.
423///
424/// Abstracts platform-specific push notification registration and handling
425/// (APNs, FCM). The testing implementation returns synthetic tokens and passes
426/// payloads through as wake signals. See ADR-006.
427pub trait Push: Send + Sync {
428    /// Register for push notifications and return a platform-specific token.
429    ///
430    /// # Errors
431    ///
432    /// Returns [`PlatformError::PushError`] if registration fails.
433    fn register(&self) -> impl Future<Output = Result<PushToken, PlatformError>> + Send;
434
435    /// Handle an incoming push notification payload and produce a wake signal.
436    ///
437    /// # Errors
438    ///
439    /// Returns [`PlatformError::PushError`] if the payload cannot be processed.
440    fn handle_notification(
441        &self,
442        payload: &[u8],
443    ) -> impl Future<Output = Result<WakeSignal, PlatformError>> + Send;
444}
445
446/// Persistent key-value byte storage trait.
447///
448/// Abstracts platform-specific secure storage (Keychain, encrypted `SQLite`,
449/// browser `IndexedDB`). Keys are UTF-8 strings; values are opaque byte
450/// slices. The testing implementation stores data in an in-memory `HashMap`.
451/// See ADR-006.
452pub trait Storage: Send + Sync {
453    /// Store a byte slice under the given key.
454    ///
455    /// Overwrites any existing value for the same key.
456    ///
457    /// # Errors
458    ///
459    /// Returns [`PlatformError::StorageError`] if the write fails.
460    fn store(
461        &self,
462        key: &str,
463        data: &[u8],
464    ) -> impl Future<Output = Result<(), PlatformError>> + Send;
465
466    /// Retrieve the byte slice stored under the given key.
467    ///
468    /// Returns `None` if the key does not exist.
469    ///
470    /// # Errors
471    ///
472    /// Returns [`PlatformError::StorageError`] if the read fails.
473    fn retrieve(
474        &self,
475        key: &str,
476    ) -> impl Future<Output = Result<Option<Vec<u8>>, PlatformError>> + Send;
477
478    /// Delete the value stored under the given key.
479    ///
480    /// No-op if the key does not exist.
481    ///
482    /// # Errors
483    ///
484    /// Returns [`PlatformError::StorageError`] if the delete fails.
485    fn delete(&self, key: &str) -> impl Future<Output = Result<(), PlatformError>> + Send;
486
487    /// List all keys matching the given prefix in lexicographic order.
488    ///
489    /// Useful for `KeyPackage` buffer management and event log range queries.
490    ///
491    /// # Errors
492    ///
493    /// Returns [`PlatformError::StorageError`] if the operation fails.
494    fn list_keys(
495        &self,
496        prefix: &str,
497    ) -> impl Future<Output = Result<Vec<String>, PlatformError>> + Send;
498
499    /// Delete all keys matching the given prefix.
500    ///
501    /// Returns the number of keys deleted. Used for context cleanup. See
502    /// ADR-006 acceptance criterion 4 (`InMemoryStorage`).
503    ///
504    /// # Errors
505    ///
506    /// Returns [`PlatformError::StorageError`] if the operation fails.
507    fn delete_prefix(
508        &self,
509        prefix: &str,
510    ) -> impl Future<Output = Result<u64, PlatformError>> + Send;
511
512    /// Check whether a key exists without reading its value.
513    ///
514    /// Used for UCAN nonce replay prevention. See ADR-006 acceptance
515    /// criterion 4 (`InMemoryStorage`).
516    ///
517    /// # Errors
518    ///
519    /// Returns [`PlatformError::StorageError`] if the operation fails.
520    fn exists(&self, key: &str) -> impl Future<Output = Result<bool, PlatformError>> + Send;
521}
522
523// ---------------------------------------------------------------------------
524// Arc<T> blanket impl for Storage
525// ---------------------------------------------------------------------------
526
527/// Blanket implementation of [`Storage`] for `Arc<T>` where `T: Storage`.
528///
529/// Enables sharing a single storage backend across multiple owners (e.g.,
530/// `ProtocolStore`, identity layer, and FFI bridge) via `Arc`. Delegates all
531/// operations to the inner `T` via `Deref`.
532///
533/// This is essential for `ProtocolStore<Arc<S>>` to work when the storage
534/// backend is shared via `Arc` (e.g., the FFI bridge's global
535/// `STORAGE_PROVIDER`). See issue #329.
536#[allow(clippy::manual_async_fn)]
537impl<T: Storage> Storage for std::sync::Arc<T> {
538    fn store(
539        &self,
540        key: &str,
541        data: &[u8],
542    ) -> impl Future<Output = Result<(), PlatformError>> + Send {
543        (**self).store(key, data)
544    }
545
546    fn retrieve(
547        &self,
548        key: &str,
549    ) -> impl Future<Output = Result<Option<Vec<u8>>, PlatformError>> + Send {
550        (**self).retrieve(key)
551    }
552
553    fn delete(&self, key: &str) -> impl Future<Output = Result<(), PlatformError>> + Send {
554        (**self).delete(key)
555    }
556
557    fn list_keys(
558        &self,
559        prefix: &str,
560    ) -> impl Future<Output = Result<Vec<String>, PlatformError>> + Send {
561        (**self).list_keys(prefix)
562    }
563
564    fn delete_prefix(
565        &self,
566        prefix: &str,
567    ) -> impl Future<Output = Result<u64, PlatformError>> + Send {
568        (**self).delete_prefix(prefix)
569    }
570
571    fn exists(&self, key: &str) -> impl Future<Output = Result<bool, PlatformError>> + Send {
572        (**self).exists(key)
573    }
574}