promocrypt_core/
lib.rs

1//! # promocrypt-core
2//!
3//! Core library for cryptographically secure promotional code generation.
4//!
5//! ## Features
6//!
7//! - **HMAC-SHA256 code generation** - Cryptographically secure codes
8//! - **Damm check digit** - 100% detection of single errors and transpositions
9//! - **Two-key encryption** - MachineID for read, secret for write
10//! - **Machine binding** - .bin files can be bound to specific machines
11//! - **Multiple counter modes** - File, in-bin, manual, or external
12//! - **FFI ready** - C-compatible interface for other languages
13//!
14//! ## Quick Start
15//!
16//! ```no_run
17//! use promocrypt_core::{BinFile, BinConfig, CounterMode, create_config};
18//!
19//! // Create a new .bin file
20//! let mut config = create_config("production");
21//! config.counter_mode = CounterMode::InBin;
22//!
23//! let mut bin = BinFile::create("production.bin", "my-secret", config).unwrap();
24//!
25//! // Generate codes
26//! let codes = bin.generate_batch(1000).unwrap();
27//!
28//! // Validate codes
29//! for code in &codes {
30//!     assert!(bin.is_valid(code));
31//! }
32//! ```
33//!
34//! ## Two-Key System
35//!
36//! Each .bin file uses a two-key encryption system:
37//!
38//! - **MachineID**: Derived from hardware identifiers, allows read-only access
39//!   (validation, reading config)
40//! - **Secret**: User-provided password, allows full access (generation,
41//!   mastering, configuration changes)
42//!
43//! ```no_run
44//! use promocrypt_core::BinFile;
45//!
46//! // Open read-only with machineID (automatic)
47//! let bin = BinFile::open_readonly("production.bin").unwrap();
48//! assert!(bin.validate("SOMECODE").is_valid() || !bin.validate("SOMECODE").is_valid());
49//!
50//! // Open with full access using secret
51//! let mut bin = BinFile::open_with_secret("production.bin", "my-secret").unwrap();
52//! let code = bin.generate().unwrap();
53//! ```
54//!
55//! ## Counter Modes
56//!
57//! - `CounterMode::File { path }` - Counter in separate file with OS locking
58//! - `CounterMode::InBin` - Counter in .bin mutable section
59//! - `CounterMode::External` - Caller provides counter values
60//! - `CounterMode::External` - Consumer manages counter (e.g., database)
61
62#![warn(missing_docs)]
63#![warn(clippy::all)]
64
65pub mod alphabet;
66pub mod audit;
67pub mod binary_file;
68pub mod counter;
69pub mod damm;
70pub mod encryption;
71pub mod error;
72pub mod generator;
73pub mod machine_id;
74pub mod validator;
75
76#[cfg(feature = "ffi")]
77pub mod ffi;
78
79// Re-exports for convenience
80pub use alphabet::{Alphabet, DEFAULT_ALPHABET, default_alphabet, validate_alphabet};
81pub use audit::{
82    AuditInfo, ConfigChange, GenerationLogEntry, History, MachineMastering, SecretRotation,
83};
84pub use binary_file::{AccessLevel, BinConfig, BinFile, BinStats, create_config};
85pub use counter::CounterMode;
86pub use damm::DammTable;
87pub use error::{ErrorCode, PromocryptError, Result, ValidationResult};
88pub use generator::{CheckPosition, CodeFormat, CodeGenerator, generate_batch, generate_code};
89pub use machine_id::{get_machine_id, is_machine_id_available};
90pub use validator::{is_valid, validate};
91
92/// Library version.
93pub const VERSION: &str = env!("CARGO_PKG_VERSION");
94
95/// Validate a code without opening a .bin file.
96///
97/// This is useful for quick validation with known parameters.
98///
99/// # Arguments
100/// * `code` - The code to validate
101/// * `alphabet_str` - Alphabet string
102/// * `code_length` - Expected code length (including check digit)
103///
104/// # Example
105///
106/// ```
107/// use promocrypt_core::{validate_code, ValidationResult};
108///
109/// let result = validate_code("ABCDEFGHIJ", "0234679ABCDEFGHJKMNPQRTUXY", 10);
110/// // result is ValidationResult::Valid or indicates the error
111/// ```
112pub fn validate_code(code: &str, alphabet_str: &str, code_length: usize) -> ValidationResult {
113    let alphabet = match Alphabet::new(alphabet_str) {
114        Ok(a) => a,
115        Err(_) => return ValidationResult::InvalidCheckDigit, // Invalid alphabet
116    };
117
118    let damm = DammTable::new(alphabet.len());
119    validate(code, &alphabet, code_length, &damm)
120}
121
122/// Generate a code with standalone parameters.
123///
124/// This is useful for testing or one-off generation without a .bin file.
125///
126/// # Arguments
127/// * `secret_key` - 32-byte secret key
128/// * `counter` - Counter value
129/// * `alphabet_str` - Alphabet string (default if None)
130/// * `code_length` - Number of random characters (9 default)
131/// * `check_position` - Check digit position (End default)
132///
133/// # Example
134///
135/// ```
136/// use promocrypt_core::{generate_code_standalone, CheckPosition};
137///
138/// let secret = [0u8; 32];
139/// let code = generate_code_standalone(&secret, 0, None, None, None);
140/// assert_eq!(code.len(), 10);
141/// ```
142pub fn generate_code_standalone(
143    secret_key: &[u8; 32],
144    counter: u64,
145    alphabet_str: Option<&str>,
146    code_length: Option<usize>,
147    check_position: Option<CheckPosition>,
148) -> String {
149    let alphabet = alphabet_str
150        .map(|s| Alphabet::new(s).unwrap_or_default())
151        .unwrap_or_default();
152    let damm = DammTable::new(alphabet.len());
153    let length = code_length.unwrap_or(9);
154    let position = check_position.unwrap_or(CheckPosition::End);
155
156    generate_code(secret_key, counter, &alphabet, length, position, &damm)
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn test_version() {
165        assert!(!VERSION.is_empty());
166    }
167
168    #[test]
169    fn test_validate_code_standalone() {
170        let secret = [42u8; 32];
171        let code = generate_code_standalone(&secret, 0, None, None, None);
172
173        let result = validate_code(&code, DEFAULT_ALPHABET, 10);
174        assert!(result.is_valid());
175    }
176
177    #[test]
178    fn test_validate_code_invalid() {
179        let result = validate_code("INVALID!", DEFAULT_ALPHABET, 10);
180        assert!(!result.is_valid());
181    }
182
183    #[test]
184    fn test_generate_standalone() {
185        let secret = [0u8; 32];
186
187        // Default parameters
188        let code = generate_code_standalone(&secret, 0, None, None, None);
189        assert_eq!(code.len(), 10);
190
191        // Custom parameters
192        let code = generate_code_standalone(
193            &secret,
194            0,
195            Some("0123456789ABCDEF"),
196            Some(8),
197            Some(CheckPosition::Start),
198        );
199        assert_eq!(code.len(), 9);
200    }
201
202    #[test]
203    fn test_reexports() {
204        // Just verify these are accessible
205        let _ = DEFAULT_ALPHABET;
206        let _ = Alphabet::default_alphabet();
207        let _ = DammTable::new(26);
208        let _ = CheckPosition::End;
209        let _ = CounterMode::External;
210        let _ = AccessLevel::ReadOnly;
211        let _ = ErrorCode::Success;
212    }
213}