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::External,
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        // Save the full config to persist generation log (not just mutable section)
662        self.save()?;
663
664        // Apply storage encryption if enabled
665        if self.config.storage_encryption_enabled {
666            encrypt_code_for_storage(&self.storage_key, &formatted)
667        } else {
668            Ok(formatted)
669        }
670    }
671
672    /// Generate batch of codes (requires FullAccess).
673    pub fn generate_batch(&mut self, count: usize) -> Result<Vec<String>> {
674        self.require_full_access()?;
675
676        let start_counter = self.counter.reserve(count as u64)?;
677        let base_codes = generate_batch(
678            &self.secret_key,
679            start_counter,
680            count,
681            &self.config.alphabet,
682            self.config.code_length,
683            self.config.check_position,
684            &self.config.damm_table,
685        );
686
687        // Apply formatting
688        let formatted: Vec<String> = base_codes
689            .iter()
690            .map(|c| self.config.format.format(c))
691            .collect();
692
693        // Record generation with log
694        if let Some(machine_id) = &self.current_machine_id {
695            self.config
696                .audit
697                .record_generation_with_log(machine_id, count as u64, start_counter);
698        } else {
699            self.config.audit.record_generation(count as u64);
700        }
701        // Save the full config to persist generation log (not just mutable section)
702        self.save()?;
703
704        // Apply storage encryption if enabled
705        if self.config.storage_encryption_enabled {
706            let code_refs: Vec<&str> = formatted.iter().map(|s| s.as_str()).collect();
707            crate::encryption::encrypt_codes_for_storage(&self.storage_key, &code_refs)
708        } else {
709            Ok(formatted)
710        }
711    }
712
713    /// Generate code at specific counter (doesn't update counter).
714    pub fn generate_at(&self, counter: u64) -> Result<String> {
715        self.require_full_access()?;
716
717        let base_code = generate_code(
718            &self.secret_key,
719            counter,
720            &self.config.alphabet,
721            self.config.code_length,
722            self.config.check_position,
723            &self.config.damm_table,
724        );
725
726        Ok(self.config.format.format(&base_code))
727    }
728
729    /// Generate batch at specific counter (doesn't update counter).
730    pub fn generate_batch_at(&self, start_counter: u64, count: usize) -> Result<Vec<String>> {
731        self.require_full_access()?;
732
733        let base_codes = generate_batch(
734            &self.secret_key,
735            start_counter,
736            count,
737            &self.config.alphabet,
738            self.config.code_length,
739            self.config.check_position,
740            &self.config.damm_table,
741        );
742
743        Ok(base_codes
744            .iter()
745            .map(|c| self.config.format.format(c))
746            .collect())
747    }
748
749    // ==================== Counter ====================
750
751    /// Get current counter value.
752    pub fn get_counter(&self) -> Result<u64> {
753        self.counter.get()
754    }
755
756    /// Set counter value (requires FullAccess).
757    ///
758    /// This sets the counter to a specific value. Use with caution as it can
759    /// cause duplicate codes if set to a previously used value.
760    ///
761    /// # Arguments
762    /// * `value` - The new counter value
763    ///
764    /// # Errors
765    /// Returns an error if:
766    /// - Access level is not FullAccess
767    /// - Counter mode is Manual or External (counter not stored)
768    /// - IO error during save
769    ///
770    /// # Example
771    /// ```no_run
772    /// use promocrypt_core::BinFile;
773    ///
774    /// let mut bin = BinFile::open_with_secret("campaign.bin", "secret")?;
775    /// bin.set_counter(1000)?;
776    /// # Ok::<(), promocrypt_core::PromocryptError>(())
777    /// ```
778    pub fn set_counter(&mut self, value: u64) -> Result<()> {
779        self.require_full_access()?;
780
781        // Validate against capacity to prevent duplicate codes
782        let capacity = self.capacity();
783        if value as u128 >= capacity {
784            return Err(PromocryptError::InvalidArgument(format!(
785                "Counter value {} exceeds capacity {}. This would cause duplicate codes.",
786                value, capacity
787            )));
788        }
789
790        self.counter.set(value)?;
791
792        // Save to persist counter change (for InBin and File modes)
793        if self.path.is_some() {
794            self.save()?;
795        }
796
797        Ok(())
798    }
799
800    /// Calculate the maximum capacity based on alphabet size and code length.
801    ///
802    /// Capacity = alphabet_size ^ code_length
803    pub fn capacity(&self) -> u128 {
804        let alphabet_size = self.config.alphabet.len();
805        let code_length = self.config.code_length;
806        (alphabet_size as u128).pow(code_length as u32)
807    }
808
809    /// Reserve a range of counter values atomically (requires FullAccess).
810    ///
811    /// Returns the starting counter value and increments the stored counter
812    /// by `count`. This is useful for parallel code generation where multiple
813    /// workers need exclusive counter ranges.
814    ///
815    /// # Arguments
816    /// * `count` - Number of counter values to reserve
817    ///
818    /// # Returns
819    /// The starting counter value of the reserved range.
820    ///
821    /// # Errors
822    /// Returns an error if:
823    /// - Access level is not FullAccess
824    /// - Counter mode is External (counter not stored)
825    /// - Counter overflow would occur
826    /// - Reservation would exceed capacity
827    /// - IO error during save
828    ///
829    /// # Example
830    /// ```no_run
831    /// use promocrypt_core::BinFile;
832    ///
833    /// let mut bin = BinFile::open_with_secret("campaign.bin", "secret")?;
834    ///
835    /// // Reserve 10,000 counter values for this worker
836    /// let start = bin.reserve_counter_range(10000)?;
837    ///
838    /// // Generate codes at the reserved counters (without updating counter)
839    /// let codes = bin.generate_batch_at(start, 10000)?;
840    /// # Ok::<(), promocrypt_core::PromocryptError>(())
841    /// ```
842    pub fn reserve_counter_range(&mut self, count: u64) -> Result<u64> {
843        self.require_full_access()?;
844
845        // Validate against capacity to prevent duplicate codes
846        let capacity = self.capacity();
847        let current = self.counter.get().unwrap_or(0);
848        let end = current.saturating_add(count);
849        if end as u128 > capacity {
850            return Err(PromocryptError::InvalidArgument(format!(
851                "Reserving {} codes would exceed capacity. Current: {}, Capacity: {}",
852                count, current, capacity
853            )));
854        }
855
856        let start = self.counter.reserve(count)?;
857
858        // Save to persist counter change (for InBin and File modes)
859        if self.path.is_some() {
860            self.save()?;
861        }
862
863        Ok(start)
864    }
865
866    // ==================== Mastering ====================
867
868    /// Master for a new machine (requires FullAccess).
869    pub fn master_for_machine(
870        &self,
871        output_path: impl AsRef<Path>,
872        target_machine_id: &[u8; 32],
873    ) -> Result<()> {
874        self.require_full_access()?;
875
876        let path = output_path.as_ref();
877
878        // Update config with new mastering info
879        let mut new_config = self.config.clone();
880        new_config.audit.record_master(target_machine_id);
881
882        // Regenerate file with new machine binding
883        let config_json = serde_json::to_vec(&new_config)?;
884
885        // Generate new nonces
886        let machine_nonce = generate_nonce()?;
887        let data_nonce = generate_nonce()?;
888
889        // Encrypt data_key for new machine
890        let machine_encrypted_key = encrypt_data_key(
891            &self.data_key,
892            target_machine_id,
893            &self.salt,
894            &machine_nonce,
895        )?;
896
897        // Keep existing secret encrypted key from raw data
898        let raw_data = self.raw_data.as_ref().ok_or_else(|| {
899            PromocryptError::InvalidArgument("No raw data available for mastering".to_string())
900        })?;
901
902        let mut secret_nonce = [0u8; NONCE_SIZE];
903        secret_nonce
904            .copy_from_slice(&raw_data[OFFSET_SECRET_NONCE..OFFSET_SECRET_NONCE + NONCE_SIZE]);
905
906        let mut secret_encrypted = [0u8; ENCRYPTED_KEY_SIZE];
907        secret_encrypted
908            .copy_from_slice(&raw_data[OFFSET_SECRET_KEY..OFFSET_SECRET_KEY + ENCRYPTED_KEY_SIZE]);
909
910        // Encrypt config
911        let encrypted_data = encrypt_data(&self.data_key, &config_json, &data_nonce)?;
912
913        // Build file
914        let flags = FLAG_IS_BOUND
915            | if self.has_mutable {
916                FLAG_HAS_MUTABLE
917            } else {
918                0
919            };
920
921        let mut file_data = Vec::new();
922
923        // Header
924        file_data.extend_from_slice(MAGIC);
925        file_data.push(VERSION);
926        file_data.push(flags);
927
928        // CRC placeholder
929        let crc_offset = file_data.len();
930        file_data.extend_from_slice(&[0u8; 4]);
931
932        // Salt and keys
933        file_data.extend_from_slice(&self.salt);
934        file_data.extend_from_slice(&machine_nonce);
935        file_data.extend_from_slice(&machine_encrypted_key);
936        file_data.extend_from_slice(&secret_nonce);
937        file_data.extend_from_slice(&secret_encrypted);
938
939        // Encrypted data
940        file_data.extend_from_slice(&data_nonce);
941        file_data.extend_from_slice(&(encrypted_data.len() as u32).to_le_bytes());
942        file_data.extend_from_slice(&encrypted_data);
943
944        // Mutable section (if needed)
945        if self.has_mutable {
946            let mutable_nonce = generate_nonce()?;
947            let counter_bytes = counter_to_bytes(self.counter.in_bin_value());
948            let encrypted_counter = encrypt_data(&self.data_key, &counter_bytes, &mutable_nonce)?;
949
950            file_data.extend_from_slice(&mutable_nonce);
951            file_data.extend_from_slice(&(encrypted_counter.len() as u32).to_le_bytes());
952            file_data.extend_from_slice(&encrypted_counter);
953        }
954
955        // Calculate and write CRC
956        let crc = crc32fast::hash(&file_data[0..10]);
957        file_data[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
958
959        // Write file
960        let mut file = File::create(path)?;
961        file.write_all(&file_data)?;
962        file.sync_all()?;
963
964        Ok(())
965    }
966
967    /// Export unbound copy (requires FullAccess).
968    pub fn export_unbound(&self, output_path: impl AsRef<Path>) -> Result<()> {
969        self.require_full_access()?;
970
971        let path = output_path.as_ref();
972
973        // Create config without machine binding
974        let mut new_config = self.config.clone();
975        new_config.audit.mastered_at = None;
976        new_config.audit.mastered_for = None;
977
978        let config_json = serde_json::to_vec(&new_config)?;
979
980        // Generate new nonces
981        let data_nonce = generate_nonce()?;
982
983        // Keep existing secret encrypted key
984        let raw_data = self
985            .raw_data
986            .as_ref()
987            .ok_or_else(|| PromocryptError::InvalidArgument("No raw data available".to_string()))?;
988
989        let mut secret_nonce = [0u8; NONCE_SIZE];
990        secret_nonce
991            .copy_from_slice(&raw_data[OFFSET_SECRET_NONCE..OFFSET_SECRET_NONCE + NONCE_SIZE]);
992
993        let mut secret_encrypted = [0u8; ENCRYPTED_KEY_SIZE];
994        secret_encrypted
995            .copy_from_slice(&raw_data[OFFSET_SECRET_KEY..OFFSET_SECRET_KEY + ENCRYPTED_KEY_SIZE]);
996
997        // Encrypt config
998        let encrypted_data = encrypt_data(&self.data_key, &config_json, &data_nonce)?;
999
1000        // Build file (no machine key, not bound)
1001        let flags = if self.has_mutable {
1002            FLAG_HAS_MUTABLE
1003        } else {
1004            0
1005        };
1006
1007        let mut file_data = Vec::new();
1008
1009        // Header
1010        file_data.extend_from_slice(MAGIC);
1011        file_data.push(VERSION);
1012        file_data.push(flags);
1013
1014        // CRC placeholder
1015        let crc_offset = file_data.len();
1016        file_data.extend_from_slice(&[0u8; 4]);
1017
1018        // Salt and keys (empty machine key)
1019        file_data.extend_from_slice(&self.salt);
1020        file_data.extend_from_slice(&[0u8; NONCE_SIZE]); // Empty machine nonce
1021        file_data.extend_from_slice(&[0u8; ENCRYPTED_KEY_SIZE]); // Empty machine key
1022        file_data.extend_from_slice(&secret_nonce);
1023        file_data.extend_from_slice(&secret_encrypted);
1024
1025        // Encrypted data
1026        file_data.extend_from_slice(&data_nonce);
1027        file_data.extend_from_slice(&(encrypted_data.len() as u32).to_le_bytes());
1028        file_data.extend_from_slice(&encrypted_data);
1029
1030        // Calculate and write CRC
1031        let crc = crc32fast::hash(&file_data[0..10]);
1032        file_data[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
1033
1034        // Write file
1035        let mut file = File::create(path)?;
1036        file.write_all(&file_data)?;
1037        file.sync_all()?;
1038
1039        Ok(())
1040    }
1041
1042    // ==================== Storage Encryption ====================
1043
1044    /// Encrypt a code for database storage (requires FullAccess).
1045    pub fn encrypt_code(&self, code: &str) -> Result<String> {
1046        self.require_full_access()?;
1047        encrypt_code_for_storage(&self.storage_key, code)
1048    }
1049
1050    /// Decrypt a code from database storage (requires FullAccess).
1051    pub fn decrypt_code(&self, encrypted: &str) -> Result<String> {
1052        self.require_full_access()?;
1053        decrypt_code_from_storage(&self.storage_key, encrypted)
1054    }
1055
1056    /// Encrypt multiple codes for storage (requires FullAccess).
1057    pub fn encrypt_codes(&self, codes: &[&str]) -> Result<Vec<String>> {
1058        self.require_full_access()?;
1059        crate::encryption::encrypt_codes_for_storage(&self.storage_key, codes)
1060    }
1061
1062    /// Decrypt multiple codes from storage (requires FullAccess).
1063    pub fn decrypt_codes(&self, encrypted: &[&str]) -> Result<Vec<String>> {
1064        self.require_full_access()?;
1065        crate::encryption::decrypt_codes_from_storage(&self.storage_key, encrypted)
1066    }
1067
1068    /// Enable or disable storage encryption (requires FullAccess).
1069    pub fn set_storage_encryption(&mut self, enabled: bool) -> Result<()> {
1070        self.require_full_access()?;
1071
1072        let old_value = self.config.storage_encryption_enabled.to_string();
1073        let new_value = enabled.to_string();
1074
1075        if let Some(machine_id) = &self.current_machine_id {
1076            self.config.audit.history.record_config_change(
1077                machine_id,
1078                "storage_encryption_enabled",
1079                Some(&old_value),
1080                Some(&new_value),
1081            );
1082        }
1083
1084        self.config.storage_encryption_enabled = enabled;
1085        Ok(())
1086    }
1087
1088    // ==================== Configuration ====================
1089
1090    /// Update counter mode (requires FullAccess).
1091    pub fn set_counter_mode(&mut self, mode: CounterMode) -> Result<()> {
1092        self.require_full_access()?;
1093
1094        self.config.counter_mode = mode.clone();
1095        self.counter.set_mode(mode);
1096        self.save_if_needed()?;
1097
1098        Ok(())
1099    }
1100
1101    /// Update code format (requires FullAccess).
1102    pub fn set_format(&mut self, format: CodeFormat) -> Result<()> {
1103        self.require_full_access()?;
1104
1105        if let Some(machine_id) = &self.current_machine_id {
1106            self.config.audit.history.record_config_change(
1107                machine_id,
1108                "format",
1109                Some(&serde_json::to_string(&self.config.format).unwrap_or_default()),
1110                Some(&serde_json::to_string(&format).unwrap_or_default()),
1111            );
1112        }
1113
1114        self.config.format = format;
1115        Ok(())
1116    }
1117
1118    /// Update check position (requires FullAccess).
1119    pub fn set_check_position(&mut self, position: CheckPosition) -> Result<()> {
1120        self.require_full_access()?;
1121
1122        if let Some(machine_id) = &self.current_machine_id {
1123            self.config.audit.history.record_config_change(
1124                machine_id,
1125                "check_position",
1126                Some(&self.config.check_position.to_string()),
1127                Some(&position.to_string()),
1128            );
1129        }
1130
1131        self.config.check_position = position;
1132        Ok(())
1133    }
1134
1135    /// Set code prefix (requires FullAccess).
1136    ///
1137    /// # Arguments
1138    /// * `prefix` - Optional prefix to prepend to generated codes
1139    pub fn set_prefix(&mut self, prefix: Option<String>) -> Result<()> {
1140        self.require_full_access()?;
1141
1142        let old_value = self.config.format.prefix.as_deref().map(|s| s.to_string());
1143
1144        if let Some(machine_id) = &self.current_machine_id {
1145            self.config.audit.history.record_config_change(
1146                machine_id,
1147                "prefix",
1148                old_value.as_deref(),
1149                prefix.as_deref(),
1150            );
1151        }
1152
1153        self.config.format.prefix = prefix;
1154        Ok(())
1155    }
1156
1157    /// Set code suffix (requires FullAccess).
1158    ///
1159    /// # Arguments
1160    /// * `suffix` - Optional suffix to append to generated codes
1161    pub fn set_suffix(&mut self, suffix: Option<String>) -> Result<()> {
1162        self.require_full_access()?;
1163
1164        let old_value = self.config.format.suffix.as_deref().map(|s| s.to_string());
1165
1166        if let Some(machine_id) = &self.current_machine_id {
1167            self.config.audit.history.record_config_change(
1168                machine_id,
1169                "suffix",
1170                old_value.as_deref(),
1171                suffix.as_deref(),
1172            );
1173        }
1174
1175        self.config.format.suffix = suffix;
1176        Ok(())
1177    }
1178
1179    /// Set separator character (requires FullAccess).
1180    ///
1181    /// # Arguments
1182    /// * `separator` - Optional separator character to insert at specified positions
1183    pub fn set_separator(&mut self, separator: Option<char>) -> Result<()> {
1184        self.require_full_access()?;
1185
1186        let old_value = self.config.format.separator.map(|c| c.to_string());
1187        let new_value = separator.map(|c| c.to_string());
1188
1189        if let Some(machine_id) = &self.current_machine_id {
1190            self.config.audit.history.record_config_change(
1191                machine_id,
1192                "separator",
1193                old_value.as_deref(),
1194                new_value.as_deref(),
1195            );
1196        }
1197
1198        self.config.format.separator = separator;
1199        Ok(())
1200    }
1201
1202    /// Set separator positions (requires FullAccess).
1203    ///
1204    /// # Arguments
1205    /// * `positions` - Positions where separator should be inserted (0-based, before the character)
1206    pub fn set_separator_positions(&mut self, positions: Vec<usize>) -> Result<()> {
1207        self.require_full_access()?;
1208
1209        let old_value = serde_json::to_string(&self.config.format.separator_positions).ok();
1210        let new_value = serde_json::to_string(&positions).ok();
1211
1212        if let Some(machine_id) = &self.current_machine_id {
1213            self.config.audit.history.record_config_change(
1214                machine_id,
1215                "separator_positions",
1216                old_value.as_deref(),
1217                new_value.as_deref(),
1218            );
1219        }
1220
1221        self.config.format.separator_positions = positions;
1222        Ok(())
1223    }
1224
1225    /// Save all configuration changes to the .bin file (requires FullAccess).
1226    ///
1227    /// This persists all in-memory configuration changes to disk, including:
1228    /// - Format changes (prefix, suffix, separator, separator_positions)
1229    /// - Check position changes
1230    /// - Storage encryption changes
1231    /// - Counter mode changes
1232    /// - History and generation log updates
1233    ///
1234    /// # Errors
1235    /// Returns an error if:
1236    /// - Access level is not FullAccess
1237    /// - No file path is associated (in-memory file)
1238    /// - IO error during write
1239    pub fn save(&mut self) -> Result<()> {
1240        self.require_full_access()?;
1241
1242        let path = self.path.as_ref().ok_or_else(|| {
1243            PromocryptError::InvalidArgument("Cannot save in-memory file without path".to_string())
1244        })?;
1245
1246        // Get existing raw_data to preserve machine encryption
1247        let raw_data = self.raw_data.as_ref().ok_or_else(|| {
1248            PromocryptError::InvalidArgument("No raw data available for save".to_string())
1249        })?;
1250
1251        // Serialize updated config
1252        let config_json = serde_json::to_vec(&self.config)?;
1253
1254        // Generate new data nonce
1255        let data_nonce = generate_nonce()?;
1256
1257        // Encrypt config
1258        let encrypted_data = encrypt_data(&self.data_key, &config_json, &data_nonce)?;
1259
1260        // Get machine encrypted key from raw data
1261        let mut machine_nonce = [0u8; NONCE_SIZE];
1262        machine_nonce
1263            .copy_from_slice(&raw_data[OFFSET_MACHINE_NONCE..OFFSET_MACHINE_NONCE + NONCE_SIZE]);
1264
1265        let mut machine_encrypted = [0u8; ENCRYPTED_KEY_SIZE];
1266        machine_encrypted.copy_from_slice(
1267            &raw_data[OFFSET_MACHINE_KEY..OFFSET_MACHINE_KEY + ENCRYPTED_KEY_SIZE],
1268        );
1269
1270        // Get secret encrypted key from raw data
1271        let mut secret_nonce = [0u8; NONCE_SIZE];
1272        secret_nonce
1273            .copy_from_slice(&raw_data[OFFSET_SECRET_NONCE..OFFSET_SECRET_NONCE + NONCE_SIZE]);
1274
1275        let mut secret_encrypted = [0u8; ENCRYPTED_KEY_SIZE];
1276        secret_encrypted
1277            .copy_from_slice(&raw_data[OFFSET_SECRET_KEY..OFFSET_SECRET_KEY + ENCRYPTED_KEY_SIZE]);
1278
1279        // Build file
1280        let flags = (if self.is_bound { FLAG_IS_BOUND } else { 0 })
1281            | (if self.has_mutable {
1282                FLAG_HAS_MUTABLE
1283            } else {
1284                0
1285            });
1286
1287        let mut file_data = Vec::new();
1288
1289        // Header
1290        file_data.extend_from_slice(MAGIC);
1291        file_data.push(VERSION);
1292        file_data.push(flags);
1293
1294        // CRC placeholder
1295        let crc_offset = file_data.len();
1296        file_data.extend_from_slice(&[0u8; 4]);
1297
1298        // Salt and keys
1299        file_data.extend_from_slice(&self.salt);
1300        file_data.extend_from_slice(&machine_nonce);
1301        file_data.extend_from_slice(&machine_encrypted);
1302        file_data.extend_from_slice(&secret_nonce);
1303        file_data.extend_from_slice(&secret_encrypted);
1304
1305        // Encrypted data
1306        file_data.extend_from_slice(&data_nonce);
1307        file_data.extend_from_slice(&(encrypted_data.len() as u32).to_le_bytes());
1308        file_data.extend_from_slice(&encrypted_data);
1309
1310        // Mutable section (if needed)
1311        if self.has_mutable {
1312            let mutable_nonce = generate_nonce()?;
1313            let counter_bytes = counter_to_bytes(self.counter.in_bin_value());
1314            let encrypted_counter = encrypt_data(&self.data_key, &counter_bytes, &mutable_nonce)?;
1315
1316            file_data.extend_from_slice(&mutable_nonce);
1317            file_data.extend_from_slice(&(encrypted_counter.len() as u32).to_le_bytes());
1318            file_data.extend_from_slice(&encrypted_counter);
1319        }
1320
1321        // Calculate and write CRC
1322        let crc = crc32fast::hash(&file_data[0..10]);
1323        file_data[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
1324
1325        // Write file
1326        let mut file = File::create(path)?;
1327        file.write_all(&file_data)?;
1328        file.sync_all()?;
1329
1330        // Update raw_data cache
1331        self.raw_data = Some(file_data);
1332
1333        // Mark counter as saved since we wrote the whole file
1334        if self.counter.is_in_bin_modified() {
1335            self.counter.mark_saved();
1336        }
1337
1338        Ok(())
1339    }
1340
1341    // ==================== Secret Rotation ====================
1342
1343    /// Rotate the secret password (requires FullAccess).
1344    /// Re-encrypts the data_key with the new secret.
1345    pub fn rotate_secret(&mut self, old_secret: &str, new_secret: &str) -> Result<()> {
1346        self.require_full_access()?;
1347
1348        // Verify old secret first (we should already have full access, but verify it matches)
1349        let path = self.path.as_ref().ok_or_else(|| {
1350            PromocryptError::InvalidArgument("Cannot rotate secret for in-memory file".to_string())
1351        })?;
1352
1353        // Read raw file to get current secret encrypted key
1354        let raw_data = self
1355            .raw_data
1356            .as_ref()
1357            .ok_or_else(|| PromocryptError::InvalidArgument("No raw data available".to_string()))?;
1358
1359        // Try to decrypt with old secret to verify it
1360        let mut secret_nonce = [0u8; NONCE_SIZE];
1361        secret_nonce
1362            .copy_from_slice(&raw_data[OFFSET_SECRET_NONCE..OFFSET_SECRET_NONCE + NONCE_SIZE]);
1363
1364        let mut secret_encrypted = [0u8; ENCRYPTED_KEY_SIZE];
1365        secret_encrypted
1366            .copy_from_slice(&raw_data[OFFSET_SECRET_KEY..OFFSET_SECRET_KEY + ENCRYPTED_KEY_SIZE]);
1367
1368        // Verify old secret
1369        let _verified_data_key = decrypt_data_key(
1370            &secret_encrypted,
1371            old_secret.as_bytes(),
1372            &self.salt,
1373            &secret_nonce,
1374        )
1375        .map_err(|_| PromocryptError::InvalidArgument("Old secret is incorrect".to_string()))?;
1376
1377        // Generate new nonce for secret encryption
1378        let new_secret_nonce = generate_nonce()?;
1379
1380        // Encrypt data_key with new secret
1381        let new_secret_encrypted = encrypt_data_key(
1382            &self.data_key,
1383            new_secret.as_bytes(),
1384            &self.salt,
1385            &new_secret_nonce,
1386        )?;
1387
1388        // Record rotation in history
1389        let old_hash = hash_secret(old_secret);
1390        let new_hash = hash_secret(new_secret);
1391
1392        if let Some(machine_id) = &self.current_machine_id {
1393            self.config
1394                .audit
1395                .history
1396                .record_secret_rotation(machine_id, &old_hash, &new_hash);
1397        }
1398
1399        // Rewrite the file with new secret encryption
1400        let config_json = serde_json::to_vec(&self.config)?;
1401        let data_nonce = generate_nonce()?;
1402        let encrypted_data = encrypt_data(&self.data_key, &config_json, &data_nonce)?;
1403
1404        // Get machine encrypted key from raw data
1405        let mut machine_nonce = [0u8; NONCE_SIZE];
1406        machine_nonce
1407            .copy_from_slice(&raw_data[OFFSET_MACHINE_NONCE..OFFSET_MACHINE_NONCE + NONCE_SIZE]);
1408
1409        let mut machine_encrypted = [0u8; ENCRYPTED_KEY_SIZE];
1410        machine_encrypted.copy_from_slice(
1411            &raw_data[OFFSET_MACHINE_KEY..OFFSET_MACHINE_KEY + ENCRYPTED_KEY_SIZE],
1412        );
1413
1414        // Build file
1415        let flags = (if self.is_bound { FLAG_IS_BOUND } else { 0 })
1416            | (if self.has_mutable {
1417                FLAG_HAS_MUTABLE
1418            } else {
1419                0
1420            });
1421
1422        let mut file_data = Vec::new();
1423
1424        // Header
1425        file_data.extend_from_slice(MAGIC);
1426        file_data.push(VERSION);
1427        file_data.push(flags);
1428
1429        // CRC placeholder
1430        let crc_offset = file_data.len();
1431        file_data.extend_from_slice(&[0u8; 4]);
1432
1433        // Salt and keys
1434        file_data.extend_from_slice(&self.salt);
1435        file_data.extend_from_slice(&machine_nonce);
1436        file_data.extend_from_slice(&machine_encrypted);
1437        file_data.extend_from_slice(&new_secret_nonce);
1438        file_data.extend_from_slice(&new_secret_encrypted);
1439
1440        // Encrypted data
1441        file_data.extend_from_slice(&data_nonce);
1442        file_data.extend_from_slice(&(encrypted_data.len() as u32).to_le_bytes());
1443        file_data.extend_from_slice(&encrypted_data);
1444
1445        // Mutable section (if needed)
1446        if self.has_mutable {
1447            let mutable_nonce = generate_nonce()?;
1448            let counter_bytes = counter_to_bytes(self.counter.in_bin_value());
1449            let encrypted_counter = encrypt_data(&self.data_key, &counter_bytes, &mutable_nonce)?;
1450
1451            file_data.extend_from_slice(&mutable_nonce);
1452            file_data.extend_from_slice(&(encrypted_counter.len() as u32).to_le_bytes());
1453            file_data.extend_from_slice(&encrypted_counter);
1454        }
1455
1456        // Calculate and write CRC
1457        let crc = crc32fast::hash(&file_data[0..10]);
1458        file_data[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
1459
1460        // Write file
1461        let mut file = File::create(path)?;
1462        file.write_all(&file_data)?;
1463        file.sync_all()?;
1464
1465        // Update raw_data
1466        self.raw_data = Some(file_data);
1467
1468        Ok(())
1469    }
1470
1471    // ==================== History & Logs ====================
1472
1473    /// Export history as JSON.
1474    pub fn export_history(&self) -> String {
1475        self.config.audit.export_history()
1476    }
1477
1478    /// Export generation log as JSON.
1479    pub fn export_generation_log(&self) -> String {
1480        self.config.audit.export_generation_log()
1481    }
1482
1483    /// Clear history (requires FullAccess).
1484    pub fn clear_history(&mut self, keep_last: Option<usize>) -> Result<()> {
1485        self.require_full_access()?;
1486        self.config.audit.clear_history(keep_last);
1487        Ok(())
1488    }
1489
1490    /// Clear generation log (requires FullAccess).
1491    pub fn clear_generation_log(&mut self, keep_last: Option<usize>) -> Result<()> {
1492        self.require_full_access()?;
1493        self.config.audit.clear_generation_log(keep_last);
1494        Ok(())
1495    }
1496
1497    // ==================== Statistics ====================
1498
1499    /// Get statistics about this .bin file.
1500    pub fn get_stats(&self) -> BinStats {
1501        BinStats::from_bin_file(self)
1502    }
1503
1504    // ==================== Internal ====================
1505
1506    fn require_full_access(&self) -> Result<()> {
1507        if self.access_level != AccessLevel::FullAccess {
1508            return Err(PromocryptError::SecretRequired);
1509        }
1510        Ok(())
1511    }
1512
1513    fn save_if_needed(&mut self) -> Result<()> {
1514        if self.counter.is_in_bin_modified() {
1515            if let Some(path) = &self.path {
1516                self.save_mutable_section(path)?;
1517            }
1518            self.counter.mark_saved();
1519        }
1520        Ok(())
1521    }
1522
1523    fn save_mutable_section(&self, path: &Path) -> Result<()> {
1524        if !self.has_mutable {
1525            return Ok(());
1526        }
1527
1528        // Read existing file
1529        let mut file = OpenOptions::new().read(true).write(true).open(path)?;
1530
1531        // Find mutable section offset
1532        let mut header = [0u8; HEADER_SIZE];
1533        file.read_exact(&mut header)?;
1534
1535        let data_length = u32::from_le_bytes([
1536            header[OFFSET_DATA_LENGTH],
1537            header[OFFSET_DATA_LENGTH + 1],
1538            header[OFFSET_DATA_LENGTH + 2],
1539            header[OFFSET_DATA_LENGTH + 3],
1540        ]) as usize;
1541
1542        let mutable_offset = OFFSET_DATA + data_length;
1543
1544        // Generate new mutable section
1545        let mutable_nonce = generate_nonce()?;
1546        let counter_bytes = counter_to_bytes(self.counter.in_bin_value());
1547        let encrypted_counter = encrypt_data(&self.data_key, &counter_bytes, &mutable_nonce)?;
1548
1549        // Write mutable section
1550        file.seek(SeekFrom::Start(mutable_offset as u64))?;
1551        file.write_all(&mutable_nonce)?;
1552        file.write_all(&(encrypted_counter.len() as u32).to_le_bytes())?;
1553        file.write_all(&encrypted_counter)?;
1554        file.sync_all()?;
1555
1556        Ok(())
1557    }
1558}
1559
1560/// Simple helper to create a BinConfig with defaults.
1561pub fn create_config(name: &str) -> BinConfig {
1562    let machine_id = get_machine_id().unwrap_or([0u8; 32]);
1563    let alphabet = Alphabet::default_alphabet();
1564    let damm_table = DammTable::new(alphabet.len());
1565
1566    BinConfig {
1567        name: name.to_string(),
1568        alphabet,
1569        code_length: 9,
1570        check_position: CheckPosition::End,
1571        secret_key: String::new(),
1572        storage_key: String::new(),
1573        storage_encryption_enabled: false,
1574        damm_table,
1575        counter_mode: CounterMode::External,
1576        format: CodeFormat::default(),
1577        audit: AuditInfo::new(&machine_id),
1578    }
1579}
1580
1581/// Statistics about a .bin file.
1582#[derive(Clone, Debug)]
1583pub struct BinStats {
1584    /// Name of the configuration.
1585    pub name: String,
1586    /// Alphabet size.
1587    pub alphabet_size: usize,
1588    /// Base code length (without check digit).
1589    pub code_length: usize,
1590    /// Total code length (with check digit, without formatting).
1591    pub base_code_length: usize,
1592    /// Total code length (with formatting).
1593    pub formatted_code_length: usize,
1594    /// Maximum possible unique codes (alphabet_size ^ code_length).
1595    pub capacity: u128,
1596    /// Current counter value.
1597    pub current_counter: u64,
1598    /// Total codes generated (from audit).
1599    pub total_generated: u64,
1600    /// Utilization percentage (current_counter / capacity * 100).
1601    pub utilization_percent: f64,
1602    /// Remaining codes before capacity exhaustion.
1603    pub remaining_capacity: u128,
1604    /// Counter mode.
1605    pub counter_mode: CounterMode,
1606    /// Has prefix/suffix/separator formatting.
1607    pub has_formatting: bool,
1608    /// Storage encryption enabled.
1609    pub storage_encryption_enabled: bool,
1610    /// Created timestamp.
1611    pub created_at: Option<String>,
1612    /// Last generated timestamp.
1613    pub last_generated_at: Option<String>,
1614    /// Number of secret rotations.
1615    pub secret_rotation_count: usize,
1616    /// Number of machine masterings.
1617    pub machine_mastering_count: usize,
1618    /// Number of config changes.
1619    pub config_change_count: usize,
1620    /// Number of generation log entries.
1621    pub generation_log_count: usize,
1622}
1623
1624impl BinStats {
1625    /// Calculate stats from a BinFile.
1626    pub fn from_bin_file(bin: &BinFile) -> Self {
1627        let config = bin.config();
1628        let audit = bin.audit();
1629
1630        let alphabet_size = config.alphabet.len();
1631        let code_length = config.code_length;
1632        let base_code_length = code_length + 1; // + check digit
1633        let formatted_code_length = config.format.total_length(base_code_length);
1634
1635        let capacity = (alphabet_size as u128).pow(code_length as u32);
1636        let current_counter = bin.get_counter().unwrap_or(0);
1637
1638        let utilization_percent = if capacity > 0 {
1639            (current_counter as f64 / capacity as f64) * 100.0
1640        } else {
1641            0.0
1642        };
1643
1644        let remaining_capacity = capacity.saturating_sub(current_counter as u128);
1645
1646        let has_formatting = config.format.has_formatting();
1647
1648        Self {
1649            name: config.name.clone(),
1650            alphabet_size,
1651            code_length,
1652            base_code_length,
1653            formatted_code_length,
1654            capacity,
1655            current_counter,
1656            total_generated: audit.generation_count,
1657            utilization_percent,
1658            remaining_capacity,
1659            counter_mode: config.counter_mode.clone(),
1660            has_formatting,
1661            storage_encryption_enabled: config.storage_encryption_enabled,
1662            created_at: Some(audit.created_at.to_rfc3339()),
1663            last_generated_at: audit.last_generated_at.map(|dt| dt.to_rfc3339()),
1664            secret_rotation_count: audit.history.secret_rotations.len(),
1665            machine_mastering_count: audit.history.machine_masterings.len(),
1666            config_change_count: audit.history.config_changes.len(),
1667            generation_log_count: audit.generation_log.len(),
1668        }
1669    }
1670
1671    /// Get a human-readable summary.
1672    pub fn summary(&self) -> String {
1673        let mut lines = Vec::new();
1674
1675        lines.push(format!("Name: {}", self.name));
1676        lines.push(format!("Alphabet size: {}", self.alphabet_size));
1677        lines.push(format!(
1678            "Code length: {} (base: {}, formatted: {})",
1679            self.code_length, self.base_code_length, self.formatted_code_length
1680        ));
1681        lines.push(format!("Capacity: {}", self.capacity));
1682        lines.push(format!("Current counter: {}", self.current_counter));
1683        lines.push(format!("Total generated: {}", self.total_generated));
1684        lines.push(format!("Utilization: {:.6}%", self.utilization_percent));
1685        lines.push(format!("Remaining: {}", self.remaining_capacity));
1686        lines.push(format!("Counter mode: {:?}", self.counter_mode));
1687        lines.push(format!("Has formatting: {}", self.has_formatting));
1688        lines.push(format!(
1689            "Storage encryption: {}",
1690            self.storage_encryption_enabled
1691        ));
1692
1693        if let Some(ref created) = self.created_at {
1694            lines.push(format!("Created: {}", created));
1695        }
1696
1697        if let Some(ref last) = self.last_generated_at {
1698            lines.push(format!("Last generated: {}", last));
1699        }
1700
1701        if self.secret_rotation_count > 0
1702            || self.machine_mastering_count > 0
1703            || self.config_change_count > 0
1704        {
1705            lines.push(format!(
1706                "History: {} rotations, {} masterings, {} config changes",
1707                self.secret_rotation_count, self.machine_mastering_count, self.config_change_count
1708            ));
1709        }
1710
1711        if self.generation_log_count > 0 {
1712            lines.push(format!(
1713                "Generation log entries: {}",
1714                self.generation_log_count
1715            ));
1716        }
1717
1718        lines.join("\n")
1719    }
1720}
1721
1722#[cfg(test)]
1723mod tests {
1724    use super::*;
1725    use tempfile::TempDir;
1726
1727    fn create_test_config(name: &str) -> BinConfig {
1728        let alphabet = Alphabet::default_alphabet();
1729        BinConfig {
1730            name: name.to_string(),
1731            alphabet: alphabet.clone(),
1732            code_length: 9,
1733            check_position: CheckPosition::End,
1734            secret_key: String::new(),
1735            storage_key: String::new(),
1736            storage_encryption_enabled: false,
1737            damm_table: DammTable::new(alphabet.len()),
1738            counter_mode: CounterMode::External,
1739            format: CodeFormat::default(),
1740            audit: AuditInfo::default(),
1741        }
1742    }
1743
1744    #[test]
1745    fn test_create_and_open_with_secret() {
1746        let temp_dir = TempDir::new().unwrap();
1747        let path = temp_dir.path().join("test.bin");
1748
1749        let config = create_test_config("test");
1750        let _bin = BinFile::create(&path, "my-secret", config).unwrap();
1751
1752        // Open with secret
1753        let bin = BinFile::open_with_secret(&path, "my-secret").unwrap();
1754        assert_eq!(bin.name(), "test");
1755        assert_eq!(bin.access_level(), AccessLevel::FullAccess);
1756    }
1757
1758    #[test]
1759    fn test_wrong_secret() {
1760        let temp_dir = TempDir::new().unwrap();
1761        let path = temp_dir.path().join("test.bin");
1762
1763        let config = create_test_config("test");
1764        let _bin = BinFile::create(&path, "correct-secret", config).unwrap();
1765
1766        // Try wrong secret
1767        let result = BinFile::open_with_secret(&path, "wrong-secret");
1768        assert!(result.is_err());
1769    }
1770
1771    #[test]
1772    fn test_generate_and_validate() {
1773        let temp_dir = TempDir::new().unwrap();
1774        let path = temp_dir.path().join("test.bin");
1775
1776        let mut config = create_test_config("test");
1777        config.counter_mode = CounterMode::InBin;
1778
1779        let mut bin = BinFile::create(&path, "secret", config).unwrap();
1780
1781        // Generate codes
1782        let codes = bin.generate_batch(10).unwrap();
1783        assert_eq!(codes.len(), 10);
1784
1785        // Validate all codes
1786        for code in &codes {
1787            assert!(bin.is_valid(code));
1788        }
1789    }
1790
1791    #[test]
1792    fn test_generate_at() {
1793        let temp_dir = TempDir::new().unwrap();
1794        let path = temp_dir.path().join("test.bin");
1795
1796        let config = create_test_config("test");
1797        let bin = BinFile::create(&path, "secret", config).unwrap();
1798
1799        // Generate at specific counter
1800        let code1 = bin.generate_at(1000).unwrap();
1801        let code2 = bin.generate_at(1000).unwrap();
1802
1803        assert_eq!(code1, code2); // Same counter = same code
1804        assert!(bin.is_valid(&code1));
1805    }
1806
1807    #[test]
1808    fn test_readonly_cannot_generate() {
1809        let temp_dir = TempDir::new().unwrap();
1810        let path = temp_dir.path().join("test.bin");
1811
1812        let config = create_test_config("test");
1813        let _bin = BinFile::create(&path, "secret", config).unwrap();
1814
1815        // Open readonly
1816        let mut bin = BinFile::open_readonly(&path).unwrap();
1817        assert_eq!(bin.access_level(), AccessLevel::ReadOnly);
1818
1819        // Try to generate
1820        let result = bin.generate();
1821        assert!(result.is_err());
1822        assert!(matches!(
1823            result.unwrap_err(),
1824            PromocryptError::SecretRequired
1825        ));
1826    }
1827
1828    #[test]
1829    fn test_code_length() {
1830        let temp_dir = TempDir::new().unwrap();
1831        let path = temp_dir.path().join("test.bin");
1832
1833        let mut config = create_test_config("test");
1834        config.code_length = 12;
1835        config.counter_mode = CounterMode::InBin;
1836
1837        let mut bin = BinFile::create(&path, "secret", config).unwrap();
1838
1839        let code = bin.generate().unwrap();
1840        assert_eq!(code.len(), 13); // 12 + 1 check digit
1841    }
1842
1843    #[test]
1844    fn test_audit_tracking() {
1845        let temp_dir = TempDir::new().unwrap();
1846        let path = temp_dir.path().join("test.bin");
1847
1848        let mut config = create_test_config("test");
1849        config.counter_mode = CounterMode::InBin;
1850
1851        let mut bin = BinFile::create(&path, "secret", config).unwrap();
1852
1853        assert_eq!(bin.audit().generation_count, 0);
1854
1855        bin.generate_batch(100).unwrap();
1856        assert_eq!(bin.audit().generation_count, 100);
1857
1858        bin.generate().unwrap();
1859        assert_eq!(bin.audit().generation_count, 101);
1860    }
1861
1862    #[test]
1863    fn test_from_bytes() {
1864        let config = create_test_config("test");
1865        let data = BinFile::create_in_memory("secret", config).unwrap();
1866
1867        // Open from bytes
1868        let bin = BinFile::from_bytes_with_secret(&data, "secret").unwrap();
1869        assert_eq!(bin.name(), "test");
1870    }
1871
1872    #[test]
1873    fn test_set_counter() {
1874        let temp_dir = TempDir::new().unwrap();
1875        let path = temp_dir.path().join("test.bin");
1876
1877        let mut config = create_test_config("test");
1878        config.counter_mode = CounterMode::InBin;
1879
1880        let mut bin = BinFile::create(&path, "secret", config).unwrap();
1881
1882        // Initial counter should be 0
1883        assert_eq!(bin.get_counter().unwrap(), 0);
1884
1885        // Set counter to specific value
1886        bin.set_counter(1000).unwrap();
1887        assert_eq!(bin.get_counter().unwrap(), 1000);
1888
1889        // Reopen and verify persistence
1890        drop(bin);
1891        let bin = BinFile::open_with_secret(&path, "secret").unwrap();
1892        assert_eq!(bin.get_counter().unwrap(), 1000);
1893    }
1894
1895    #[test]
1896    fn test_set_counter_requires_full_access() {
1897        let temp_dir = TempDir::new().unwrap();
1898        let path = temp_dir.path().join("test.bin");
1899
1900        let mut config = create_test_config("test");
1901        config.counter_mode = CounterMode::InBin;
1902
1903        let _bin = BinFile::create(&path, "secret", config).unwrap();
1904
1905        // Open readonly
1906        let mut bin = BinFile::open_readonly(&path).unwrap();
1907
1908        // Try to set counter - should fail
1909        let result = bin.set_counter(100);
1910        assert!(result.is_err());
1911        assert!(matches!(
1912            result.unwrap_err(),
1913            PromocryptError::SecretRequired
1914        ));
1915    }
1916
1917    #[test]
1918    fn test_reserve_counter_range() {
1919        let temp_dir = TempDir::new().unwrap();
1920        let path = temp_dir.path().join("test.bin");
1921
1922        let mut config = create_test_config("test");
1923        config.counter_mode = CounterMode::InBin;
1924
1925        let mut bin = BinFile::create(&path, "secret", config).unwrap();
1926
1927        // Initial counter should be 0
1928        assert_eq!(bin.get_counter().unwrap(), 0);
1929
1930        // Reserve 100 counter values
1931        let start = bin.reserve_counter_range(100).unwrap();
1932        assert_eq!(start, 0);
1933        assert_eq!(bin.get_counter().unwrap(), 100);
1934
1935        // Reserve 50 more
1936        let start2 = bin.reserve_counter_range(50).unwrap();
1937        assert_eq!(start2, 100);
1938        assert_eq!(bin.get_counter().unwrap(), 150);
1939
1940        // Reopen and verify persistence
1941        drop(bin);
1942        let bin = BinFile::open_with_secret(&path, "secret").unwrap();
1943        assert_eq!(bin.get_counter().unwrap(), 150);
1944    }
1945
1946    #[test]
1947    fn test_reserve_counter_range_for_parallel_generation() {
1948        let temp_dir = TempDir::new().unwrap();
1949        let path = temp_dir.path().join("test.bin");
1950
1951        let mut config = create_test_config("test");
1952        config.counter_mode = CounterMode::InBin;
1953
1954        let mut bin = BinFile::create(&path, "secret", config).unwrap();
1955
1956        // Reserve a range
1957        let start = bin.reserve_counter_range(1000).unwrap();
1958
1959        // Generate codes at reserved counters (doesn't update counter again)
1960        let codes = bin.generate_batch_at(start, 10).unwrap();
1961        assert_eq!(codes.len(), 10);
1962
1963        // Counter should still be at 1000 (reserved but not incremented again)
1964        assert_eq!(bin.get_counter().unwrap(), 1000);
1965
1966        // All generated codes should be valid
1967        for code in &codes {
1968            assert!(bin.is_valid(code));
1969        }
1970    }
1971
1972    #[test]
1973    fn test_reserve_counter_requires_full_access() {
1974        let temp_dir = TempDir::new().unwrap();
1975        let path = temp_dir.path().join("test.bin");
1976
1977        let mut config = create_test_config("test");
1978        config.counter_mode = CounterMode::InBin;
1979
1980        let _bin = BinFile::create(&path, "secret", config).unwrap();
1981
1982        // Open readonly
1983        let mut bin = BinFile::open_readonly(&path).unwrap();
1984
1985        // Try to reserve - should fail
1986        let result = bin.reserve_counter_range(100);
1987        assert!(result.is_err());
1988        assert!(matches!(
1989            result.unwrap_err(),
1990            PromocryptError::SecretRequired
1991        ));
1992    }
1993}