promocrypt_core/
binary_file.rs

1//! Binary file format handling for .bin files.
2//!
3//! Implements the promocrypt binary file format including:
4//! - Two-key encryption (machineID + secret)
5//! - Machine binding
6//! - Counter management
7//! - Code generation and validation
8//! - Storage encryption for database codes
9//! - History tracking and generation logs
10
11use std::fs::{File, OpenOptions};
12use std::io::{Read, Seek, SeekFrom, Write};
13use std::path::{Path, PathBuf};
14
15use serde::{Deserialize, Serialize};
16
17use crate::alphabet::Alphabet;
18use crate::audit::{AuditInfo, GenerationLogEntry, History};
19use crate::counter::{CounterManager, CounterMode, counter_from_bytes, counter_to_bytes};
20use crate::damm::DammTable;
21use crate::encryption::{
22    ENCRYPTED_KEY_SIZE, NONCE_SIZE, SALT_SIZE, decrypt_code_from_storage, decrypt_data,
23    decrypt_data_key, encrypt_code_for_storage, encrypt_data, encrypt_data_key, generate_nonce,
24    generate_random_key, generate_salt, hash_secret,
25};
26use crate::error::{PromocryptError, Result, ValidationResult};
27use crate::generator::{CheckPosition, CodeFormat, generate_batch, generate_code};
28use crate::machine_id::get_machine_id;
29
30// File format constants
31const MAGIC: &[u8; 8] = b"PROMOCRY";
32const VERSION: u8 = 0x02;
33
34// Header offsets
35const OFFSET_MAGIC: usize = 0;
36const OFFSET_VERSION: usize = 8;
37const OFFSET_FLAGS: usize = 9;
38const OFFSET_HEADER_CRC: usize = 10;
39const OFFSET_SALT: usize = 14;
40const OFFSET_MACHINE_NONCE: usize = 30;
41const OFFSET_MACHINE_KEY: usize = 42;
42const OFFSET_SECRET_NONCE: usize = 90;
43const OFFSET_SECRET_KEY: usize = 102;
44const OFFSET_DATA_NONCE: usize = 150;
45const OFFSET_DATA_LENGTH: usize = 162;
46const OFFSET_DATA: usize = 166;
47
48// Header size (before encrypted data)
49const HEADER_SIZE: usize = OFFSET_DATA;
50
51// Flags
52const FLAG_IS_BOUND: u8 = 0x01;
53const FLAG_HAS_MUTABLE: u8 = 0x02;
54
55/// Access level when opening a .bin file.
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub enum AccessLevel {
58    /// Opened with machineID - can only read and validate.
59    ReadOnly,
60    /// Opened with secret - full access including generation.
61    FullAccess,
62}
63
64/// Configuration stored in a .bin file.
65#[derive(Clone, Debug, Serialize, Deserialize)]
66pub struct BinConfig {
67    /// Name/identifier for this configuration
68    pub name: String,
69    /// Alphabet for code generation
70    pub alphabet: Alphabet,
71    /// Number of random characters (not including check digit)
72    pub code_length: usize,
73    /// Position of check digit (index-based)
74    pub check_position: CheckPosition,
75    /// 256-bit secret key for HMAC (hex encoded)
76    pub secret_key: String,
77    /// 256-bit storage key for database encryption (hex encoded)
78    #[serde(default, skip_serializing_if = "String::is_empty")]
79    pub storage_key: String,
80    /// Whether storage encryption is enabled
81    #[serde(default)]
82    pub storage_encryption_enabled: bool,
83    /// Damm table for check digit calculation
84    pub damm_table: DammTable,
85    /// Counter storage mode
86    pub counter_mode: CounterMode,
87    /// Code formatting (prefix, suffix, separators)
88    #[serde(default, skip_serializing_if = "CodeFormat::is_default")]
89    pub format: CodeFormat,
90    /// Audit trail
91    pub audit: AuditInfo,
92}
93
94impl CodeFormat {
95    /// Check if this is the default (empty) format.
96    pub fn is_default(&self) -> bool {
97        self.prefix.is_none()
98            && self.suffix.is_none()
99            && self.separator.is_none()
100            && self.separator_positions.is_empty()
101    }
102}
103
104impl BinConfig {
105    /// Get the base code length (code_length + 1 for check digit).
106    pub fn base_code_length(&self) -> usize {
107        self.code_length + 1
108    }
109
110    /// Get the total formatted code length.
111    pub fn total_code_length(&self) -> usize {
112        self.format.total_length(self.base_code_length())
113    }
114}
115
116/// Handle to an opened .bin file.
117pub struct BinFile {
118    /// File path (None if in-memory)
119    path: Option<PathBuf>,
120    /// Raw file data (for in-memory operations)
121    raw_data: Option<Vec<u8>>,
122    /// Configuration
123    config: BinConfig,
124    /// 32-byte data key (decrypted)
125    data_key: [u8; 32],
126    /// 32-byte secret key for HMAC
127    secret_key: [u8; 32],
128    /// 32-byte storage key for database encryption
129    storage_key: [u8; 32],
130    /// Counter manager
131    counter: CounterManager,
132    /// Access level
133    access_level: AccessLevel,
134    /// Salt from file
135    salt: [u8; SALT_SIZE],
136    /// Is the file bound to a machine?
137    is_bound: bool,
138    /// Has mutable section?
139    has_mutable: bool,
140    /// Current machine ID (cached)
141    current_machine_id: Option<[u8; 32]>,
142}
143
144impl BinFile {
145    // ==================== Creation ====================
146
147    /// Create a new .bin file.
148    ///
149    /// # Arguments
150    /// * `path` - File path for the new .bin file
151    /// * `secret` - Secret password for accessing the file
152    /// * `config` - Configuration for the .bin file
153    ///
154    /// # Example
155    ///
156    /// ```no_run
157    /// use promocrypt_core::{BinFile, BinConfig, Alphabet, CheckPosition, CounterMode, AuditInfo, CodeFormat};
158    ///
159    /// let config = BinConfig {
160    ///     name: "production".to_string(),
161    ///     alphabet: Alphabet::default_alphabet(),
162    ///     code_length: 9,
163    ///     check_position: CheckPosition::End,
164    ///     secret_key: String::new(), // Will be generated
165    ///     storage_key: String::new(), // Will be generated
166    ///     storage_encryption_enabled: false,
167    ///     damm_table: promocrypt_core::DammTable::new(26),
168    ///     counter_mode: CounterMode::Manual,
169    ///     format: CodeFormat::default(),
170    ///     audit: AuditInfo::default(),
171    /// };
172    ///
173    /// let bin = BinFile::create("production.bin", "my-secret", config).unwrap();
174    /// ```
175    pub fn create(path: impl AsRef<Path>, secret: &str, mut config: BinConfig) -> Result<Self> {
176        let path = path.as_ref();
177
178        // Get machine ID
179        let machine_id = get_machine_id()?;
180
181        // Generate random keys
182        let data_key = generate_random_key()?;
183        let secret_key = generate_random_key()?;
184        let storage_key = generate_random_key()?;
185        let salt = generate_salt()?;
186
187        // Set keys in config
188        config.secret_key = hex::encode(secret_key);
189        config.storage_key = hex::encode(storage_key);
190
191        // Update audit
192        config.audit = AuditInfo::new(&machine_id);
193
194        // Serialize config to JSON
195        let config_json = serde_json::to_vec(&config)?;
196
197        // Generate nonces
198        let machine_nonce = generate_nonce()?;
199        let secret_nonce = generate_nonce()?;
200        let data_nonce = generate_nonce()?;
201
202        // Encrypt data_key for machine and secret
203        let machine_encrypted_key =
204            encrypt_data_key(&data_key, &machine_id, &salt, &machine_nonce)?;
205        let secret_encrypted_key =
206            encrypt_data_key(&data_key, secret.as_bytes(), &salt, &secret_nonce)?;
207
208        // Encrypt config
209        let encrypted_data = encrypt_data(&data_key, &config_json, &data_nonce)?;
210
211        // Build file
212        let flags = FLAG_IS_BOUND; // Bound to creation machine
213        let has_mutable = config.counter_mode.is_in_bin();
214
215        let mut file_data = Vec::new();
216
217        // Header
218        file_data.extend_from_slice(MAGIC);
219        file_data.push(VERSION);
220        file_data.push(flags | if has_mutable { FLAG_HAS_MUTABLE } else { 0 });
221
222        // CRC placeholder (4 bytes)
223        let crc_offset = file_data.len();
224        file_data.extend_from_slice(&[0u8; 4]);
225
226        // Salt and encrypted keys
227        file_data.extend_from_slice(&salt);
228        file_data.extend_from_slice(&machine_nonce);
229        file_data.extend_from_slice(&machine_encrypted_key);
230        file_data.extend_from_slice(&secret_nonce);
231        file_data.extend_from_slice(&secret_encrypted_key);
232
233        // Encrypted data
234        file_data.extend_from_slice(&data_nonce);
235        file_data.extend_from_slice(&(encrypted_data.len() as u32).to_le_bytes());
236        file_data.extend_from_slice(&encrypted_data);
237
238        // Mutable section (if needed)
239        if has_mutable {
240            let mutable_nonce = generate_nonce()?;
241            let counter_bytes = counter_to_bytes(0);
242            let encrypted_counter = encrypt_data(&data_key, &counter_bytes, &mutable_nonce)?;
243
244            file_data.extend_from_slice(&mutable_nonce);
245            file_data.extend_from_slice(&(encrypted_counter.len() as u32).to_le_bytes());
246            file_data.extend_from_slice(&encrypted_counter);
247        }
248
249        // Calculate and write CRC
250        let crc = crc32fast::hash(&file_data[0..10]);
251        file_data[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
252
253        // Write file
254        let mut file = File::create(path)?;
255        file.write_all(&file_data)?;
256        file.sync_all()?;
257
258        // Create counter manager
259        let counter = CounterManager::new(config.counter_mode.clone());
260
261        Ok(Self {
262            path: Some(path.to_path_buf()),
263            raw_data: Some(file_data), // Cache the raw data for save() method
264            config,
265            data_key,
266            secret_key,
267            storage_key,
268            counter,
269            access_level: AccessLevel::FullAccess,
270            salt,
271            is_bound: true,
272            has_mutable,
273            current_machine_id: Some(machine_id),
274        })
275    }
276
277    /// Create .bin in memory (for database storage).
278    pub fn create_in_memory(secret: &str, config: BinConfig) -> Result<Vec<u8>> {
279        // Create a temporary file path-like name
280        let temp_path =
281            std::env::temp_dir().join(format!("promocrypt_{}.bin", rand::random::<u64>()));
282        let bin = Self::create(&temp_path, secret, config)?;
283
284        // Read back the file content
285        let data = std::fs::read(&temp_path)?;
286
287        // Clean up temp file
288        std::fs::remove_file(&temp_path).ok();
289
290        drop(bin);
291        Ok(data)
292    }
293
294    // ==================== Opening ====================
295
296    /// Open with machineID (read-only access).
297    pub fn open_readonly(path: impl AsRef<Path>) -> Result<Self> {
298        let path = path.as_ref();
299        let data = std::fs::read(path)
300            .map_err(|_| PromocryptError::FileNotFound(path.display().to_string()))?;
301
302        let machine_id = get_machine_id()?;
303        Self::open_from_bytes_internal(&data, Some(&machine_id), None, Some(path.to_path_buf()))
304    }
305
306    /// Open with secret (full access).
307    pub fn open_with_secret(path: impl AsRef<Path>, secret: &str) -> Result<Self> {
308        let path = path.as_ref();
309        let data = std::fs::read(path)
310            .map_err(|_| PromocryptError::FileNotFound(path.display().to_string()))?;
311
312        // Get machine ID for history tracking (optional - don't fail if unavailable)
313        let machine_id = get_machine_id().ok();
314        Self::open_from_bytes_internal(
315            &data,
316            machine_id.as_ref(),
317            Some(secret),
318            Some(path.to_path_buf()),
319        )
320    }
321
322    /// Open from bytes with machineID (read-only access).
323    pub fn from_bytes_readonly(data: &[u8]) -> Result<Self> {
324        let machine_id = get_machine_id()?;
325        Self::open_from_bytes_internal(data, Some(&machine_id), None, None)
326    }
327
328    /// Open from bytes with secret (full access).
329    pub fn from_bytes_with_secret(data: &[u8], secret: &str) -> Result<Self> {
330        // Get machine ID for history tracking (optional - don't fail if unavailable)
331        let machine_id = get_machine_id().ok();
332        Self::open_from_bytes_internal(data, machine_id.as_ref(), Some(secret), None)
333    }
334
335    /// Internal method to open from bytes.
336    fn open_from_bytes_internal(
337        data: &[u8],
338        machine_id: Option<&[u8; 32]>,
339        secret: Option<&str>,
340        path: Option<PathBuf>,
341    ) -> Result<Self> {
342        // Validate minimum size
343        if data.len() < HEADER_SIZE {
344            return Err(PromocryptError::InvalidFileFormat);
345        }
346
347        // Check magic
348        if &data[OFFSET_MAGIC..OFFSET_MAGIC + 8] != MAGIC {
349            return Err(PromocryptError::InvalidFileFormat);
350        }
351
352        // Check version
353        let version = data[OFFSET_VERSION];
354        if version != VERSION {
355            return Err(PromocryptError::UnsupportedVersion(version));
356        }
357
358        // Get flags
359        let flags = data[OFFSET_FLAGS];
360        let is_bound = (flags & FLAG_IS_BOUND) != 0;
361        let has_mutable = (flags & FLAG_HAS_MUTABLE) != 0;
362
363        // Verify CRC
364        let stored_crc = u32::from_le_bytes([
365            data[OFFSET_HEADER_CRC],
366            data[OFFSET_HEADER_CRC + 1],
367            data[OFFSET_HEADER_CRC + 2],
368            data[OFFSET_HEADER_CRC + 3],
369        ]);
370        let computed_crc = crc32fast::hash(&data[0..10]);
371        if stored_crc != computed_crc {
372            return Err(PromocryptError::HeaderChecksumMismatch);
373        }
374
375        // Extract salt
376        let mut salt = [0u8; SALT_SIZE];
377        salt.copy_from_slice(&data[OFFSET_SALT..OFFSET_SALT + SALT_SIZE]);
378
379        // Determine access level and decrypt data_key
380        let (data_key, access_level) = if let Some(secret) = secret {
381            // Try secret key
382            let mut secret_nonce = [0u8; NONCE_SIZE];
383            secret_nonce
384                .copy_from_slice(&data[OFFSET_SECRET_NONCE..OFFSET_SECRET_NONCE + NONCE_SIZE]);
385
386            let mut secret_encrypted = [0u8; ENCRYPTED_KEY_SIZE];
387            secret_encrypted
388                .copy_from_slice(&data[OFFSET_SECRET_KEY..OFFSET_SECRET_KEY + ENCRYPTED_KEY_SIZE]);
389
390            let data_key =
391                decrypt_data_key(&secret_encrypted, secret.as_bytes(), &salt, &secret_nonce)?;
392            (data_key, AccessLevel::FullAccess)
393        } else if let Some(machine_id) = machine_id {
394            // Try machine key
395            if !is_bound {
396                return Err(PromocryptError::MachineMismatch);
397            }
398
399            let mut machine_nonce = [0u8; NONCE_SIZE];
400            machine_nonce
401                .copy_from_slice(&data[OFFSET_MACHINE_NONCE..OFFSET_MACHINE_NONCE + NONCE_SIZE]);
402
403            let mut machine_encrypted = [0u8; ENCRYPTED_KEY_SIZE];
404            machine_encrypted.copy_from_slice(
405                &data[OFFSET_MACHINE_KEY..OFFSET_MACHINE_KEY + ENCRYPTED_KEY_SIZE],
406            );
407
408            let data_key = decrypt_data_key(&machine_encrypted, machine_id, &salt, &machine_nonce)
409                .map_err(|_| PromocryptError::MachineMismatch)?;
410            (data_key, AccessLevel::ReadOnly)
411        } else {
412            return Err(PromocryptError::InvalidArgument(
413                "Either machine_id or secret must be provided".to_string(),
414            ));
415        };
416
417        // Decrypt config
418        let mut data_nonce = [0u8; NONCE_SIZE];
419        data_nonce.copy_from_slice(&data[OFFSET_DATA_NONCE..OFFSET_DATA_NONCE + NONCE_SIZE]);
420
421        let data_length = u32::from_le_bytes([
422            data[OFFSET_DATA_LENGTH],
423            data[OFFSET_DATA_LENGTH + 1],
424            data[OFFSET_DATA_LENGTH + 2],
425            data[OFFSET_DATA_LENGTH + 3],
426        ]) as usize;
427
428        let data_end = OFFSET_DATA + data_length;
429        if data.len() < data_end {
430            return Err(PromocryptError::InvalidFileFormat);
431        }
432
433        let encrypted_config = &data[OFFSET_DATA..data_end];
434        let config_json = decrypt_data(&data_key, encrypted_config, &data_nonce)?;
435        let config: BinConfig = serde_json::from_slice(&config_json)?;
436
437        // Parse secret key
438        let secret_key_bytes = hex::decode(&config.secret_key).map_err(|_| {
439            PromocryptError::InvalidFileFormatDetails("Invalid secret key hex".to_string())
440        })?;
441        if secret_key_bytes.len() != 32 {
442            return Err(PromocryptError::InvalidFileFormatDetails(
443                "Secret key must be 32 bytes".to_string(),
444            ));
445        }
446        let mut secret_key = [0u8; 32];
447        secret_key.copy_from_slice(&secret_key_bytes);
448
449        // Parse storage key (may be empty for older files)
450        let mut storage_key = [0u8; 32];
451        if !config.storage_key.is_empty() {
452            let storage_key_bytes = hex::decode(&config.storage_key).map_err(|_| {
453                PromocryptError::InvalidFileFormatDetails("Invalid storage key hex".to_string())
454            })?;
455            if storage_key_bytes.len() == 32 {
456                storage_key.copy_from_slice(&storage_key_bytes);
457            }
458        }
459
460        // Read counter from mutable section if present
461        let mut counter_value = 0u64;
462        if has_mutable && data.len() > data_end {
463            let mutable_offset = data_end;
464            if data.len() >= mutable_offset + NONCE_SIZE + 4 {
465                let mut mutable_nonce = [0u8; NONCE_SIZE];
466                mutable_nonce.copy_from_slice(&data[mutable_offset..mutable_offset + NONCE_SIZE]);
467
468                let mutable_length = u32::from_le_bytes([
469                    data[mutable_offset + NONCE_SIZE],
470                    data[mutable_offset + NONCE_SIZE + 1],
471                    data[mutable_offset + NONCE_SIZE + 2],
472                    data[mutable_offset + NONCE_SIZE + 3],
473                ]) as usize;
474
475                let mutable_start = mutable_offset + NONCE_SIZE + 4;
476                let mutable_end = mutable_start + mutable_length;
477
478                if data.len() >= mutable_end {
479                    let encrypted_counter = &data[mutable_start..mutable_end];
480                    if let Ok(counter_bytes) =
481                        decrypt_data(&data_key, encrypted_counter, &mutable_nonce)
482                        && let Ok(val) = counter_from_bytes(&counter_bytes)
483                    {
484                        counter_value = val;
485                    }
486                }
487            }
488        }
489
490        let counter = CounterManager::with_in_bin_value(config.counter_mode.clone(), counter_value);
491
492        Ok(Self {
493            path,
494            raw_data: Some(data.to_vec()),
495            config,
496            data_key,
497            secret_key,
498            storage_key,
499            counter,
500            access_level,
501            salt,
502            is_bound,
503            has_mutable,
504            current_machine_id: machine_id.copied(),
505        })
506    }
507
508    /// Try to open with machineID, returns None on any error.
509    pub fn try_open_readonly(path: impl AsRef<Path>) -> Option<Self> {
510        Self::open_readonly(path).ok()
511    }
512
513    /// Try to open with secret, returns None on any error.
514    pub fn try_open_with_secret(path: impl AsRef<Path>, secret: &str) -> Option<Self> {
515        Self::open_with_secret(path, secret).ok()
516    }
517
518    // ==================== Getters ====================
519
520    /// Get configuration (both access levels).
521    pub fn config(&self) -> &BinConfig {
522        &self.config
523    }
524
525    /// Get name.
526    pub fn name(&self) -> &str {
527        &self.config.name
528    }
529
530    /// Get alphabet.
531    pub fn alphabet(&self) -> &Alphabet {
532        &self.config.alphabet
533    }
534
535    /// Get code length (without check digit).
536    pub fn code_length(&self) -> usize {
537        self.config.code_length
538    }
539
540    /// Get total code length (with check digit).
541    pub fn total_length(&self) -> usize {
542        self.config.code_length + 1
543    }
544
545    /// Get check position.
546    pub fn check_position(&self) -> CheckPosition {
547        self.config.check_position
548    }
549
550    /// Get counter mode.
551    pub fn counter_mode(&self) -> &CounterMode {
552        &self.config.counter_mode
553    }
554
555    /// Get audit info (both access levels).
556    pub fn audit(&self) -> &AuditInfo {
557        &self.config.audit
558    }
559
560    /// Get access level.
561    pub fn access_level(&self) -> AccessLevel {
562        self.access_level
563    }
564
565    /// Check if file is bound to a machine.
566    pub fn is_bound(&self) -> bool {
567        self.is_bound
568    }
569
570    /// Get code format.
571    pub fn format(&self) -> &CodeFormat {
572        &self.config.format
573    }
574
575    /// Check if storage encryption is enabled.
576    pub fn is_storage_encryption_enabled(&self) -> bool {
577        self.config.storage_encryption_enabled
578    }
579
580    /// Get history.
581    pub fn get_history(&self) -> &History {
582        &self.config.audit.history
583    }
584
585    /// Get generation log.
586    pub fn get_generation_log(&self) -> &[GenerationLogEntry] {
587        self.config.audit.get_generation_log()
588    }
589
590    /// Get total codes generated.
591    pub fn total_codes_generated(&self) -> u64 {
592        self.config.audit.generation_count
593    }
594
595    // ==================== Validation ====================
596
597    /// Validate a code (both access levels).
598    /// Handles formatted codes by stripping prefix/suffix/separators.
599    pub fn validate(&self, code: &str) -> ValidationResult {
600        // Strip formatting if present
601        let base_code = if self.config.format.has_formatting() {
602            match self.config.format.strip(code) {
603                Some(c) => c,
604                None => {
605                    return ValidationResult::InvalidFormat {
606                        reason: "prefix or suffix mismatch".to_string(),
607                    };
608                }
609            }
610        } else {
611            code.to_string()
612        };
613
614        crate::validator::validate(
615            &base_code,
616            &self.config.alphabet,
617            self.total_length(),
618            &self.config.damm_table,
619        )
620    }
621
622    /// Quick validation check.
623    pub fn is_valid(&self, code: &str) -> bool {
624        self.validate(code).is_valid()
625    }
626
627    /// Validate multiple codes at once.
628    pub fn validate_batch(&self, codes: &[&str]) -> Vec<ValidationResult> {
629        codes.iter().map(|c| self.validate(c)).collect()
630    }
631
632    // ==================== Generation ====================
633
634    /// Generate a single code (requires FullAccess).
635    /// Returns formatted code with prefix/suffix/separators if configured.
636    /// If storage_encryption_enabled, returns encrypted code.
637    pub fn generate(&mut self) -> Result<String> {
638        self.require_full_access()?;
639
640        let counter = self.counter.reserve(1)?;
641        let base_code = generate_code(
642            &self.secret_key,
643            counter,
644            &self.config.alphabet,
645            self.config.code_length,
646            self.config.check_position,
647            &self.config.damm_table,
648        );
649
650        // Apply formatting
651        let formatted = self.config.format.format(&base_code);
652
653        // Record generation with log
654        if let Some(machine_id) = &self.current_machine_id {
655            self.config
656                .audit
657                .record_generation_with_log(machine_id, 1, counter);
658        } else {
659            self.config.audit.record_generation(1);
660        }
661        self.save_if_needed()?;
662
663        // Apply storage encryption if enabled
664        if self.config.storage_encryption_enabled {
665            encrypt_code_for_storage(&self.storage_key, &formatted)
666        } else {
667            Ok(formatted)
668        }
669    }
670
671    /// Generate batch of codes (requires FullAccess).
672    pub fn generate_batch(&mut self, count: usize) -> Result<Vec<String>> {
673        self.require_full_access()?;
674
675        let start_counter = self.counter.reserve(count as u64)?;
676        let base_codes = generate_batch(
677            &self.secret_key,
678            start_counter,
679            count,
680            &self.config.alphabet,
681            self.config.code_length,
682            self.config.check_position,
683            &self.config.damm_table,
684        );
685
686        // Apply formatting
687        let formatted: Vec<String> = base_codes
688            .iter()
689            .map(|c| self.config.format.format(c))
690            .collect();
691
692        // Record generation with log
693        if let Some(machine_id) = &self.current_machine_id {
694            self.config
695                .audit
696                .record_generation_with_log(machine_id, count as u64, start_counter);
697        } else {
698            self.config.audit.record_generation(count as u64);
699        }
700        self.save_if_needed()?;
701
702        // Apply storage encryption if enabled
703        if self.config.storage_encryption_enabled {
704            let code_refs: Vec<&str> = formatted.iter().map(|s| s.as_str()).collect();
705            crate::encryption::encrypt_codes_for_storage(&self.storage_key, &code_refs)
706        } else {
707            Ok(formatted)
708        }
709    }
710
711    /// Generate code at specific counter (doesn't update counter).
712    pub fn generate_at(&self, counter: u64) -> Result<String> {
713        self.require_full_access()?;
714
715        let base_code = generate_code(
716            &self.secret_key,
717            counter,
718            &self.config.alphabet,
719            self.config.code_length,
720            self.config.check_position,
721            &self.config.damm_table,
722        );
723
724        Ok(self.config.format.format(&base_code))
725    }
726
727    /// Generate batch at specific counter (doesn't update counter).
728    pub fn generate_batch_at(&self, start_counter: u64, count: usize) -> Result<Vec<String>> {
729        self.require_full_access()?;
730
731        let base_codes = generate_batch(
732            &self.secret_key,
733            start_counter,
734            count,
735            &self.config.alphabet,
736            self.config.code_length,
737            self.config.check_position,
738            &self.config.damm_table,
739        );
740
741        Ok(base_codes
742            .iter()
743            .map(|c| self.config.format.format(c))
744            .collect())
745    }
746
747    // ==================== Counter ====================
748
749    /// Get current counter value.
750    pub fn get_counter(&self) -> Result<u64> {
751        self.counter.get()
752    }
753
754    // ==================== Mastering ====================
755
756    /// Master for a new machine (requires FullAccess).
757    pub fn master_for_machine(
758        &self,
759        output_path: impl AsRef<Path>,
760        target_machine_id: &[u8; 32],
761    ) -> Result<()> {
762        self.require_full_access()?;
763
764        let path = output_path.as_ref();
765
766        // Update config with new mastering info
767        let mut new_config = self.config.clone();
768        new_config.audit.record_master(target_machine_id);
769
770        // Regenerate file with new machine binding
771        let config_json = serde_json::to_vec(&new_config)?;
772
773        // Generate new nonces
774        let machine_nonce = generate_nonce()?;
775        let data_nonce = generate_nonce()?;
776
777        // Encrypt data_key for new machine
778        let machine_encrypted_key = encrypt_data_key(
779            &self.data_key,
780            target_machine_id,
781            &self.salt,
782            &machine_nonce,
783        )?;
784
785        // Keep existing secret encrypted key from raw data
786        let raw_data = self.raw_data.as_ref().ok_or_else(|| {
787            PromocryptError::InvalidArgument("No raw data available for mastering".to_string())
788        })?;
789
790        let mut secret_nonce = [0u8; NONCE_SIZE];
791        secret_nonce
792            .copy_from_slice(&raw_data[OFFSET_SECRET_NONCE..OFFSET_SECRET_NONCE + NONCE_SIZE]);
793
794        let mut secret_encrypted = [0u8; ENCRYPTED_KEY_SIZE];
795        secret_encrypted
796            .copy_from_slice(&raw_data[OFFSET_SECRET_KEY..OFFSET_SECRET_KEY + ENCRYPTED_KEY_SIZE]);
797
798        // Encrypt config
799        let encrypted_data = encrypt_data(&self.data_key, &config_json, &data_nonce)?;
800
801        // Build file
802        let flags = FLAG_IS_BOUND
803            | if self.has_mutable {
804                FLAG_HAS_MUTABLE
805            } else {
806                0
807            };
808
809        let mut file_data = Vec::new();
810
811        // Header
812        file_data.extend_from_slice(MAGIC);
813        file_data.push(VERSION);
814        file_data.push(flags);
815
816        // CRC placeholder
817        let crc_offset = file_data.len();
818        file_data.extend_from_slice(&[0u8; 4]);
819
820        // Salt and keys
821        file_data.extend_from_slice(&self.salt);
822        file_data.extend_from_slice(&machine_nonce);
823        file_data.extend_from_slice(&machine_encrypted_key);
824        file_data.extend_from_slice(&secret_nonce);
825        file_data.extend_from_slice(&secret_encrypted);
826
827        // Encrypted data
828        file_data.extend_from_slice(&data_nonce);
829        file_data.extend_from_slice(&(encrypted_data.len() as u32).to_le_bytes());
830        file_data.extend_from_slice(&encrypted_data);
831
832        // Mutable section (if needed)
833        if self.has_mutable {
834            let mutable_nonce = generate_nonce()?;
835            let counter_bytes = counter_to_bytes(self.counter.in_bin_value());
836            let encrypted_counter = encrypt_data(&self.data_key, &counter_bytes, &mutable_nonce)?;
837
838            file_data.extend_from_slice(&mutable_nonce);
839            file_data.extend_from_slice(&(encrypted_counter.len() as u32).to_le_bytes());
840            file_data.extend_from_slice(&encrypted_counter);
841        }
842
843        // Calculate and write CRC
844        let crc = crc32fast::hash(&file_data[0..10]);
845        file_data[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
846
847        // Write file
848        let mut file = File::create(path)?;
849        file.write_all(&file_data)?;
850        file.sync_all()?;
851
852        Ok(())
853    }
854
855    /// Export unbound copy (requires FullAccess).
856    pub fn export_unbound(&self, output_path: impl AsRef<Path>) -> Result<()> {
857        self.require_full_access()?;
858
859        let path = output_path.as_ref();
860
861        // Create config without machine binding
862        let mut new_config = self.config.clone();
863        new_config.audit.mastered_at = None;
864        new_config.audit.mastered_for = None;
865
866        let config_json = serde_json::to_vec(&new_config)?;
867
868        // Generate new nonces
869        let data_nonce = generate_nonce()?;
870
871        // Keep existing secret encrypted key
872        let raw_data = self
873            .raw_data
874            .as_ref()
875            .ok_or_else(|| PromocryptError::InvalidArgument("No raw data available".to_string()))?;
876
877        let mut secret_nonce = [0u8; NONCE_SIZE];
878        secret_nonce
879            .copy_from_slice(&raw_data[OFFSET_SECRET_NONCE..OFFSET_SECRET_NONCE + NONCE_SIZE]);
880
881        let mut secret_encrypted = [0u8; ENCRYPTED_KEY_SIZE];
882        secret_encrypted
883            .copy_from_slice(&raw_data[OFFSET_SECRET_KEY..OFFSET_SECRET_KEY + ENCRYPTED_KEY_SIZE]);
884
885        // Encrypt config
886        let encrypted_data = encrypt_data(&self.data_key, &config_json, &data_nonce)?;
887
888        // Build file (no machine key, not bound)
889        let flags = if self.has_mutable {
890            FLAG_HAS_MUTABLE
891        } else {
892            0
893        };
894
895        let mut file_data = Vec::new();
896
897        // Header
898        file_data.extend_from_slice(MAGIC);
899        file_data.push(VERSION);
900        file_data.push(flags);
901
902        // CRC placeholder
903        let crc_offset = file_data.len();
904        file_data.extend_from_slice(&[0u8; 4]);
905
906        // Salt and keys (empty machine key)
907        file_data.extend_from_slice(&self.salt);
908        file_data.extend_from_slice(&[0u8; NONCE_SIZE]); // Empty machine nonce
909        file_data.extend_from_slice(&[0u8; ENCRYPTED_KEY_SIZE]); // Empty machine key
910        file_data.extend_from_slice(&secret_nonce);
911        file_data.extend_from_slice(&secret_encrypted);
912
913        // Encrypted data
914        file_data.extend_from_slice(&data_nonce);
915        file_data.extend_from_slice(&(encrypted_data.len() as u32).to_le_bytes());
916        file_data.extend_from_slice(&encrypted_data);
917
918        // Calculate and write CRC
919        let crc = crc32fast::hash(&file_data[0..10]);
920        file_data[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
921
922        // Write file
923        let mut file = File::create(path)?;
924        file.write_all(&file_data)?;
925        file.sync_all()?;
926
927        Ok(())
928    }
929
930    // ==================== Storage Encryption ====================
931
932    /// Encrypt a code for database storage (requires FullAccess).
933    pub fn encrypt_code(&self, code: &str) -> Result<String> {
934        self.require_full_access()?;
935        encrypt_code_for_storage(&self.storage_key, code)
936    }
937
938    /// Decrypt a code from database storage (requires FullAccess).
939    pub fn decrypt_code(&self, encrypted: &str) -> Result<String> {
940        self.require_full_access()?;
941        decrypt_code_from_storage(&self.storage_key, encrypted)
942    }
943
944    /// Encrypt multiple codes for storage (requires FullAccess).
945    pub fn encrypt_codes(&self, codes: &[&str]) -> Result<Vec<String>> {
946        self.require_full_access()?;
947        crate::encryption::encrypt_codes_for_storage(&self.storage_key, codes)
948    }
949
950    /// Decrypt multiple codes from storage (requires FullAccess).
951    pub fn decrypt_codes(&self, encrypted: &[&str]) -> Result<Vec<String>> {
952        self.require_full_access()?;
953        crate::encryption::decrypt_codes_from_storage(&self.storage_key, encrypted)
954    }
955
956    /// Enable or disable storage encryption (requires FullAccess).
957    pub fn set_storage_encryption(&mut self, enabled: bool) -> Result<()> {
958        self.require_full_access()?;
959
960        let old_value = self.config.storage_encryption_enabled.to_string();
961        let new_value = enabled.to_string();
962
963        if let Some(machine_id) = &self.current_machine_id {
964            self.config.audit.history.record_config_change(
965                machine_id,
966                "storage_encryption_enabled",
967                Some(&old_value),
968                Some(&new_value),
969            );
970        }
971
972        self.config.storage_encryption_enabled = enabled;
973        Ok(())
974    }
975
976    // ==================== Configuration ====================
977
978    /// Update counter mode (requires FullAccess).
979    pub fn set_counter_mode(&mut self, mode: CounterMode) -> Result<()> {
980        self.require_full_access()?;
981
982        self.config.counter_mode = mode.clone();
983        self.counter.set_mode(mode);
984        self.save_if_needed()?;
985
986        Ok(())
987    }
988
989    /// Update code format (requires FullAccess).
990    pub fn set_format(&mut self, format: CodeFormat) -> Result<()> {
991        self.require_full_access()?;
992
993        if let Some(machine_id) = &self.current_machine_id {
994            self.config.audit.history.record_config_change(
995                machine_id,
996                "format",
997                Some(&serde_json::to_string(&self.config.format).unwrap_or_default()),
998                Some(&serde_json::to_string(&format).unwrap_or_default()),
999            );
1000        }
1001
1002        self.config.format = format;
1003        Ok(())
1004    }
1005
1006    /// Update check position (requires FullAccess).
1007    pub fn set_check_position(&mut self, position: CheckPosition) -> Result<()> {
1008        self.require_full_access()?;
1009
1010        if let Some(machine_id) = &self.current_machine_id {
1011            self.config.audit.history.record_config_change(
1012                machine_id,
1013                "check_position",
1014                Some(&self.config.check_position.to_string()),
1015                Some(&position.to_string()),
1016            );
1017        }
1018
1019        self.config.check_position = position;
1020        Ok(())
1021    }
1022
1023    /// Set code prefix (requires FullAccess).
1024    ///
1025    /// # Arguments
1026    /// * `prefix` - Optional prefix to prepend to generated codes
1027    pub fn set_prefix(&mut self, prefix: Option<String>) -> Result<()> {
1028        self.require_full_access()?;
1029
1030        let old_value = self.config.format.prefix.as_deref().map(|s| s.to_string());
1031
1032        if let Some(machine_id) = &self.current_machine_id {
1033            self.config.audit.history.record_config_change(
1034                machine_id,
1035                "prefix",
1036                old_value.as_deref(),
1037                prefix.as_deref(),
1038            );
1039        }
1040
1041        self.config.format.prefix = prefix;
1042        Ok(())
1043    }
1044
1045    /// Set code suffix (requires FullAccess).
1046    ///
1047    /// # Arguments
1048    /// * `suffix` - Optional suffix to append to generated codes
1049    pub fn set_suffix(&mut self, suffix: Option<String>) -> Result<()> {
1050        self.require_full_access()?;
1051
1052        let old_value = self.config.format.suffix.as_deref().map(|s| s.to_string());
1053
1054        if let Some(machine_id) = &self.current_machine_id {
1055            self.config.audit.history.record_config_change(
1056                machine_id,
1057                "suffix",
1058                old_value.as_deref(),
1059                suffix.as_deref(),
1060            );
1061        }
1062
1063        self.config.format.suffix = suffix;
1064        Ok(())
1065    }
1066
1067    /// Set separator character (requires FullAccess).
1068    ///
1069    /// # Arguments
1070    /// * `separator` - Optional separator character to insert at specified positions
1071    pub fn set_separator(&mut self, separator: Option<char>) -> Result<()> {
1072        self.require_full_access()?;
1073
1074        let old_value = self.config.format.separator.map(|c| c.to_string());
1075        let new_value = separator.map(|c| c.to_string());
1076
1077        if let Some(machine_id) = &self.current_machine_id {
1078            self.config.audit.history.record_config_change(
1079                machine_id,
1080                "separator",
1081                old_value.as_deref(),
1082                new_value.as_deref(),
1083            );
1084        }
1085
1086        self.config.format.separator = separator;
1087        Ok(())
1088    }
1089
1090    /// Set separator positions (requires FullAccess).
1091    ///
1092    /// # Arguments
1093    /// * `positions` - Positions where separator should be inserted (0-based, before the character)
1094    pub fn set_separator_positions(&mut self, positions: Vec<usize>) -> Result<()> {
1095        self.require_full_access()?;
1096
1097        let old_value = serde_json::to_string(&self.config.format.separator_positions).ok();
1098        let new_value = serde_json::to_string(&positions).ok();
1099
1100        if let Some(machine_id) = &self.current_machine_id {
1101            self.config.audit.history.record_config_change(
1102                machine_id,
1103                "separator_positions",
1104                old_value.as_deref(),
1105                new_value.as_deref(),
1106            );
1107        }
1108
1109        self.config.format.separator_positions = positions;
1110        Ok(())
1111    }
1112
1113    /// Save all configuration changes to the .bin file (requires FullAccess).
1114    ///
1115    /// This persists all in-memory configuration changes to disk, including:
1116    /// - Format changes (prefix, suffix, separator, separator_positions)
1117    /// - Check position changes
1118    /// - Storage encryption changes
1119    /// - Counter mode changes
1120    /// - History and generation log updates
1121    ///
1122    /// # Errors
1123    /// Returns an error if:
1124    /// - Access level is not FullAccess
1125    /// - No file path is associated (in-memory file)
1126    /// - IO error during write
1127    pub fn save(&mut self) -> Result<()> {
1128        self.require_full_access()?;
1129
1130        let path = self.path.as_ref().ok_or_else(|| {
1131            PromocryptError::InvalidArgument("Cannot save in-memory file without path".to_string())
1132        })?;
1133
1134        // Get existing raw_data to preserve machine encryption
1135        let raw_data = self.raw_data.as_ref().ok_or_else(|| {
1136            PromocryptError::InvalidArgument("No raw data available for save".to_string())
1137        })?;
1138
1139        // Serialize updated config
1140        let config_json = serde_json::to_vec(&self.config)?;
1141
1142        // Generate new data nonce
1143        let data_nonce = generate_nonce()?;
1144
1145        // Encrypt config
1146        let encrypted_data = encrypt_data(&self.data_key, &config_json, &data_nonce)?;
1147
1148        // Get machine encrypted key from raw data
1149        let mut machine_nonce = [0u8; NONCE_SIZE];
1150        machine_nonce
1151            .copy_from_slice(&raw_data[OFFSET_MACHINE_NONCE..OFFSET_MACHINE_NONCE + NONCE_SIZE]);
1152
1153        let mut machine_encrypted = [0u8; ENCRYPTED_KEY_SIZE];
1154        machine_encrypted.copy_from_slice(
1155            &raw_data[OFFSET_MACHINE_KEY..OFFSET_MACHINE_KEY + ENCRYPTED_KEY_SIZE],
1156        );
1157
1158        // Get secret encrypted key from raw data
1159        let mut secret_nonce = [0u8; NONCE_SIZE];
1160        secret_nonce
1161            .copy_from_slice(&raw_data[OFFSET_SECRET_NONCE..OFFSET_SECRET_NONCE + NONCE_SIZE]);
1162
1163        let mut secret_encrypted = [0u8; ENCRYPTED_KEY_SIZE];
1164        secret_encrypted
1165            .copy_from_slice(&raw_data[OFFSET_SECRET_KEY..OFFSET_SECRET_KEY + ENCRYPTED_KEY_SIZE]);
1166
1167        // Build file
1168        let flags = (if self.is_bound { FLAG_IS_BOUND } else { 0 })
1169            | (if self.has_mutable {
1170                FLAG_HAS_MUTABLE
1171            } else {
1172                0
1173            });
1174
1175        let mut file_data = Vec::new();
1176
1177        // Header
1178        file_data.extend_from_slice(MAGIC);
1179        file_data.push(VERSION);
1180        file_data.push(flags);
1181
1182        // CRC placeholder
1183        let crc_offset = file_data.len();
1184        file_data.extend_from_slice(&[0u8; 4]);
1185
1186        // Salt and keys
1187        file_data.extend_from_slice(&self.salt);
1188        file_data.extend_from_slice(&machine_nonce);
1189        file_data.extend_from_slice(&machine_encrypted);
1190        file_data.extend_from_slice(&secret_nonce);
1191        file_data.extend_from_slice(&secret_encrypted);
1192
1193        // Encrypted data
1194        file_data.extend_from_slice(&data_nonce);
1195        file_data.extend_from_slice(&(encrypted_data.len() as u32).to_le_bytes());
1196        file_data.extend_from_slice(&encrypted_data);
1197
1198        // Mutable section (if needed)
1199        if self.has_mutable {
1200            let mutable_nonce = generate_nonce()?;
1201            let counter_bytes = counter_to_bytes(self.counter.in_bin_value());
1202            let encrypted_counter = encrypt_data(&self.data_key, &counter_bytes, &mutable_nonce)?;
1203
1204            file_data.extend_from_slice(&mutable_nonce);
1205            file_data.extend_from_slice(&(encrypted_counter.len() as u32).to_le_bytes());
1206            file_data.extend_from_slice(&encrypted_counter);
1207        }
1208
1209        // Calculate and write CRC
1210        let crc = crc32fast::hash(&file_data[0..10]);
1211        file_data[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
1212
1213        // Write file
1214        let mut file = File::create(path)?;
1215        file.write_all(&file_data)?;
1216        file.sync_all()?;
1217
1218        // Update raw_data cache
1219        self.raw_data = Some(file_data);
1220
1221        // Mark counter as saved since we wrote the whole file
1222        if self.counter.is_in_bin_modified() {
1223            self.counter.mark_saved();
1224        }
1225
1226        Ok(())
1227    }
1228
1229    // ==================== Secret Rotation ====================
1230
1231    /// Rotate the secret password (requires FullAccess).
1232    /// Re-encrypts the data_key with the new secret.
1233    pub fn rotate_secret(&mut self, old_secret: &str, new_secret: &str) -> Result<()> {
1234        self.require_full_access()?;
1235
1236        // Verify old secret first (we should already have full access, but verify it matches)
1237        let path = self.path.as_ref().ok_or_else(|| {
1238            PromocryptError::InvalidArgument("Cannot rotate secret for in-memory file".to_string())
1239        })?;
1240
1241        // Read raw file to get current secret encrypted key
1242        let raw_data = self
1243            .raw_data
1244            .as_ref()
1245            .ok_or_else(|| PromocryptError::InvalidArgument("No raw data available".to_string()))?;
1246
1247        // Try to decrypt with old secret to verify it
1248        let mut secret_nonce = [0u8; NONCE_SIZE];
1249        secret_nonce
1250            .copy_from_slice(&raw_data[OFFSET_SECRET_NONCE..OFFSET_SECRET_NONCE + NONCE_SIZE]);
1251
1252        let mut secret_encrypted = [0u8; ENCRYPTED_KEY_SIZE];
1253        secret_encrypted
1254            .copy_from_slice(&raw_data[OFFSET_SECRET_KEY..OFFSET_SECRET_KEY + ENCRYPTED_KEY_SIZE]);
1255
1256        // Verify old secret
1257        let _verified_data_key = decrypt_data_key(
1258            &secret_encrypted,
1259            old_secret.as_bytes(),
1260            &self.salt,
1261            &secret_nonce,
1262        )
1263        .map_err(|_| PromocryptError::InvalidArgument("Old secret is incorrect".to_string()))?;
1264
1265        // Generate new nonce for secret encryption
1266        let new_secret_nonce = generate_nonce()?;
1267
1268        // Encrypt data_key with new secret
1269        let new_secret_encrypted = encrypt_data_key(
1270            &self.data_key,
1271            new_secret.as_bytes(),
1272            &self.salt,
1273            &new_secret_nonce,
1274        )?;
1275
1276        // Record rotation in history
1277        let old_hash = hash_secret(old_secret);
1278        let new_hash = hash_secret(new_secret);
1279
1280        if let Some(machine_id) = &self.current_machine_id {
1281            self.config
1282                .audit
1283                .history
1284                .record_secret_rotation(machine_id, &old_hash, &new_hash);
1285        }
1286
1287        // Rewrite the file with new secret encryption
1288        let config_json = serde_json::to_vec(&self.config)?;
1289        let data_nonce = generate_nonce()?;
1290        let encrypted_data = encrypt_data(&self.data_key, &config_json, &data_nonce)?;
1291
1292        // Get machine encrypted key from raw data
1293        let mut machine_nonce = [0u8; NONCE_SIZE];
1294        machine_nonce
1295            .copy_from_slice(&raw_data[OFFSET_MACHINE_NONCE..OFFSET_MACHINE_NONCE + NONCE_SIZE]);
1296
1297        let mut machine_encrypted = [0u8; ENCRYPTED_KEY_SIZE];
1298        machine_encrypted.copy_from_slice(
1299            &raw_data[OFFSET_MACHINE_KEY..OFFSET_MACHINE_KEY + ENCRYPTED_KEY_SIZE],
1300        );
1301
1302        // Build file
1303        let flags = (if self.is_bound { FLAG_IS_BOUND } else { 0 })
1304            | (if self.has_mutable {
1305                FLAG_HAS_MUTABLE
1306            } else {
1307                0
1308            });
1309
1310        let mut file_data = Vec::new();
1311
1312        // Header
1313        file_data.extend_from_slice(MAGIC);
1314        file_data.push(VERSION);
1315        file_data.push(flags);
1316
1317        // CRC placeholder
1318        let crc_offset = file_data.len();
1319        file_data.extend_from_slice(&[0u8; 4]);
1320
1321        // Salt and keys
1322        file_data.extend_from_slice(&self.salt);
1323        file_data.extend_from_slice(&machine_nonce);
1324        file_data.extend_from_slice(&machine_encrypted);
1325        file_data.extend_from_slice(&new_secret_nonce);
1326        file_data.extend_from_slice(&new_secret_encrypted);
1327
1328        // Encrypted data
1329        file_data.extend_from_slice(&data_nonce);
1330        file_data.extend_from_slice(&(encrypted_data.len() as u32).to_le_bytes());
1331        file_data.extend_from_slice(&encrypted_data);
1332
1333        // Mutable section (if needed)
1334        if self.has_mutable {
1335            let mutable_nonce = generate_nonce()?;
1336            let counter_bytes = counter_to_bytes(self.counter.in_bin_value());
1337            let encrypted_counter = encrypt_data(&self.data_key, &counter_bytes, &mutable_nonce)?;
1338
1339            file_data.extend_from_slice(&mutable_nonce);
1340            file_data.extend_from_slice(&(encrypted_counter.len() as u32).to_le_bytes());
1341            file_data.extend_from_slice(&encrypted_counter);
1342        }
1343
1344        // Calculate and write CRC
1345        let crc = crc32fast::hash(&file_data[0..10]);
1346        file_data[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
1347
1348        // Write file
1349        let mut file = File::create(path)?;
1350        file.write_all(&file_data)?;
1351        file.sync_all()?;
1352
1353        // Update raw_data
1354        self.raw_data = Some(file_data);
1355
1356        Ok(())
1357    }
1358
1359    // ==================== History & Logs ====================
1360
1361    /// Export history as JSON.
1362    pub fn export_history(&self) -> String {
1363        self.config.audit.export_history()
1364    }
1365
1366    /// Export generation log as JSON.
1367    pub fn export_generation_log(&self) -> String {
1368        self.config.audit.export_generation_log()
1369    }
1370
1371    /// Clear history (requires FullAccess).
1372    pub fn clear_history(&mut self, keep_last: Option<usize>) -> Result<()> {
1373        self.require_full_access()?;
1374        self.config.audit.clear_history(keep_last);
1375        Ok(())
1376    }
1377
1378    /// Clear generation log (requires FullAccess).
1379    pub fn clear_generation_log(&mut self, keep_last: Option<usize>) -> Result<()> {
1380        self.require_full_access()?;
1381        self.config.audit.clear_generation_log(keep_last);
1382        Ok(())
1383    }
1384
1385    // ==================== Statistics ====================
1386
1387    /// Get statistics about this .bin file.
1388    pub fn get_stats(&self) -> BinStats {
1389        BinStats::from_bin_file(self)
1390    }
1391
1392    // ==================== Internal ====================
1393
1394    fn require_full_access(&self) -> Result<()> {
1395        if self.access_level != AccessLevel::FullAccess {
1396            return Err(PromocryptError::SecretRequired);
1397        }
1398        Ok(())
1399    }
1400
1401    fn save_if_needed(&mut self) -> Result<()> {
1402        if self.counter.is_in_bin_modified() {
1403            if let Some(path) = &self.path {
1404                self.save_mutable_section(path)?;
1405            }
1406            self.counter.mark_saved();
1407        }
1408        Ok(())
1409    }
1410
1411    fn save_mutable_section(&self, path: &Path) -> Result<()> {
1412        if !self.has_mutable {
1413            return Ok(());
1414        }
1415
1416        // Read existing file
1417        let mut file = OpenOptions::new().read(true).write(true).open(path)?;
1418
1419        // Find mutable section offset
1420        let mut header = [0u8; HEADER_SIZE];
1421        file.read_exact(&mut header)?;
1422
1423        let data_length = u32::from_le_bytes([
1424            header[OFFSET_DATA_LENGTH],
1425            header[OFFSET_DATA_LENGTH + 1],
1426            header[OFFSET_DATA_LENGTH + 2],
1427            header[OFFSET_DATA_LENGTH + 3],
1428        ]) as usize;
1429
1430        let mutable_offset = OFFSET_DATA + data_length;
1431
1432        // Generate new mutable section
1433        let mutable_nonce = generate_nonce()?;
1434        let counter_bytes = counter_to_bytes(self.counter.in_bin_value());
1435        let encrypted_counter = encrypt_data(&self.data_key, &counter_bytes, &mutable_nonce)?;
1436
1437        // Write mutable section
1438        file.seek(SeekFrom::Start(mutable_offset as u64))?;
1439        file.write_all(&mutable_nonce)?;
1440        file.write_all(&(encrypted_counter.len() as u32).to_le_bytes())?;
1441        file.write_all(&encrypted_counter)?;
1442        file.sync_all()?;
1443
1444        Ok(())
1445    }
1446}
1447
1448/// Simple helper to create a BinConfig with defaults.
1449pub fn create_config(name: &str) -> BinConfig {
1450    let machine_id = get_machine_id().unwrap_or([0u8; 32]);
1451    let alphabet = Alphabet::default_alphabet();
1452    let damm_table = DammTable::new(alphabet.len());
1453
1454    BinConfig {
1455        name: name.to_string(),
1456        alphabet,
1457        code_length: 9,
1458        check_position: CheckPosition::End,
1459        secret_key: String::new(),
1460        storage_key: String::new(),
1461        storage_encryption_enabled: false,
1462        damm_table,
1463        counter_mode: CounterMode::Manual,
1464        format: CodeFormat::default(),
1465        audit: AuditInfo::new(&machine_id),
1466    }
1467}
1468
1469/// Statistics about a .bin file.
1470#[derive(Clone, Debug)]
1471pub struct BinStats {
1472    /// Name of the configuration.
1473    pub name: String,
1474    /// Alphabet size.
1475    pub alphabet_size: usize,
1476    /// Base code length (without check digit).
1477    pub code_length: usize,
1478    /// Total code length (with check digit, without formatting).
1479    pub base_code_length: usize,
1480    /// Total code length (with formatting).
1481    pub formatted_code_length: usize,
1482    /// Maximum possible unique codes (alphabet_size ^ code_length).
1483    pub capacity: u128,
1484    /// Current counter value.
1485    pub current_counter: u64,
1486    /// Total codes generated (from audit).
1487    pub total_generated: u64,
1488    /// Utilization percentage (current_counter / capacity * 100).
1489    pub utilization_percent: f64,
1490    /// Remaining codes before capacity exhaustion.
1491    pub remaining_capacity: u128,
1492    /// Counter mode.
1493    pub counter_mode: CounterMode,
1494    /// Has prefix/suffix/separator formatting.
1495    pub has_formatting: bool,
1496    /// Storage encryption enabled.
1497    pub storage_encryption_enabled: bool,
1498    /// Created timestamp.
1499    pub created_at: Option<String>,
1500    /// Last generated timestamp.
1501    pub last_generated_at: Option<String>,
1502    /// Number of secret rotations.
1503    pub secret_rotation_count: usize,
1504    /// Number of machine masterings.
1505    pub machine_mastering_count: usize,
1506    /// Number of config changes.
1507    pub config_change_count: usize,
1508    /// Number of generation log entries.
1509    pub generation_log_count: usize,
1510}
1511
1512impl BinStats {
1513    /// Calculate stats from a BinFile.
1514    pub fn from_bin_file(bin: &BinFile) -> Self {
1515        let config = bin.config();
1516        let audit = bin.audit();
1517
1518        let alphabet_size = config.alphabet.len();
1519        let code_length = config.code_length;
1520        let base_code_length = code_length + 1; // + check digit
1521        let formatted_code_length = config.format.total_length(base_code_length);
1522
1523        let capacity = (alphabet_size as u128).pow(code_length as u32);
1524        let current_counter = bin.get_counter().unwrap_or(0);
1525
1526        let utilization_percent = if capacity > 0 {
1527            (current_counter as f64 / capacity as f64) * 100.0
1528        } else {
1529            0.0
1530        };
1531
1532        let remaining_capacity = capacity.saturating_sub(current_counter as u128);
1533
1534        let has_formatting = config.format.has_formatting();
1535
1536        Self {
1537            name: config.name.clone(),
1538            alphabet_size,
1539            code_length,
1540            base_code_length,
1541            formatted_code_length,
1542            capacity,
1543            current_counter,
1544            total_generated: audit.generation_count,
1545            utilization_percent,
1546            remaining_capacity,
1547            counter_mode: config.counter_mode.clone(),
1548            has_formatting,
1549            storage_encryption_enabled: config.storage_encryption_enabled,
1550            created_at: Some(audit.created_at.to_rfc3339()),
1551            last_generated_at: audit.last_generated_at.map(|dt| dt.to_rfc3339()),
1552            secret_rotation_count: audit.history.secret_rotations.len(),
1553            machine_mastering_count: audit.history.machine_masterings.len(),
1554            config_change_count: audit.history.config_changes.len(),
1555            generation_log_count: audit.generation_log.len(),
1556        }
1557    }
1558
1559    /// Get a human-readable summary.
1560    pub fn summary(&self) -> String {
1561        let mut lines = Vec::new();
1562
1563        lines.push(format!("Name: {}", self.name));
1564        lines.push(format!("Alphabet size: {}", self.alphabet_size));
1565        lines.push(format!(
1566            "Code length: {} (base: {}, formatted: {})",
1567            self.code_length, self.base_code_length, self.formatted_code_length
1568        ));
1569        lines.push(format!("Capacity: {}", self.capacity));
1570        lines.push(format!("Current counter: {}", self.current_counter));
1571        lines.push(format!("Total generated: {}", self.total_generated));
1572        lines.push(format!("Utilization: {:.6}%", self.utilization_percent));
1573        lines.push(format!("Remaining: {}", self.remaining_capacity));
1574        lines.push(format!("Counter mode: {:?}", self.counter_mode));
1575        lines.push(format!("Has formatting: {}", self.has_formatting));
1576        lines.push(format!(
1577            "Storage encryption: {}",
1578            self.storage_encryption_enabled
1579        ));
1580
1581        if let Some(ref created) = self.created_at {
1582            lines.push(format!("Created: {}", created));
1583        }
1584
1585        if let Some(ref last) = self.last_generated_at {
1586            lines.push(format!("Last generated: {}", last));
1587        }
1588
1589        if self.secret_rotation_count > 0
1590            || self.machine_mastering_count > 0
1591            || self.config_change_count > 0
1592        {
1593            lines.push(format!(
1594                "History: {} rotations, {} masterings, {} config changes",
1595                self.secret_rotation_count, self.machine_mastering_count, self.config_change_count
1596            ));
1597        }
1598
1599        if self.generation_log_count > 0 {
1600            lines.push(format!(
1601                "Generation log entries: {}",
1602                self.generation_log_count
1603            ));
1604        }
1605
1606        lines.join("\n")
1607    }
1608}
1609
1610#[cfg(test)]
1611mod tests {
1612    use super::*;
1613    use tempfile::TempDir;
1614
1615    fn create_test_config(name: &str) -> BinConfig {
1616        let alphabet = Alphabet::default_alphabet();
1617        BinConfig {
1618            name: name.to_string(),
1619            alphabet: alphabet.clone(),
1620            code_length: 9,
1621            check_position: CheckPosition::End,
1622            secret_key: String::new(),
1623            storage_key: String::new(),
1624            storage_encryption_enabled: false,
1625            damm_table: DammTable::new(alphabet.len()),
1626            counter_mode: CounterMode::Manual,
1627            format: CodeFormat::default(),
1628            audit: AuditInfo::default(),
1629        }
1630    }
1631
1632    #[test]
1633    fn test_create_and_open_with_secret() {
1634        let temp_dir = TempDir::new().unwrap();
1635        let path = temp_dir.path().join("test.bin");
1636
1637        let config = create_test_config("test");
1638        let _bin = BinFile::create(&path, "my-secret", config).unwrap();
1639
1640        // Open with secret
1641        let bin = BinFile::open_with_secret(&path, "my-secret").unwrap();
1642        assert_eq!(bin.name(), "test");
1643        assert_eq!(bin.access_level(), AccessLevel::FullAccess);
1644    }
1645
1646    #[test]
1647    fn test_wrong_secret() {
1648        let temp_dir = TempDir::new().unwrap();
1649        let path = temp_dir.path().join("test.bin");
1650
1651        let config = create_test_config("test");
1652        let _bin = BinFile::create(&path, "correct-secret", config).unwrap();
1653
1654        // Try wrong secret
1655        let result = BinFile::open_with_secret(&path, "wrong-secret");
1656        assert!(result.is_err());
1657    }
1658
1659    #[test]
1660    fn test_generate_and_validate() {
1661        let temp_dir = TempDir::new().unwrap();
1662        let path = temp_dir.path().join("test.bin");
1663
1664        let mut config = create_test_config("test");
1665        config.counter_mode = CounterMode::InBin;
1666
1667        let mut bin = BinFile::create(&path, "secret", config).unwrap();
1668
1669        // Generate codes
1670        let codes = bin.generate_batch(10).unwrap();
1671        assert_eq!(codes.len(), 10);
1672
1673        // Validate all codes
1674        for code in &codes {
1675            assert!(bin.is_valid(code));
1676        }
1677    }
1678
1679    #[test]
1680    fn test_generate_at() {
1681        let temp_dir = TempDir::new().unwrap();
1682        let path = temp_dir.path().join("test.bin");
1683
1684        let config = create_test_config("test");
1685        let bin = BinFile::create(&path, "secret", config).unwrap();
1686
1687        // Generate at specific counter
1688        let code1 = bin.generate_at(1000).unwrap();
1689        let code2 = bin.generate_at(1000).unwrap();
1690
1691        assert_eq!(code1, code2); // Same counter = same code
1692        assert!(bin.is_valid(&code1));
1693    }
1694
1695    #[test]
1696    fn test_readonly_cannot_generate() {
1697        let temp_dir = TempDir::new().unwrap();
1698        let path = temp_dir.path().join("test.bin");
1699
1700        let config = create_test_config("test");
1701        let _bin = BinFile::create(&path, "secret", config).unwrap();
1702
1703        // Open readonly
1704        let mut bin = BinFile::open_readonly(&path).unwrap();
1705        assert_eq!(bin.access_level(), AccessLevel::ReadOnly);
1706
1707        // Try to generate
1708        let result = bin.generate();
1709        assert!(result.is_err());
1710        assert!(matches!(
1711            result.unwrap_err(),
1712            PromocryptError::SecretRequired
1713        ));
1714    }
1715
1716    #[test]
1717    fn test_code_length() {
1718        let temp_dir = TempDir::new().unwrap();
1719        let path = temp_dir.path().join("test.bin");
1720
1721        let mut config = create_test_config("test");
1722        config.code_length = 12;
1723        config.counter_mode = CounterMode::InBin;
1724
1725        let mut bin = BinFile::create(&path, "secret", config).unwrap();
1726
1727        let code = bin.generate().unwrap();
1728        assert_eq!(code.len(), 13); // 12 + 1 check digit
1729    }
1730
1731    #[test]
1732    fn test_audit_tracking() {
1733        let temp_dir = TempDir::new().unwrap();
1734        let path = temp_dir.path().join("test.bin");
1735
1736        let mut config = create_test_config("test");
1737        config.counter_mode = CounterMode::InBin;
1738
1739        let mut bin = BinFile::create(&path, "secret", config).unwrap();
1740
1741        assert_eq!(bin.audit().generation_count, 0);
1742
1743        bin.generate_batch(100).unwrap();
1744        assert_eq!(bin.audit().generation_count, 100);
1745
1746        bin.generate().unwrap();
1747        assert_eq!(bin.audit().generation_count, 101);
1748    }
1749
1750    #[test]
1751    fn test_from_bytes() {
1752        let config = create_test_config("test");
1753        let data = BinFile::create_in_memory("secret", config).unwrap();
1754
1755        // Open from bytes
1756        let bin = BinFile::from_bytes_with_secret(&data, "secret").unwrap();
1757        assert_eq!(bin.name(), "test");
1758    }
1759}