1pub type DevicePublicKey = [u8; 64];
39
40pub type DeviceSignature = [u8; 64];
42
43#[derive(Debug, thiserror::Error)]
44pub enum DeviceKeyError {
45 #[error("secure enclave error: {0}")]
46 Enclave(String),
47 #[error("attestation not supported by this backend")]
48 AttestationUnsupported,
49 #[error("key not found: {0}")]
50 NotFound(String),
51}
52
53pub type Result<T> = std::result::Result<T, DeviceKeyError>;
54
55#[derive(Debug, Clone)]
57pub struct Attestation {
58 pub backend: &'static str,
59 pub evidence: Vec<u8>,
60 pub public_key: DevicePublicKey,
61}
62
63pub trait DeviceKey: Send + Sync {
66 fn label(&self) -> &str;
68
69 fn public_key(&self) -> Result<DevicePublicKey>;
71
72 fn sign_prehash(&self, hash: &[u8; 32]) -> Result<DeviceSignature>;
74
75 fn wrap_secret(&self, secret: &[u8]) -> Result<Vec<u8>>;
79
80 fn unwrap_secret(&self, ciphertext: &[u8]) -> Result<zeroize::Zeroizing<Vec<u8>>>;
83
84 fn attest(&self) -> Result<Attestation> {
86 Err(DeviceKeyError::AttestationUnsupported)
87 }
88}
89
90pub fn open(label: &str) -> Result<Box<dyn DeviceKey>> {
92 backend::open(label)
93}
94
95pub fn create(label: &str) -> Result<Box<dyn DeviceKey>> {
97 backend::create(label)
98}
99
100pub fn delete(label: &str) -> Result<()> {
102 backend::delete(label)
103}
104
105mod unlocker;
106pub use unlocker::SecureEnclaveUnlocker;
107
108pub mod pq_companion;
109pub use pq_companion::PqCompanion;
110
111#[cfg(any(target_os = "macos", target_os = "ios"))]
114mod backend {
115 pub use super::macos::{create, delete, open};
116}
117
118#[cfg(not(any(target_os = "macos", target_os = "ios")))]
119mod backend {
120 use super::{DeviceKey, DeviceKeyError, Result};
121 const STUB: &str = "device-key backend not yet wired for this platform";
122 pub fn open(_label: &str) -> Result<Box<dyn DeviceKey>> {
123 Err(DeviceKeyError::Enclave(STUB.into()))
124 }
125 pub fn create(_label: &str) -> Result<Box<dyn DeviceKey>> {
126 Err(DeviceKeyError::Enclave(STUB.into()))
127 }
128 pub fn delete(_label: &str) -> Result<()> {
129 Err(DeviceKeyError::Enclave(STUB.into()))
130 }
131}
132
133#[cfg(any(target_os = "macos", target_os = "ios"))]
136mod macos {
137 use super::{
138 Attestation, DeviceKey, DeviceKeyError, DevicePublicKey, DeviceSignature, Result,
139 };
140 use security_framework::access_control::{ProtectionMode, SecAccessControl};
141 use security_framework::item::{
142 ItemClass, ItemSearchOptions, Limit, Location, Reference, SearchResult,
143 };
144 use security_framework::key::{Algorithm, GenerateKeyOptions, KeyType, SecKey, Token};
145 use std::collections::HashMap;
146 use std::sync::{Mutex, OnceLock};
147
148 const SAC_USER_PRESENCE: usize = 1 << 0;
150 const SAC_PRIVATE_KEY_USAGE: usize = 1 << 30;
151
152 const ECIES: Algorithm = Algorithm::ECIESEncryptionStandardVariableIVX963SHA256AESGCM;
155
156 const ERR_SEC_ITEM_NOT_FOUND: i32 = -25300;
158
159 fn handles() -> &'static Mutex<HashMap<String, SecKey>> {
165 static HANDLES: OnceLock<Mutex<HashMap<String, SecKey>>> = OnceLock::new();
166 HANDLES.get_or_init(|| Mutex::new(HashMap::new()))
167 }
168
169 fn cache_handle(label: &str, key: SecKey) {
170 if let Ok(mut map) = handles().lock() {
171 map.insert(label.to_string(), key);
172 }
173 }
174
175 fn lookup_handle(label: &str) -> Option<SecKey> {
176 handles().lock().ok()?.get(label).cloned()
177 }
178
179 fn forget_handle(label: &str) {
180 if let Ok(mut map) = handles().lock() {
181 map.remove(label);
182 }
183 }
184
185 pub(super) struct MacEnclaveKey {
186 label: String,
187 key: SecKey,
188 }
189
190 impl MacEnclaveKey {
191 fn raw_public_key(key: &SecKey) -> Result<DevicePublicKey> {
192 let pk = key
193 .public_key()
194 .ok_or_else(|| DeviceKeyError::Enclave("no public key on SecKey".into()))?;
195 let cfdata = pk
196 .external_representation()
197 .ok_or_else(|| DeviceKeyError::Enclave("public key not exportable".into()))?;
198 let bytes = cfdata.bytes();
199 if bytes.len() != 65 || bytes[0] != 0x04 {
200 return Err(DeviceKeyError::Enclave(format!(
201 "unexpected public key format: len={} prefix=0x{:02x}",
202 bytes.len(),
203 bytes.first().copied().unwrap_or(0)
204 )));
205 }
206 let mut out = [0u8; 64];
207 out.copy_from_slice(&bytes[1..]);
208 Ok(out)
209 }
210
211 fn der_to_raw_signature(der: &[u8]) -> Result<DeviceSignature> {
212 fn read_int(p: &[u8]) -> Result<(&[u8], &[u8])> {
213 if p.len() < 2 || p[0] != 0x02 {
214 return Err(DeviceKeyError::Enclave("bad DER INTEGER tag".into()));
215 }
216 let len = p[1] as usize;
217 if p.len() < 2 + len {
218 return Err(DeviceKeyError::Enclave("DER INTEGER truncated".into()));
219 }
220 Ok((&p[2..2 + len], &p[2 + len..]))
221 }
222 if der.len() < 2 || der[0] != 0x30 {
223 return Err(DeviceKeyError::Enclave("bad DER SEQUENCE".into()));
224 }
225 let body = &der[2..];
226 let (r, rest) = read_int(body)?;
227 let (s, _) = read_int(rest)?;
228 let mut out = [0u8; 64];
229 let r_strip = if r.len() == 33 && r[0] == 0 { &r[1..] } else { r };
230 let s_strip = if s.len() == 33 && s[0] == 0 { &s[1..] } else { s };
231 if r_strip.len() > 32 || s_strip.len() > 32 {
232 return Err(DeviceKeyError::Enclave("ECDSA scalar > 32 bytes".into()));
233 }
234 out[32 - r_strip.len()..32].copy_from_slice(r_strip);
235 out[64 - s_strip.len()..64].copy_from_slice(s_strip);
236 Ok(out)
237 }
238 }
239
240 impl DeviceKey for MacEnclaveKey {
241 fn label(&self) -> &str {
242 &self.label
243 }
244
245 fn public_key(&self) -> Result<DevicePublicKey> {
246 Self::raw_public_key(&self.key)
247 }
248
249 fn sign_prehash(&self, hash: &[u8; 32]) -> Result<DeviceSignature> {
250 let der = self
251 .key
252 .create_signature(Algorithm::ECDSASignatureDigestX962SHA256, hash)
253 .map_err(|e| DeviceKeyError::Enclave(format!("create_signature: {}", e)))?;
254 Self::der_to_raw_signature(&der)
255 }
256
257 fn wrap_secret(&self, secret: &[u8]) -> Result<Vec<u8>> {
258 let pubkey = self
262 .key
263 .public_key()
264 .ok_or_else(|| DeviceKeyError::Enclave("no public key on SecKey".into()))?;
265 pubkey
266 .encrypt_data(ECIES, secret)
267 .map_err(|e| DeviceKeyError::Enclave(format!("encrypt_data: {}", e)))
268 }
269
270 fn unwrap_secret(&self, ciphertext: &[u8]) -> Result<zeroize::Zeroizing<Vec<u8>>> {
271 let pt = self
272 .key
273 .decrypt_data(ECIES, ciphertext)
274 .map_err(|e| DeviceKeyError::Enclave(format!("decrypt_data: {}", e)))?;
275 Ok(zeroize::Zeroizing::new(pt))
276 }
277
278 fn attest(&self) -> Result<Attestation> {
279 Err(DeviceKeyError::AttestationUnsupported)
280 }
281 }
282
283 pub fn open(label: &str) -> Result<Box<dyn DeviceKey>> {
284 if let Some(key) = lookup_handle(label) {
287 return Ok(Box::new(MacEnclaveKey {
288 label: label.to_string(),
289 key,
290 }));
291 }
292
293 let mut search = ItemSearchOptions::new();
301 search
302 .class(ItemClass::key())
303 .label(label)
304 .load_refs(true)
305 .limit(Limit::Max(1));
306 if let Ok(results) = search.search() {
307 for r in results {
308 if let SearchResult::Ref(Reference::Key(key)) = r {
309 cache_handle(label, key.clone());
310 return Ok(Box::new(MacEnclaveKey {
311 label: label.to_string(),
312 key,
313 }));
314 }
315 }
316 }
317
318 Err(DeviceKeyError::NotFound(label.to_string()))
319 }
320
321 pub fn create(label: &str) -> Result<Box<dyn DeviceKey>> {
322 let make_opts = |persistent: bool| {
334 let acl = SecAccessControl::create_with_protection(
335 Some(ProtectionMode::AccessibleWhenUnlockedThisDeviceOnly),
336 SAC_USER_PRESENCE | SAC_PRIVATE_KEY_USAGE,
337 )
338 .ok();
339 let mut opts = GenerateKeyOptions::default();
340 opts.set_key_type(KeyType::ec())
341 .set_size_in_bits(256)
342 .set_label(label.to_string())
343 .set_token(Token::SecureEnclave);
344 if let Some(acl) = acl {
345 opts.set_access_control(acl);
346 }
347 if persistent {
348 opts.set_location(Location::DefaultFileKeychain);
349 }
350 opts
351 };
352
353 let key = match SecKey::new(&make_opts(true)) {
354 Ok(k) => k,
355 Err(persistent_err) => {
356 tracing::warn!(
357 label = %label,
358 error = %persistent_err,
359 "persistent Secure Enclave key creation in the file keychain failed; \
360 falling back to a session-only key — wallet will NOT persist across \
361 restarts in this context"
362 );
363 SecKey::new(&make_opts(false))
364 .map_err(|e| DeviceKeyError::Enclave(format!("SecKey::new: {}", e)))?
365 }
366 };
367
368 cache_handle(label, key.clone());
369
370 Ok(Box::new(MacEnclaveKey {
371 label: label.to_string(),
372 key,
373 }))
374 }
375
376 pub fn delete(label: &str) -> Result<()> {
377 forget_handle(label);
380
381 let mut search = ItemSearchOptions::new();
385 search.class(ItemClass::key()).label(label);
386 match search.delete() {
387 Ok(()) => Ok(()),
388 Err(e) if e.code() == ERR_SEC_ITEM_NOT_FOUND => Ok(()),
389 Err(e) => Err(DeviceKeyError::Enclave(format!("SecItemDelete: {}", e))),
390 }
391 }
392}