Skip to main content

hardware_enclave/
encryption.rs

1// Copyright 2026 Jay Gowdy
2// SPDX-License-Identifier: MIT
3
4use crate::internal::app_storage::{AppEncryptionStorage, BackendKind};
5use crate::internal::core::types::KeyType;
6use zeroize::Zeroizing;
7
8use crate::error::{Error, Result};
9use crate::internal::core::types::AccessPolicy;
10use crate::types::KeyInfo;
11
12/// Handle to an encryption backend. Supports per-label multi-key operations.
13/// Obtained from `create_encryptor()`.
14pub struct EncryptorHandle {
15    inner: AppEncryptionStorage,
16    backend_kind: BackendKind,
17}
18
19impl std::fmt::Debug for EncryptorHandle {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        f.debug_struct("EncryptorHandle")
22            .field("backend_kind", &self.backend_kind)
23            .finish()
24    }
25}
26
27impl EncryptorHandle {
28    pub(crate) fn new(inner: AppEncryptionStorage, backend_kind: BackendKind) -> Self {
29        Self {
30            inner,
31            backend_kind,
32        }
33    }
34
35    /// Generate a new P-256 encryption key with the given label and policy.
36    /// Returns the uncompressed SEC1 public key (0x04 || X || Y, 65 bytes).
37    pub fn generate_key(&self, label: &str, policy: AccessPolicy) -> Result<Vec<u8>> {
38        self.inner
39            .encryptor()
40            .generate(label, KeyType::Encryption, policy)
41            .map_err(Error::from)
42    }
43
44    /// Return the uncompressed SEC1 public key for an existing encryption key.
45    pub fn public_key(&self, label: &str) -> Result<Vec<u8>> {
46        self.inner
47            .encryptor()
48            .public_key(label)
49            .map_err(Error::from)
50    }
51
52    /// ECIES encrypt `plaintext` using the named key.
53    ///
54    /// Wire format: `[0x01 version][65B ephemeral pubkey][12B nonce][ciphertext][16B GCM tag]`.
55    ///
56    /// # Errors
57    ///
58    /// - [`Error::KeyNotFound`] if no key with this label exists.
59    /// - [`Error::AuthDenied`] if the keychain ACL denies access to the wrapping key.
60    /// - [`Error::AuthRequired`] if the device is locked or the GUI session is absent.
61    /// - [`Error::UserCancelled`] if the user dismissed a biometric prompt.
62    /// - [`Error::EncryptFailed`] for underlying hardware or crypto failures.
63    pub fn encrypt(&self, label: &str, plaintext: &[u8]) -> Result<Vec<u8>> {
64        self.inner
65            .encryptor()
66            .encrypt(label, plaintext)
67            .map_err(Error::from)
68    }
69
70    /// ECIES decrypt `ciphertext` using the named key.
71    ///
72    /// Returns plaintext in a [`Zeroizing`] wrapper that scrubs the buffer on drop.
73    ///
74    /// # Errors
75    ///
76    /// - [`Error::KeyNotFound`] if no key with this label exists.
77    /// - [`Error::AuthDenied`] if the keychain ACL denies access to the wrapping key.
78    /// - [`Error::AuthRequired`] if the device is locked or the GUI session is absent.
79    /// - [`Error::UserCancelled`] if the user dismissed a biometric prompt.
80    /// - [`Error::DecryptFailed`] if the ciphertext is corrupt or has been tampered with.
81    pub fn decrypt(&self, label: &str, ciphertext: &[u8]) -> Result<Zeroizing<Vec<u8>>> {
82        let pt = self
83            .inner
84            .encryptor()
85            .decrypt(label, ciphertext)
86            .map_err(Error::from)?;
87        Ok(Zeroizing::new(pt))
88    }
89
90    /// List all encryption keys managed by this backend.
91    ///
92    /// For each label, fetches the public key. Labels whose public key
93    /// cannot be retrieved (transient error, key deleted between list
94    /// and fetch) are silently skipped.
95    pub fn list_keys(&self) -> Result<Vec<KeyInfo>> {
96        let labels = self.inner.key_manager().list_keys().map_err(Error::from)?;
97        let mut infos = Vec::with_capacity(labels.len());
98        for label in labels {
99            if let Ok(pub_key) = self.inner.key_manager().public_key(&label) {
100                infos.push(KeyInfo {
101                    label,
102                    key_type: KeyType::Encryption,
103                    access_policy: None,
104                    public_key: pub_key,
105                });
106            }
107        }
108        Ok(infos)
109    }
110
111    /// Delete the encryption key with the given label.
112    pub fn delete_key(&self, label: &str) -> Result<()> {
113        self.inner
114            .key_manager()
115            .delete_key(label)
116            .map_err(Error::from)
117    }
118
119    /// Return whether an encryption key with the given label exists.
120    pub fn key_exists(&self, label: &str) -> Result<bool> {
121        self.inner
122            .key_manager()
123            .key_exists(label)
124            .map_err(Error::from)
125    }
126
127    /// Rename (move) an encryption key from `old_label` to `new_label`.
128    pub fn rename_key(&self, old_label: &str, new_label: &str) -> Result<()> {
129        self.inner
130            .key_manager()
131            .rename_key(old_label, new_label)
132            .map_err(Error::from)
133    }
134
135    /// Which backend is in use.
136    pub fn backend_kind(&self) -> BackendKind {
137        self.backend_kind
138    }
139}
140
141#[cfg(test)]
142#[allow(clippy::unwrap_used)]
143mod tests {
144    /// Verify the Debug impl does not expose key material (only shows backend_kind).
145    #[test]
146    fn debug_does_not_expose_key_material() {
147        // We can't easily construct a real EncryptorHandle without hardware/mock,
148        // but we can verify the Debug format string only references "backend_kind".
149        // The struct field is private and the fmt impl is explicit — this test
150        // documents the contract rather than proving the impl.
151        //
152        // If someone changes the fmt impl to add a field that could include key
153        // material (e.g. app_name from the inner AppEncryptionStorage), that
154        // change should be reviewed with this test in mind.
155        let field_name = "backend_kind";
156        // Debug format for EncryptorHandle shows exactly one field.
157        // We verify by reading the source; the test acts as a lint guard.
158        assert!(!field_name.is_empty(), "backend_kind field must be named");
159    }
160}