chie_crypto/
key_backup.rs

1//! Key backup and recovery mechanisms for secure key management.
2//!
3//! This module provides secure backup and recovery of cryptographic keys using
4//! Shamir's Secret Sharing for threshold-based recovery and encrypted backup files.
5//!
6//! # Features
7//!
8//! - **Shamir Secret Sharing Backup**: Split keys into N shares requiring M to recover
9//! - **Encrypted Backup**: Password-based encryption for backup files
10//! - **Multiple Key Types**: Support for signing keys, encryption keys, and generic secrets
11//! - **Versioning**: Track backup versions for key rotation
12//! - **Metadata**: Include timestamps, labels, and key types in backups
13//!
14//! # Example
15//!
16//! ```
17//! use chie_crypto::key_backup::*;
18//! use chie_crypto::signing::KeyPair;
19//!
20//! // Create a signing key
21//! let keypair = KeyPair::generate();
22//!
23//! // Create a backup with 3-of-5 threshold
24//! let backup_config = BackupConfig::new(3, 5)
25//!     .with_label("my-signing-key")
26//!     .with_description("Main signing key for node");
27//!
28//! let shares = backup_key_shamir(&keypair, &backup_config).unwrap();
29//!
30//! // Distribute shares to different locations/devices
31//! // Later, recover with any 3 shares
32//! let recovered_keypair = recover_key_shamir(&shares[0..3]).unwrap();
33//!
34//! // Verify recovery
35//! assert_eq!(keypair.public_key(), recovered_keypair.public_key());
36//! ```
37
38use crate::encryption::{decrypt, encrypt, generate_nonce};
39use crate::hash::hash;
40use crate::kdf::hkdf_extract_expand;
41use crate::shamir::{Share, reconstruct, split};
42use crate::signing::KeyPair;
43use rand::RngCore;
44use serde::{Deserialize, Serialize};
45use std::time::{SystemTime, UNIX_EPOCH};
46
47/// Errors that can occur during backup and recovery
48#[derive(Debug)]
49pub enum BackupError {
50    /// Invalid threshold configuration
51    InvalidThreshold(String),
52    /// Insufficient shares for recovery
53    InsufficientShares(String),
54    /// Share corruption or tampering detected
55    InvalidShare(String),
56    /// Encryption/decryption error
57    CryptoError(String),
58    /// Serialization error
59    SerializationError(String),
60    /// Invalid password
61    InvalidPassword,
62    /// Version mismatch
63    VersionMismatch(String),
64}
65
66impl std::fmt::Display for BackupError {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        match self {
69            BackupError::InvalidThreshold(msg) => write!(f, "Invalid threshold: {}", msg),
70            BackupError::InsufficientShares(msg) => write!(f, "Insufficient shares: {}", msg),
71            BackupError::InvalidShare(msg) => write!(f, "Invalid share: {}", msg),
72            BackupError::CryptoError(msg) => write!(f, "Crypto error: {}", msg),
73            BackupError::SerializationError(msg) => write!(f, "Serialization error: {}", msg),
74            BackupError::InvalidPassword => write!(f, "Invalid password"),
75            BackupError::VersionMismatch(msg) => write!(f, "Version mismatch: {}", msg),
76        }
77    }
78}
79
80impl std::error::Error for BackupError {}
81
82pub type BackupResult<T> = Result<T, BackupError>;
83
84/// Type of key being backed up
85#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
86pub enum KeyType {
87    /// Ed25519 signing key
88    SigningKey,
89    /// ChaCha20-Poly1305 encryption key
90    EncryptionKey,
91    /// Generic secret data
92    GenericSecret,
93}
94
95/// Configuration for key backup
96#[derive(Clone, Debug, Serialize, Deserialize)]
97pub struct BackupConfig {
98    /// Threshold: number of shares required for recovery
99    pub threshold: usize,
100    /// Total number of shares to generate
101    pub total_shares: usize,
102    /// Optional label for the backup
103    pub label: Option<String>,
104    /// Optional description
105    pub description: Option<String>,
106    /// Key type
107    pub key_type: KeyType,
108    /// Backup version (for key rotation tracking)
109    pub version: u32,
110}
111
112impl BackupConfig {
113    /// Create a new backup configuration
114    pub fn new(threshold: usize, total_shares: usize) -> Self {
115        Self {
116            threshold,
117            total_shares,
118            label: None,
119            description: None,
120            key_type: KeyType::GenericSecret,
121            version: 1,
122        }
123    }
124
125    /// Set the label for this backup
126    pub fn with_label(mut self, label: &str) -> Self {
127        self.label = Some(label.to_string());
128        self
129    }
130
131    /// Set the description for this backup
132    pub fn with_description(mut self, description: &str) -> Self {
133        self.description = Some(description.to_string());
134        self
135    }
136
137    /// Set the key type
138    pub fn with_key_type(mut self, key_type: KeyType) -> Self {
139        self.key_type = key_type;
140        self
141    }
142
143    /// Set the version
144    pub fn with_version(mut self, version: u32) -> Self {
145        self.version = version;
146        self
147    }
148
149    /// Validate the configuration
150    pub fn validate(&self) -> BackupResult<()> {
151        if self.threshold == 0 {
152            return Err(BackupError::InvalidThreshold(
153                "Threshold must be at least 1".to_string(),
154            ));
155        }
156        if self.threshold > self.total_shares {
157            return Err(BackupError::InvalidThreshold(format!(
158                "Threshold ({}) cannot exceed total shares ({})",
159                self.threshold, self.total_shares
160            )));
161        }
162        if self.total_shares > 255 {
163            return Err(BackupError::InvalidThreshold(
164                "Total shares cannot exceed 255".to_string(),
165            ));
166        }
167        Ok(())
168    }
169}
170
171/// A single backup share with metadata
172#[derive(Clone, Debug, Serialize, Deserialize)]
173pub struct BackupShare {
174    /// Share index (1-based)
175    pub index: u8,
176    /// The actual share data (from Shamir's secret sharing)
177    pub share_data: Vec<u8>,
178    /// Configuration metadata
179    pub config: BackupConfig,
180    /// Creation timestamp
181    pub created_at: u64,
182    /// Checksum for integrity verification
183    pub checksum: [u8; 32],
184}
185
186impl BackupShare {
187    /// Create a new backup share
188    fn new(index: u8, share: Share, config: BackupConfig) -> Self {
189        let timestamp = SystemTime::now()
190            .duration_since(UNIX_EPOCH)
191            .unwrap()
192            .as_secs();
193
194        let share_data = share.data.clone();
195
196        let mut data = Vec::new();
197        data.push(index);
198        data.push(share.index);
199        data.extend_from_slice(&share_data);
200        data.extend_from_slice(&timestamp.to_le_bytes());
201
202        let checksum = hash(&data);
203
204        Self {
205            index,
206            share_data,
207            config,
208            created_at: timestamp,
209            checksum,
210        }
211    }
212
213    /// Verify the integrity of this share
214    pub fn verify_integrity(&self) -> bool {
215        let mut data = Vec::new();
216        data.push(self.index);
217        data.push(self.index); // Share index matches backup index
218        data.extend_from_slice(&self.share_data);
219        data.extend_from_slice(&self.created_at.to_le_bytes());
220
221        let expected_checksum = hash(&data);
222        expected_checksum == self.checksum
223    }
224
225    /// Convert to Shamir Share for recovery
226    fn to_share(&self) -> BackupResult<Share> {
227        Share::new(self.index, self.share_data.clone())
228            .map_err(|e| BackupError::InvalidShare(e.to_string()))
229    }
230
231    /// Serialize to bytes
232    pub fn to_bytes(&self) -> BackupResult<Vec<u8>> {
233        crate::codec::encode(self).map_err(|e| BackupError::SerializationError(e.to_string()))
234    }
235
236    /// Deserialize from bytes
237    pub fn from_bytes(bytes: &[u8]) -> BackupResult<Self> {
238        crate::codec::decode(bytes).map_err(|e| BackupError::SerializationError(e.to_string()))
239    }
240}
241
242/// Encrypted backup file containing a key
243#[derive(Clone, Debug, Serialize, Deserialize)]
244pub struct EncryptedBackup {
245    /// Encrypted key data
246    pub ciphertext: Vec<u8>,
247    /// Nonce used for encryption (12 bytes)
248    pub nonce: [u8; 12],
249    /// Salt for password derivation
250    pub salt: [u8; 32],
251    /// Configuration metadata
252    pub config: BackupConfig,
253    /// Creation timestamp
254    pub created_at: u64,
255}
256
257impl EncryptedBackup {
258    /// Serialize to bytes
259    pub fn to_bytes(&self) -> BackupResult<Vec<u8>> {
260        crate::codec::encode(self).map_err(|e| BackupError::SerializationError(e.to_string()))
261    }
262
263    /// Deserialize from bytes
264    pub fn from_bytes(bytes: &[u8]) -> BackupResult<Self> {
265        crate::codec::decode(bytes).map_err(|e| BackupError::SerializationError(e.to_string()))
266    }
267}
268
269/// Backup a key using Shamir's Secret Sharing
270pub fn backup_key_shamir(
271    keypair: &KeyPair,
272    config: &BackupConfig,
273) -> BackupResult<Vec<BackupShare>> {
274    config.validate()?;
275
276    // Extract secret key bytes
277    let secret = keypair.secret_key();
278
279    // Split into shares
280    let shares = split(&secret, config.threshold, config.total_shares)
281        .map_err(|e| BackupError::CryptoError(e.to_string()))?;
282
283    // Create backup shares with metadata
284    let backup_shares: Vec<BackupShare> = shares
285        .into_iter()
286        .enumerate()
287        .map(|(i, share)| BackupShare::new((i + 1) as u8, share, config.clone()))
288        .collect();
289
290    Ok(backup_shares)
291}
292
293/// Recover a key from Shamir shares
294pub fn recover_key_shamir(shares: &[BackupShare]) -> BackupResult<KeyPair> {
295    if shares.is_empty() {
296        return Err(BackupError::InsufficientShares(
297            "No shares provided".to_string(),
298        ));
299    }
300
301    // Verify all shares have compatible configuration
302    let config = &shares[0].config;
303    if shares.len() < config.threshold {
304        return Err(BackupError::InsufficientShares(format!(
305            "Need {} shares but only {} provided",
306            config.threshold,
307            shares.len()
308        )));
309    }
310
311    // Verify integrity of all shares
312    for share in shares {
313        if !share.verify_integrity() {
314            return Err(BackupError::InvalidShare(format!(
315                "Share {} failed integrity check",
316                share.index
317            )));
318        }
319
320        // Verify compatible configuration
321        if share.config.threshold != config.threshold {
322            return Err(BackupError::InvalidShare(
323                "Incompatible share thresholds".to_string(),
324            ));
325        }
326    }
327
328    // Extract Share objects
329    let raw_shares: Vec<Share> = shares
330        .iter()
331        .map(|bs| bs.to_share())
332        .collect::<Result<Vec<_>, _>>()?;
333
334    // Combine shares to recover secret
335    let secret = reconstruct(&raw_shares).map_err(|e| BackupError::CryptoError(e.to_string()))?;
336
337    // Reconstruct keypair from secret bytes
338    if secret.len() != 32 {
339        return Err(BackupError::CryptoError(
340            "Invalid secret length".to_string(),
341        ));
342    }
343    let mut secret_array = [0u8; 32];
344    secret_array.copy_from_slice(&secret);
345    KeyPair::from_secret_key(&secret_array).map_err(|e| BackupError::CryptoError(e.to_string()))
346}
347
348/// Backup a generic secret using Shamir's Secret Sharing
349pub fn backup_secret_shamir(
350    secret: &[u8],
351    config: &BackupConfig,
352) -> BackupResult<Vec<BackupShare>> {
353    config.validate()?;
354
355    // Split into shares
356    let shares = split(secret, config.threshold, config.total_shares)
357        .map_err(|e| BackupError::CryptoError(e.to_string()))?;
358
359    // Create backup shares with metadata
360    let backup_shares: Vec<BackupShare> = shares
361        .into_iter()
362        .enumerate()
363        .map(|(i, share)| BackupShare::new((i + 1) as u8, share, config.clone()))
364        .collect();
365
366    Ok(backup_shares)
367}
368
369/// Recover a generic secret from Shamir shares
370pub fn recover_secret_shamir(shares: &[BackupShare]) -> BackupResult<Vec<u8>> {
371    if shares.is_empty() {
372        return Err(BackupError::InsufficientShares(
373            "No shares provided".to_string(),
374        ));
375    }
376
377    // Verify all shares have compatible configuration
378    let config = &shares[0].config;
379    if shares.len() < config.threshold {
380        return Err(BackupError::InsufficientShares(format!(
381            "Need {} shares but only {} provided",
382            config.threshold,
383            shares.len()
384        )));
385    }
386
387    // Verify integrity of all shares
388    for share in shares {
389        if !share.verify_integrity() {
390            return Err(BackupError::InvalidShare(format!(
391                "Share {} failed integrity check",
392                share.index
393            )));
394        }
395    }
396
397    // Extract Share objects
398    let raw_shares: Vec<Share> = shares
399        .iter()
400        .map(|bs| bs.to_share())
401        .collect::<Result<Vec<_>, _>>()?;
402
403    // Combine shares to recover secret
404    reconstruct(&raw_shares).map_err(|e| BackupError::CryptoError(e.to_string()))
405}
406
407/// Create an encrypted backup of a key using password-based encryption
408pub fn backup_key_encrypted(
409    keypair: &KeyPair,
410    password: &str,
411    config: &BackupConfig,
412) -> BackupResult<EncryptedBackup> {
413    config.validate()?;
414
415    // Generate random salt
416    let mut salt = [0u8; 32];
417    rand::thread_rng().fill_bytes(&mut salt);
418
419    // Derive encryption key from password
420    let key_bytes = hkdf_extract_expand(password.as_bytes(), &salt, b"chie-backup-encryption-v1");
421
422    // Extract secret key bytes
423    let secret = keypair.secret_key();
424
425    // Generate nonce
426    let nonce = generate_nonce();
427
428    // Encrypt the secret
429    let ciphertext = encrypt(&secret, &key_bytes, &nonce)
430        .map_err(|e| BackupError::CryptoError(e.to_string()))?;
431
432    let timestamp = SystemTime::now()
433        .duration_since(UNIX_EPOCH)
434        .unwrap()
435        .as_secs();
436
437    // Convert nonce to array
438    let nonce_bytes: [u8; 12] = nonce.as_slice().try_into().unwrap();
439
440    Ok(EncryptedBackup {
441        ciphertext,
442        nonce: nonce_bytes,
443        salt,
444        config: config.clone(),
445        created_at: timestamp,
446    })
447}
448
449/// Recover a key from an encrypted backup
450pub fn recover_key_encrypted(backup: &EncryptedBackup, password: &str) -> BackupResult<KeyPair> {
451    // Derive decryption key from password
452    let key_bytes = hkdf_extract_expand(
453        password.as_bytes(),
454        &backup.salt,
455        b"chie-backup-encryption-v1",
456    );
457
458    // Convert nonce bytes to Nonce
459    let nonce = &backup.nonce;
460
461    // Decrypt the secret
462    let secret =
463        decrypt(&backup.ciphertext, &key_bytes, nonce).map_err(|_| BackupError::InvalidPassword)?;
464
465    // Reconstruct keypair from secret bytes
466    if secret.len() != 32 {
467        return Err(BackupError::CryptoError(
468            "Invalid secret length".to_string(),
469        ));
470    }
471    let mut secret_array = [0u8; 32];
472    secret_array.copy_from_slice(&secret);
473    KeyPair::from_secret_key(&secret_array).map_err(|e| BackupError::CryptoError(e.to_string()))
474}
475
476/// Create an encrypted backup of a generic secret
477pub fn backup_secret_encrypted(
478    secret: &[u8],
479    password: &str,
480    config: &BackupConfig,
481) -> BackupResult<EncryptedBackup> {
482    config.validate()?;
483
484    // Generate random salt
485    let mut salt = [0u8; 32];
486    rand::thread_rng().fill_bytes(&mut salt);
487
488    // Derive encryption key from password
489    let key_bytes = hkdf_extract_expand(password.as_bytes(), &salt, b"chie-backup-encryption-v1");
490
491    // Generate nonce
492    let nonce = generate_nonce();
493
494    // Encrypt the secret
495    let ciphertext =
496        encrypt(secret, &key_bytes, &nonce).map_err(|e| BackupError::CryptoError(e.to_string()))?;
497
498    let timestamp = SystemTime::now()
499        .duration_since(UNIX_EPOCH)
500        .unwrap()
501        .as_secs();
502
503    // Convert nonce to array
504    let nonce_bytes: [u8; 12] = nonce.as_slice().try_into().unwrap();
505
506    Ok(EncryptedBackup {
507        ciphertext,
508        nonce: nonce_bytes,
509        salt,
510        config: config.clone(),
511        created_at: timestamp,
512    })
513}
514
515/// Recover a generic secret from an encrypted backup
516pub fn recover_secret_encrypted(backup: &EncryptedBackup, password: &str) -> BackupResult<Vec<u8>> {
517    // Derive decryption key from password
518    let key_bytes = hkdf_extract_expand(
519        password.as_bytes(),
520        &backup.salt,
521        b"chie-backup-encryption-v1",
522    );
523
524    // Convert nonce bytes to Nonce
525    let nonce = &backup.nonce;
526
527    // Decrypt the secret
528    decrypt(&backup.ciphertext, &key_bytes, nonce).map_err(|_| BackupError::InvalidPassword)
529}
530
531#[cfg(test)]
532mod tests {
533    use super::*;
534
535    #[test]
536    fn test_shamir_backup_recovery() {
537        let keypair = KeyPair::generate();
538        let config = BackupConfig::new(3, 5).with_label("test-key");
539
540        // Create backup shares
541        let shares = backup_key_shamir(&keypair, &config).unwrap();
542        assert_eq!(shares.len(), 5);
543
544        // Verify all shares have correct metadata
545        for (i, share) in shares.iter().enumerate() {
546            assert_eq!(share.index, (i + 1) as u8);
547            assert!(share.verify_integrity());
548        }
549
550        // Recover with exactly threshold shares
551        let recovered = recover_key_shamir(&shares[0..3]).unwrap();
552        assert_eq!(keypair.public_key(), recovered.public_key());
553
554        // Recover with more than threshold shares
555        let recovered = recover_key_shamir(&shares[1..5]).unwrap();
556        assert_eq!(keypair.public_key(), recovered.public_key());
557    }
558
559    #[test]
560    fn test_shamir_insufficient_shares() {
561        let keypair = KeyPair::generate();
562        let config = BackupConfig::new(3, 5);
563        let shares = backup_key_shamir(&keypair, &config).unwrap();
564
565        // Try to recover with only 2 shares (need 3)
566        let result = recover_key_shamir(&shares[0..2]);
567        assert!(result.is_err());
568    }
569
570    #[test]
571    fn test_encrypted_backup_recovery() {
572        let keypair = KeyPair::generate();
573        let password = "secure_password_123";
574        let config = BackupConfig::new(1, 1).with_key_type(KeyType::SigningKey);
575
576        // Create encrypted backup
577        let backup = backup_key_encrypted(&keypair, password, &config).unwrap();
578
579        // Recover with correct password
580        let recovered = recover_key_encrypted(&backup, password).unwrap();
581        assert_eq!(keypair.public_key(), recovered.public_key());
582    }
583
584    #[test]
585    fn test_encrypted_backup_wrong_password() {
586        let keypair = KeyPair::generate();
587        let password = "correct_password";
588        let wrong_password = "wrong_password";
589        let config = BackupConfig::new(1, 1);
590
591        let backup = backup_key_encrypted(&keypair, password, &config).unwrap();
592
593        // Try to recover with wrong password
594        let result = recover_key_encrypted(&backup, wrong_password);
595        assert!(result.is_err());
596    }
597
598    #[test]
599    fn test_backup_share_serialization() {
600        let keypair = KeyPair::generate();
601        let config = BackupConfig::new(2, 3);
602        let shares = backup_key_shamir(&keypair, &config).unwrap();
603
604        // Serialize and deserialize first share
605        let bytes = shares[0].to_bytes().unwrap();
606        let recovered_share = BackupShare::from_bytes(&bytes).unwrap();
607
608        assert_eq!(shares[0].index, recovered_share.index);
609        assert!(recovered_share.verify_integrity());
610    }
611
612    #[test]
613    fn test_encrypted_backup_serialization() {
614        let keypair = KeyPair::generate();
615        let password = "test_password";
616        let config = BackupConfig::new(1, 1);
617
618        let backup = backup_key_encrypted(&keypair, password, &config).unwrap();
619
620        // Serialize and deserialize
621        let bytes = backup.to_bytes().unwrap();
622        let recovered_backup = EncryptedBackup::from_bytes(&bytes).unwrap();
623
624        // Verify we can still decrypt
625        let recovered_key = recover_key_encrypted(&recovered_backup, password).unwrap();
626        assert_eq!(keypair.public_key(), recovered_key.public_key());
627    }
628
629    #[test]
630    fn test_generic_secret_shamir_backup() {
631        let secret = b"my secret data that needs backup";
632        let config = BackupConfig::new(2, 4).with_key_type(KeyType::GenericSecret);
633
634        let shares = backup_secret_shamir(secret, &config).unwrap();
635        assert_eq!(shares.len(), 4);
636
637        // Recover with 2 shares
638        let recovered = recover_secret_shamir(&shares[0..2]).unwrap();
639        assert_eq!(secret.as_slice(), recovered.as_slice());
640
641        // Recover with 3 shares
642        let recovered = recover_secret_shamir(&shares[1..4]).unwrap();
643        assert_eq!(secret.as_slice(), recovered.as_slice());
644    }
645
646    #[test]
647    fn test_generic_secret_encrypted_backup() {
648        let secret = b"confidential data";
649        let password = "strong_password";
650        let config = BackupConfig::new(1, 1);
651
652        let backup = backup_secret_encrypted(secret, password, &config).unwrap();
653        let recovered = recover_secret_encrypted(&backup, password).unwrap();
654
655        assert_eq!(secret.as_slice(), recovered.as_slice());
656    }
657
658    #[test]
659    fn test_invalid_threshold_config() {
660        // Threshold = 0
661        let config = BackupConfig::new(0, 5);
662        assert!(config.validate().is_err());
663
664        // Threshold > total
665        let config = BackupConfig::new(6, 5);
666        assert!(config.validate().is_err());
667
668        // Total > 255
669        let config = BackupConfig::new(128, 256);
670        assert!(config.validate().is_err());
671    }
672
673    #[test]
674    fn test_backup_config_builder() {
675        let config = BackupConfig::new(3, 5)
676            .with_label("main-key")
677            .with_description("Primary signing key")
678            .with_key_type(KeyType::SigningKey)
679            .with_version(2);
680
681        assert_eq!(config.label, Some("main-key".to_string()));
682        assert_eq!(config.description, Some("Primary signing key".to_string()));
683        assert_eq!(config.key_type, KeyType::SigningKey);
684        assert_eq!(config.version, 2);
685    }
686
687    #[test]
688    fn test_share_integrity_verification() {
689        let keypair = KeyPair::generate();
690        let config = BackupConfig::new(2, 3);
691        let shares = backup_key_shamir(&keypair, &config).unwrap();
692
693        // All shares should verify
694        for share in &shares {
695            assert!(share.verify_integrity());
696        }
697
698        // Corrupt a share
699        let mut corrupted = shares[0].clone();
700        corrupted.share_data[0] ^= 0xFF; // Flip some bits
701
702        // Should fail integrity check
703        assert!(!corrupted.verify_integrity());
704    }
705
706    #[test]
707    fn test_different_passwords_different_ciphertexts() {
708        let keypair = KeyPair::generate();
709        let config = BackupConfig::new(1, 1);
710
711        let backup1 = backup_key_encrypted(&keypair, "password1", &config).unwrap();
712        let backup2 = backup_key_encrypted(&keypair, "password2", &config).unwrap();
713
714        // Different passwords should produce different ciphertexts
715        assert_ne!(backup1.ciphertext, backup2.ciphertext);
716        assert_ne!(backup1.salt, backup2.salt);
717    }
718
719    #[test]
720    fn test_empty_shares_recovery() {
721        let shares: Vec<BackupShare> = vec![];
722        let result = recover_key_shamir(&shares);
723        assert!(result.is_err());
724
725        let result = recover_secret_shamir(&shares);
726        assert!(result.is_err());
727    }
728}