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}