1use 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
30const MAGIC: &[u8; 8] = b"PROMOCRY";
32const VERSION: u8 = 0x02;
33
34const 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
48const HEADER_SIZE: usize = OFFSET_DATA;
50
51const FLAG_IS_BOUND: u8 = 0x01;
53const FLAG_HAS_MUTABLE: u8 = 0x02;
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub enum AccessLevel {
58 ReadOnly,
60 FullAccess,
62}
63
64#[derive(Clone, Debug, Serialize, Deserialize)]
66pub struct BinConfig {
67 pub name: String,
69 pub alphabet: Alphabet,
71 pub code_length: usize,
73 pub check_position: CheckPosition,
75 pub secret_key: String,
77 #[serde(default, skip_serializing_if = "String::is_empty")]
79 pub storage_key: String,
80 #[serde(default)]
82 pub storage_encryption_enabled: bool,
83 pub damm_table: DammTable,
85 pub counter_mode: CounterMode,
87 #[serde(default, skip_serializing_if = "CodeFormat::is_default")]
89 pub format: CodeFormat,
90 pub audit: AuditInfo,
92}
93
94impl CodeFormat {
95 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 pub fn base_code_length(&self) -> usize {
107 self.code_length + 1
108 }
109
110 pub fn total_code_length(&self) -> usize {
112 self.format.total_length(self.base_code_length())
113 }
114}
115
116pub struct BinFile {
118 path: Option<PathBuf>,
120 raw_data: Option<Vec<u8>>,
122 config: BinConfig,
124 data_key: [u8; 32],
126 secret_key: [u8; 32],
128 storage_key: [u8; 32],
130 counter: CounterManager,
132 access_level: AccessLevel,
134 salt: [u8; SALT_SIZE],
136 is_bound: bool,
138 has_mutable: bool,
140 current_machine_id: Option<[u8; 32]>,
142}
143
144impl BinFile {
145 pub fn create(path: impl AsRef<Path>, secret: &str, mut config: BinConfig) -> Result<Self> {
176 let path = path.as_ref();
177
178 let machine_id = get_machine_id()?;
180
181 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 config.secret_key = hex::encode(secret_key);
189 config.storage_key = hex::encode(storage_key);
190
191 config.audit = AuditInfo::new(&machine_id);
193
194 let config_json = serde_json::to_vec(&config)?;
196
197 let machine_nonce = generate_nonce()?;
199 let secret_nonce = generate_nonce()?;
200 let data_nonce = generate_nonce()?;
201
202 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 let encrypted_data = encrypt_data(&data_key, &config_json, &data_nonce)?;
210
211 let flags = FLAG_IS_BOUND; let has_mutable = config.counter_mode.is_in_bin();
214
215 let mut file_data = Vec::new();
216
217 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 let crc_offset = file_data.len();
224 file_data.extend_from_slice(&[0u8; 4]);
225
226 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 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 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 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 let mut file = File::create(path)?;
255 file.write_all(&file_data)?;
256 file.sync_all()?;
257
258 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), 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 pub fn create_in_memory(secret: &str, config: BinConfig) -> Result<Vec<u8>> {
279 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 let data = std::fs::read(&temp_path)?;
286
287 std::fs::remove_file(&temp_path).ok();
289
290 drop(bin);
291 Ok(data)
292 }
293
294 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 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 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 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 pub fn from_bytes_with_secret(data: &[u8], secret: &str) -> Result<Self> {
330 let machine_id = get_machine_id().ok();
332 Self::open_from_bytes_internal(data, machine_id.as_ref(), Some(secret), None)
333 }
334
335 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 if data.len() < HEADER_SIZE {
344 return Err(PromocryptError::InvalidFileFormat);
345 }
346
347 if &data[OFFSET_MAGIC..OFFSET_MAGIC + 8] != MAGIC {
349 return Err(PromocryptError::InvalidFileFormat);
350 }
351
352 let version = data[OFFSET_VERSION];
354 if version != VERSION {
355 return Err(PromocryptError::UnsupportedVersion(version));
356 }
357
358 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 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 let mut salt = [0u8; SALT_SIZE];
377 salt.copy_from_slice(&data[OFFSET_SALT..OFFSET_SALT + SALT_SIZE]);
378
379 let (data_key, access_level) = if let Some(secret) = secret {
381 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 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 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 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 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 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 pub fn try_open_readonly(path: impl AsRef<Path>) -> Option<Self> {
510 Self::open_readonly(path).ok()
511 }
512
513 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 pub fn config(&self) -> &BinConfig {
522 &self.config
523 }
524
525 pub fn name(&self) -> &str {
527 &self.config.name
528 }
529
530 pub fn alphabet(&self) -> &Alphabet {
532 &self.config.alphabet
533 }
534
535 pub fn code_length(&self) -> usize {
537 self.config.code_length
538 }
539
540 pub fn total_length(&self) -> usize {
542 self.config.code_length + 1
543 }
544
545 pub fn check_position(&self) -> CheckPosition {
547 self.config.check_position
548 }
549
550 pub fn counter_mode(&self) -> &CounterMode {
552 &self.config.counter_mode
553 }
554
555 pub fn audit(&self) -> &AuditInfo {
557 &self.config.audit
558 }
559
560 pub fn access_level(&self) -> AccessLevel {
562 self.access_level
563 }
564
565 pub fn is_bound(&self) -> bool {
567 self.is_bound
568 }
569
570 pub fn format(&self) -> &CodeFormat {
572 &self.config.format
573 }
574
575 pub fn is_storage_encryption_enabled(&self) -> bool {
577 self.config.storage_encryption_enabled
578 }
579
580 pub fn get_history(&self) -> &History {
582 &self.config.audit.history
583 }
584
585 pub fn get_generation_log(&self) -> &[GenerationLogEntry] {
587 self.config.audit.get_generation_log()
588 }
589
590 pub fn total_codes_generated(&self) -> u64 {
592 self.config.audit.generation_count
593 }
594
595 pub fn validate(&self, code: &str) -> ValidationResult {
600 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 pub fn is_valid(&self, code: &str) -> bool {
624 self.validate(code).is_valid()
625 }
626
627 pub fn validate_batch(&self, codes: &[&str]) -> Vec<ValidationResult> {
629 codes.iter().map(|c| self.validate(c)).collect()
630 }
631
632 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 let formatted = self.config.format.format(&base_code);
652
653 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 if self.config.storage_encryption_enabled {
665 encrypt_code_for_storage(&self.storage_key, &formatted)
666 } else {
667 Ok(formatted)
668 }
669 }
670
671 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 let formatted: Vec<String> = base_codes
688 .iter()
689 .map(|c| self.config.format.format(c))
690 .collect();
691
692 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 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 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 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 pub fn get_counter(&self) -> Result<u64> {
751 self.counter.get()
752 }
753
754 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 let mut new_config = self.config.clone();
768 new_config.audit.record_master(target_machine_id);
769
770 let config_json = serde_json::to_vec(&new_config)?;
772
773 let machine_nonce = generate_nonce()?;
775 let data_nonce = generate_nonce()?;
776
777 let machine_encrypted_key = encrypt_data_key(
779 &self.data_key,
780 target_machine_id,
781 &self.salt,
782 &machine_nonce,
783 )?;
784
785 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 let encrypted_data = encrypt_data(&self.data_key, &config_json, &data_nonce)?;
800
801 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 file_data.extend_from_slice(MAGIC);
813 file_data.push(VERSION);
814 file_data.push(flags);
815
816 let crc_offset = file_data.len();
818 file_data.extend_from_slice(&[0u8; 4]);
819
820 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 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 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 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 let mut file = File::create(path)?;
849 file.write_all(&file_data)?;
850 file.sync_all()?;
851
852 Ok(())
853 }
854
855 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 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 let data_nonce = generate_nonce()?;
870
871 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 let encrypted_data = encrypt_data(&self.data_key, &config_json, &data_nonce)?;
887
888 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 file_data.extend_from_slice(MAGIC);
899 file_data.push(VERSION);
900 file_data.push(flags);
901
902 let crc_offset = file_data.len();
904 file_data.extend_from_slice(&[0u8; 4]);
905
906 file_data.extend_from_slice(&self.salt);
908 file_data.extend_from_slice(&[0u8; NONCE_SIZE]); file_data.extend_from_slice(&[0u8; ENCRYPTED_KEY_SIZE]); file_data.extend_from_slice(&secret_nonce);
911 file_data.extend_from_slice(&secret_encrypted);
912
913 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 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 let mut file = File::create(path)?;
924 file.write_all(&file_data)?;
925 file.sync_all()?;
926
927 Ok(())
928 }
929
930 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 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 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 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 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 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 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 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 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 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 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 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 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 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 let config_json = serde_json::to_vec(&self.config)?;
1141
1142 let data_nonce = generate_nonce()?;
1144
1145 let encrypted_data = encrypt_data(&self.data_key, &config_json, &data_nonce)?;
1147
1148 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 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 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 file_data.extend_from_slice(MAGIC);
1179 file_data.push(VERSION);
1180 file_data.push(flags);
1181
1182 let crc_offset = file_data.len();
1184 file_data.extend_from_slice(&[0u8; 4]);
1185
1186 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 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 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 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 let mut file = File::create(path)?;
1215 file.write_all(&file_data)?;
1216 file.sync_all()?;
1217
1218 self.raw_data = Some(file_data);
1220
1221 if self.counter.is_in_bin_modified() {
1223 self.counter.mark_saved();
1224 }
1225
1226 Ok(())
1227 }
1228
1229 pub fn rotate_secret(&mut self, old_secret: &str, new_secret: &str) -> Result<()> {
1234 self.require_full_access()?;
1235
1236 let path = self.path.as_ref().ok_or_else(|| {
1238 PromocryptError::InvalidArgument("Cannot rotate secret for in-memory file".to_string())
1239 })?;
1240
1241 let raw_data = self
1243 .raw_data
1244 .as_ref()
1245 .ok_or_else(|| PromocryptError::InvalidArgument("No raw data available".to_string()))?;
1246
1247 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 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 let new_secret_nonce = generate_nonce()?;
1267
1268 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 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 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 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 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 file_data.extend_from_slice(MAGIC);
1314 file_data.push(VERSION);
1315 file_data.push(flags);
1316
1317 let crc_offset = file_data.len();
1319 file_data.extend_from_slice(&[0u8; 4]);
1320
1321 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 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 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 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 let mut file = File::create(path)?;
1350 file.write_all(&file_data)?;
1351 file.sync_all()?;
1352
1353 self.raw_data = Some(file_data);
1355
1356 Ok(())
1357 }
1358
1359 pub fn export_history(&self) -> String {
1363 self.config.audit.export_history()
1364 }
1365
1366 pub fn export_generation_log(&self) -> String {
1368 self.config.audit.export_generation_log()
1369 }
1370
1371 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 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 pub fn get_stats(&self) -> BinStats {
1389 BinStats::from_bin_file(self)
1390 }
1391
1392 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 let mut file = OpenOptions::new().read(true).write(true).open(path)?;
1418
1419 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 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 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
1448pub 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#[derive(Clone, Debug)]
1471pub struct BinStats {
1472 pub name: String,
1474 pub alphabet_size: usize,
1476 pub code_length: usize,
1478 pub base_code_length: usize,
1480 pub formatted_code_length: usize,
1482 pub capacity: u128,
1484 pub current_counter: u64,
1486 pub total_generated: u64,
1488 pub utilization_percent: f64,
1490 pub remaining_capacity: u128,
1492 pub counter_mode: CounterMode,
1494 pub has_formatting: bool,
1496 pub storage_encryption_enabled: bool,
1498 pub created_at: Option<String>,
1500 pub last_generated_at: Option<String>,
1502 pub secret_rotation_count: usize,
1504 pub machine_mastering_count: usize,
1506 pub config_change_count: usize,
1508 pub generation_log_count: usize,
1510}
1511
1512impl BinStats {
1513 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; 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 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 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 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 let codes = bin.generate_batch(10).unwrap();
1671 assert_eq!(codes.len(), 10);
1672
1673 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 let code1 = bin.generate_at(1000).unwrap();
1689 let code2 = bin.generate_at(1000).unwrap();
1690
1691 assert_eq!(code1, code2); 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 let mut bin = BinFile::open_readonly(&path).unwrap();
1705 assert_eq!(bin.access_level(), AccessLevel::ReadOnly);
1706
1707 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); }
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 let bin = BinFile::from_bytes_with_secret(&data, "secret").unwrap();
1757 assert_eq!(bin.name(), "test");
1758 }
1759}