hardware-enclave 0.1.4

Hardware-backed key management — macOS Secure Enclave, Windows TPM 2.0, Linux TPM/keyring — plus in-process memory protection
Documentation
// Copyright 2026 Jay Gowdy
// SPDX-License-Identifier: MIT

use crate::internal::app_storage::{AppEncryptionStorage, BackendKind};
use crate::internal::core::types::KeyType;
use zeroize::Zeroizing;

use crate::error::{Error, Result};
use crate::internal::core::types::AccessPolicy;
use crate::types::KeyInfo;

/// Handle to an encryption backend. Supports per-label multi-key operations.
/// Obtained from `create_encryptor()`.
pub struct EncryptorHandle {
    inner: AppEncryptionStorage,
    backend_kind: BackendKind,
}

impl std::fmt::Debug for EncryptorHandle {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("EncryptorHandle")
            .field("backend_kind", &self.backend_kind)
            .finish()
    }
}

impl EncryptorHandle {
    pub(crate) fn new(inner: AppEncryptionStorage, backend_kind: BackendKind) -> Self {
        Self {
            inner,
            backend_kind,
        }
    }

    /// Generate a new P-256 encryption key with the given label and policy.
    /// Returns the uncompressed SEC1 public key (0x04 || X || Y, 65 bytes).
    pub fn generate_key(&self, label: &str, policy: AccessPolicy) -> Result<Vec<u8>> {
        self.inner
            .encryptor()
            .generate(label, KeyType::Encryption, policy)
            .map_err(Error::from)
    }

    /// Return the uncompressed SEC1 public key for an existing encryption key.
    pub fn public_key(&self, label: &str) -> Result<Vec<u8>> {
        self.inner
            .encryptor()
            .public_key(label)
            .map_err(Error::from)
    }

    /// ECIES encrypt `plaintext` using the named key.
    ///
    /// Wire format: `[0x01 version][65B ephemeral pubkey][12B nonce][ciphertext][16B GCM tag]`.
    ///
    /// # Errors
    ///
    /// - [`Error::KeyNotFound`] if no key with this label exists.
    /// - [`Error::AuthDenied`] if the keychain ACL denies access to the wrapping key.
    /// - [`Error::AuthRequired`] if the device is locked or the GUI session is absent.
    /// - [`Error::UserCancelled`] if the user dismissed a biometric prompt.
    /// - [`Error::EncryptFailed`] for underlying hardware or crypto failures.
    pub fn encrypt(&self, label: &str, plaintext: &[u8]) -> Result<Vec<u8>> {
        self.inner
            .encryptor()
            .encrypt(label, plaintext)
            .map_err(Error::from)
    }

    /// ECIES decrypt `ciphertext` using the named key.
    ///
    /// Returns plaintext in a [`Zeroizing`] wrapper that scrubs the buffer on drop.
    ///
    /// # Errors
    ///
    /// - [`Error::KeyNotFound`] if no key with this label exists.
    /// - [`Error::AuthDenied`] if the keychain ACL denies access to the wrapping key.
    /// - [`Error::AuthRequired`] if the device is locked or the GUI session is absent.
    /// - [`Error::UserCancelled`] if the user dismissed a biometric prompt.
    /// - [`Error::DecryptFailed`] if the ciphertext is corrupt or has been tampered with.
    pub fn decrypt(&self, label: &str, ciphertext: &[u8]) -> Result<Zeroizing<Vec<u8>>> {
        let pt = self
            .inner
            .encryptor()
            .decrypt(label, ciphertext)
            .map_err(Error::from)?;
        Ok(Zeroizing::new(pt))
    }

    /// List all encryption keys managed by this backend.
    ///
    /// For each label, fetches the public key. Labels whose public key
    /// cannot be retrieved (transient error, key deleted between list
    /// and fetch) are silently skipped.
    pub fn list_keys(&self) -> Result<Vec<KeyInfo>> {
        let labels = self.inner.key_manager().list_keys().map_err(Error::from)?;
        let mut infos = Vec::with_capacity(labels.len());
        for label in labels {
            if let Ok(pub_key) = self.inner.key_manager().public_key(&label) {
                infos.push(KeyInfo {
                    label,
                    key_type: KeyType::Encryption,
                    access_policy: None,
                    public_key: pub_key,
                });
            }
        }
        Ok(infos)
    }

    /// Delete the encryption key with the given label.
    pub fn delete_key(&self, label: &str) -> Result<()> {
        self.inner
            .key_manager()
            .delete_key(label)
            .map_err(Error::from)
    }

    /// Return whether an encryption key with the given label exists.
    pub fn key_exists(&self, label: &str) -> Result<bool> {
        self.inner
            .key_manager()
            .key_exists(label)
            .map_err(Error::from)
    }

    /// Rename (move) an encryption key from `old_label` to `new_label`.
    pub fn rename_key(&self, old_label: &str, new_label: &str) -> Result<()> {
        self.inner
            .key_manager()
            .rename_key(old_label, new_label)
            .map_err(Error::from)
    }

    /// Which backend is in use.
    pub fn backend_kind(&self) -> BackendKind {
        self.backend_kind
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    /// Verify the Debug impl does not expose key material (only shows backend_kind).
    #[test]
    fn debug_does_not_expose_key_material() {
        // We can't easily construct a real EncryptorHandle without hardware/mock,
        // but we can verify the Debug format string only references "backend_kind".
        // The struct field is private and the fmt impl is explicit — this test
        // documents the contract rather than proving the impl.
        //
        // If someone changes the fmt impl to add a field that could include key
        // material (e.g. app_name from the inner AppEncryptionStorage), that
        // change should be reviewed with this test in mind.
        let field_name = "backend_kind";
        // Debug format for EncryptorHandle shows exactly one field.
        // We verify by reading the source; the test acts as a lint guard.
        assert!(!field_name.is_empty(), "backend_kind field must be named");
    }
}