auths_crypto/provider.rs
1//! Pluggable cryptographic abstraction for Ed25519 operations.
2//!
3//! Defines the [`CryptoProvider`] trait for Ed25519 verification, signing, and
4//! key generation — enabling `ring` on native targets and `WebCrypto` on WASM.
5
6use async_trait::async_trait;
7use zeroize::{Zeroize, ZeroizeOnDrop};
8
9/// Error type for cryptographic operations.
10///
11/// Usage:
12/// ```ignore
13/// match result {
14/// Err(CryptoError::InvalidSignature) => { /* signature did not verify */ }
15/// Err(CryptoError::UnsupportedTarget) => { /* not available on this platform */ }
16/// Err(CryptoError::OperationFailed(msg)) => { /* backend error */ }
17/// Ok(()) => { /* success */ }
18/// }
19/// ```
20#[derive(Debug, Clone, thiserror::Error)]
21#[non_exhaustive]
22pub enum CryptoError {
23 #[error("Invalid signature")]
24 InvalidSignature,
25
26 #[error("Invalid public key length: expected {expected}, got {actual}")]
27 InvalidKeyLength { expected: usize, actual: usize },
28
29 #[error("Invalid private key: {0}")]
30 InvalidPrivateKey(String),
31
32 #[error("Crypto operation failed: {0}")]
33 OperationFailed(String),
34
35 #[error("Operation not supported on current compilation target")]
36 UnsupportedTarget,
37}
38
39impl crate::AuthsErrorInfo for CryptoError {
40 fn error_code(&self) -> &'static str {
41 match self {
42 Self::InvalidSignature => "AUTHS-E1001",
43 Self::InvalidKeyLength { .. } => {
44 "
45 "
46 }
47 Self::InvalidPrivateKey(_) => "AUTHS-E1003",
48 Self::OperationFailed(_) => "AUTHS-E1004",
49 Self::UnsupportedTarget => "AUTHS-E1005",
50 }
51 }
52
53 fn suggestion(&self) -> Option<&'static str> {
54 match self {
55 Self::InvalidSignature => Some("The signature does not match the data or public key"),
56 Self::InvalidKeyLength { .. } => Some("Ensure the key is exactly 32 bytes for Ed25519"),
57 Self::UnsupportedTarget => {
58 Some("This operation is not available on the current platform")
59 }
60 _ => None,
61 }
62 }
63}
64
65/// Zeroize-on-drop wrapper for a raw 32-byte Ed25519 seed.
66///
67/// This is the portable key representation that crosses the [`CryptoProvider`]
68/// boundary. No ring types leak through the trait — only this raw seed.
69/// The provider materializes the internal keypair from the seed on each call.
70///
71/// Usage:
72/// ```ignore
73/// let (seed, pubkey) = provider.generate_ed25519_keypair().await?;
74/// let sig = provider.sign_ed25519(&seed, b"hello").await?;
75/// // seed is securely zeroed when dropped
76/// ```
77#[derive(Clone, Zeroize, ZeroizeOnDrop)]
78pub struct SecureSeed([u8; 32]);
79
80impl SecureSeed {
81 pub fn new(bytes: [u8; 32]) -> Self {
82 Self(bytes)
83 }
84
85 pub fn as_bytes(&self) -> &[u8; 32] {
86 &self.0
87 }
88}
89
90impl std::fmt::Debug for SecureSeed {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 f.write_str("SecureSeed([REDACTED])")
93 }
94}
95
96/// Abstraction for Ed25519 cryptographic operations across target architectures.
97///
98/// All method signatures use primitive Rust types or [`SecureSeed`] — no
99/// ring-specific types. This ensures domain crates (`auths-core`, `auths-sdk`)
100/// compile without any ring dependency.
101///
102/// Usage:
103/// ```ignore
104/// use auths_crypto::CryptoProvider;
105///
106/// async fn roundtrip(provider: &dyn CryptoProvider) {
107/// let (seed, pk) = provider.generate_ed25519_keypair().await.unwrap();
108/// let sig = provider.sign_ed25519(&seed, b"msg").await.unwrap();
109/// provider.verify_ed25519(&pk, b"msg", &sig).await.unwrap();
110/// }
111/// ```
112#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
113#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
114pub trait CryptoProvider: Send + Sync {
115 /// Verify an Ed25519 signature against a public key and message.
116 async fn verify_ed25519(
117 &self,
118 pubkey: &[u8],
119 message: &[u8],
120 signature: &[u8],
121 ) -> Result<(), CryptoError>;
122
123 /// Sign a message using a raw 32-byte Ed25519 seed.
124 ///
125 /// The provider materializes the internal keypair from the seed on each
126 /// call. This trades minor CPU overhead for a pure, ring-free domain layer.
127 ///
128 /// Args:
129 /// * `seed`: Raw 32-byte Ed25519 private key seed.
130 /// * `message`: The data to sign.
131 ///
132 /// Usage:
133 /// ```ignore
134 /// let sig = provider.sign_ed25519(&seed, b"hello").await?;
135 /// assert_eq!(sig.len(), 64);
136 /// ```
137 async fn sign_ed25519(&self, seed: &SecureSeed, message: &[u8])
138 -> Result<Vec<u8>, CryptoError>;
139
140 /// Generate a fresh Ed25519 keypair.
141 ///
142 /// Returns the raw 32-byte seed and 32-byte public key.
143 ///
144 /// Usage:
145 /// ```ignore
146 /// let (seed, pubkey) = provider.generate_ed25519_keypair().await?;
147 /// assert_eq!(pubkey.len(), 32);
148 /// ```
149 async fn generate_ed25519_keypair(&self) -> Result<(SecureSeed, [u8; 32]), CryptoError>;
150
151 /// Derive the 32-byte public key from a raw seed.
152 ///
153 /// Args:
154 /// * `seed`: Raw 32-byte Ed25519 private key seed.
155 ///
156 /// Usage:
157 /// ```ignore
158 /// let pk = provider.ed25519_public_key_from_seed(&seed).await?;
159 /// assert_eq!(pk.len(), 32);
160 /// ```
161 async fn ed25519_public_key_from_seed(
162 &self,
163 seed: &SecureSeed,
164 ) -> Result<[u8; 32], CryptoError>;
165}
166
167/// Ed25519 public key length in bytes.
168pub const ED25519_PUBLIC_KEY_LEN: usize = 32;
169
170/// Ed25519 signature length in bytes.
171pub const ED25519_SIGNATURE_LEN: usize = 64;