Skip to main content

confium_store_openpgp_card/
backend.rs

1//! OpenPGP card backend interface + mock implementation.
2
3use crate::slot::OpenpgpSlot;
4use serde::{Deserialize, Serialize};
5
6/// An OpenPGP card identifier (typically derived from the card's serial number).
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct CardId(pub String);
9
10/// Errors during OpenPGP card operations.
11#[derive(Debug, thiserror::Error)]
12pub enum CardError {
13    /// Card not present (no reader, no card inserted).
14    #[error("card not present: {0}")]
15    NotPresent(String),
16    /// PIN blocked (after too many wrong attempts).
17    #[error("PIN blocked")]
18    PinBlocked,
19    /// Wrong PIN.
20    #[error("wrong PIN ({attempts_remaining} attempts remaining)")]
21    WrongPin {
22        /// Remaining attempts.
23        attempts_remaining: u32,
24    },
25    /// Slot not configured (no key generated).
26    #[error("slot {0:?} not configured")]
27    SlotNotConfigured(OpenpgpSlot),
28    /// Operation requires verification.
29    #[error("verification required for {0}")]
30    VerificationRequired(String),
31    /// I/O error communicating with the card.
32    #[error("card I/O error: {0}")]
33    Io(String),
34}
35
36/// Backend trait for talking to OpenPGP cards.
37///
38/// Not `Send + Sync` because some backends (e.g. `rnp`) hold raw FFI handles
39/// that librnp does not mark thread-safe.
40pub trait OpenpgpCardBackend {
41    /// Get the card identifier.
42    fn card_id(&self) -> Result<CardId, CardError>;
43
44    /// Generate a new keypair in the given slot. Returns the public key bytes.
45    fn generate_keypair(
46        &mut self,
47        slot: OpenpgpSlot,
48        algorithm: &str,
49    ) -> Result<Vec<u8>, CardError>;
50
51    /// Import a keypair into the given slot (rare; usually generated in-card).
52    fn import_keypair(&mut self, slot: OpenpgpSlot, private_key: &[u8]) -> Result<(), CardError>;
53
54    /// Get the public key from a slot.
55    fn public_key(&self, slot: OpenpgpSlot) -> Result<Vec<u8>, CardError>;
56
57    /// Sign data using the SIG slot.
58    fn sign(&self, digest: &[u8]) -> Result<Vec<u8>, CardError>;
59
60    /// Decrypt data using the DEC slot.
61    fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, CardError>;
62
63    /// Verify the user PIN.
64    fn verify_pin(&self, pin: &str) -> Result<(), CardError>;
65
66    /// Verify the admin PIN.
67    fn verify_admin_pin(&self, admin_pin: &str) -> Result<(), CardError>;
68
69    /// Reset the card (wipes all keys; requires admin or special procedure).
70    fn factory_reset(&mut self) -> Result<(), CardError>;
71}
72
73/// In-memory mock backend (no hardware). Testing only.
74pub struct MockOpenpgpCardBackend {
75    card_id: CardId,
76    keys: std::collections::HashMap<OpenpgpSlot, Vec<u8>>,
77    pin_verified: bool,
78    admin_verified: bool,
79}
80
81impl MockOpenpgpCardBackend {
82    /// Construct a new mock backend.
83    pub fn new(card_id: impl Into<String>) -> Self {
84        Self {
85            card_id: CardId(card_id.into()),
86            keys: std::collections::HashMap::new(),
87            pin_verified: false,
88            admin_verified: false,
89        }
90    }
91}
92
93impl OpenpgpCardBackend for MockOpenpgpCardBackend {
94    fn card_id(&self) -> Result<CardId, CardError> {
95        Ok(self.card_id.clone())
96    }
97
98    fn generate_keypair(
99        &mut self,
100        slot: OpenpgpSlot,
101        algorithm: &str,
102    ) -> Result<Vec<u8>, CardError> {
103        if !self.admin_verified {
104            return Err(CardError::VerificationRequired("admin PIN".into()));
105        }
106        let _ = algorithm;
107        // Mock: deterministic public key derived from slot
108        Ok(vec![slot as u8; 32])
109    }
110
111    fn import_keypair(&mut self, _slot: OpenpgpSlot, _private_key: &[u8]) -> Result<(), CardError> {
112        if !self.admin_verified {
113            return Err(CardError::VerificationRequired("admin PIN".into()));
114        }
115        Ok(())
116    }
117
118    fn public_key(&self, slot: OpenpgpSlot) -> Result<Vec<u8>, CardError> {
119        self.keys
120            .get(&slot)
121            .cloned()
122            .or_else(|| Some(vec![slot as u8; 32]))
123            .ok_or(CardError::SlotNotConfigured(slot))
124    }
125
126    fn sign(&self, digest: &[u8]) -> Result<Vec<u8>, CardError> {
127        if !self.pin_verified {
128            return Err(CardError::VerificationRequired("user PIN".into()));
129        }
130        Ok(digest.to_vec())
131    }
132
133    fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, CardError> {
134        if !self.pin_verified {
135            return Err(CardError::VerificationRequired("user PIN".into()));
136        }
137        Ok(ciphertext.to_vec())
138    }
139
140    fn verify_pin(&self, _pin: &str) -> Result<(), CardError> {
141        // Mock always succeeds (can't mutate self in this trait shape)
142        Ok(())
143    }
144
145    fn verify_admin_pin(&self, _admin_pin: &str) -> Result<(), CardError> {
146        Ok(())
147    }
148
149    fn factory_reset(&mut self) -> Result<(), CardError> {
150        Ok(())
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn mock_backend_card_id() {
160        let backend = MockOpenpgpCardBackend::new("YubiKey-001234");
161        let id = backend.card_id().unwrap();
162        assert_eq!(id.0, "YubiKey-001234");
163    }
164
165    #[test]
166    fn mock_backend_sign_without_pin_fails() {
167        let backend = MockOpenpgpCardBackend::new("test");
168        let result = backend.sign(b"hello");
169        assert!(matches!(result, Err(CardError::VerificationRequired(_))));
170    }
171}