| Crates.io | promocrypt-core |
| lib.rs | promocrypt-core |
| version | 1.1.0 |
| created_at | 2026-01-07 17:57:23.829715+00 |
| updated_at | 2026-01-10 08:11:33.694493+00 |
| description | Core library for cryptographically secure promotional code generation |
| homepage | |
| repository | https://github.com/professor93/promocrypt-core |
| max_upload_size | |
| id | 2028762 |
| size | 471,758 |
Core Rust library for cryptographically secure promotional code generation and validation.
promocrypt-core is a high-performance library for generating and validating promotional codes with enterprise-grade security. It uses HMAC-SHA256 for code generation and Damm algorithm for check digit validation, ensuring 100% detection of single-character errors and transpositions.
.bin files can be bound to specific machines| Feature | Description |
|---|---|
| Code Generation | HMAC-SHA256 based, deterministic |
| Code Validation | Damm check digit with detailed error reporting |
| Batch Operations | Generate/validate thousands of codes efficiently |
| Prefix/Suffix | Optional formatting for branded codes |
| Separators | Configurable separator positions (e.g., XXXX-XXXX-XX) |
| Check Position | Index-based placement (-1=end, 0=start, N=position) |
| Storage Encryption | AES-256-SIV for database storage |
| Two-Key Encryption | MachineID (read) + Secret (write) |
| Machine Binding | Bind .bin files to specific machines |
| Counter Modes | File-based, in-bin, or manual |
| Secret Rotation | Change password without invalidating codes |
| History Tracking | Track rotations, masterings, config changes |
| Generation Log | Record all code generation operations |
| Statistics | Capacity, utilization, generation counts |
| FFI/C API | Use from PHP, Python, Go, etc. |
git clone https://github.com/professor93/promocrypt-core.git
cd promocrypt-core
cargo build --release
[dependencies]
promocrypt-core = "1.0"
Download from GitHub Releases:
libpromocrypt-linux-x86_64.so - Linux x86_64libpromocrypt-macos-aarch64.dylib - macOS Apple Siliconpromocrypt.h - C header fileuse promocrypt_core::{BinFile, create_config, CounterMode};
// Create a new .bin file
let mut config = create_config("my-campaign");
config.counter_mode = CounterMode::InBin;
let mut bin = BinFile::create("campaign.bin", "my-secret-password", config)?;
// Generate codes
let codes = bin.generate_batch(1000)?;
println!("Generated {} codes", codes.len());
// Validate codes
for code in &codes {
assert!(bin.validate(code).is_valid());
}
use promocrypt_core::BinFile;
// Open with machine ID (read-only)
let bin = BinFile::open_readonly("campaign.bin")?;
// Validate user input
let result = bin.validate("ABC123DEF0");
if result.is_valid() {
println!("Code is valid!");
} else {
println!("Invalid code: {:?}", result);
}
// Create new .bin file (requires secret)
BinFile::create(path, secret, config) -> Result<BinFile>
// Open read-only with machine ID
BinFile::open_readonly(path) -> Result<BinFile>
// Open with full access using secret
BinFile::open_with_secret(path, secret) -> Result<BinFile>
// Validate with detailed result
bin.validate(code) -> ValidationResult
// Quick boolean check
bin.is_valid(code) -> bool
// Batch validation
bin.validate_batch(&[codes]) -> Vec<ValidationResult>
// Generate single code
bin.generate() -> Result<String>
// Generate batch
bin.generate_batch(count) -> Result<Vec<String>>
// Generate at specific counter (manual mode)
bin.generate_at(counter) -> Result<String>
// Generate batch at specific counter
bin.generate_batch_at(start_counter, count) -> Result<Vec<String>>
// Encrypt code for database storage
bin.encrypt_code(code) -> Result<String>
// Decrypt code from storage
bin.decrypt_code(encrypted) -> Result<String>
// Batch operations
bin.encrypt_codes(&[codes]) -> Result<Vec<String>>
bin.decrypt_codes(&[encrypted]) -> Result<Vec<String>>
// Enable/disable storage encryption
bin.set_storage_encryption(enabled) -> Result<()>
bin.is_storage_encryption_enabled() -> bool
// Get current counter
bin.get_counter() -> Result<u64>
// Set counter (requires secret)
bin.set_counter(value) -> Result<()>
// Reserve counter range atomically
bin.reserve_counter_range(count) -> Result<u64>
// Update format options
bin.set_format(CodeFormat) -> Result<()>
// Update check position
bin.set_check_position(CheckPosition) -> Result<()>
// Rotate secret password
bin.rotate_secret(old_secret, new_secret) -> Result<()>
// Create copy bound to another machine
bin.master_for_machine(output_path, target_machine_id) -> Result<()>
// Export unbound copy
bin.export_unbound(output_path) -> Result<()>
// Get history
bin.get_history() -> &History
bin.export_history() -> String // JSON
bin.clear_history(keep_last: Option<usize>) -> Result<()>
// Get generation log
bin.get_generation_log() -> &[GenerationLogEntry]
bin.export_generation_log() -> String // JSON
bin.clear_generation_log(keep_last: Option<usize>) -> Result<()>
// Get statistics
bin.get_stats() -> BinStats
bin.total_codes_generated() -> u64
The check digit can be placed at any position for added security:
use promocrypt_core::CheckPosition;
// End (default) - index 9 for 10-char code
let pos = CheckPosition::End; // XXXXXXXXX[C]
// Start - index 0
let pos = CheckPosition::Start; // [C]XXXXXXXXX
// Custom index
let pos = CheckPosition::Index(4); // XXXX[C]XXXXX
let pos = CheckPosition::Index(-3); // XXXXXXX[C]XX
use promocrypt_core::CodeFormat;
// Create format with prefix and suffix
let format = CodeFormat::new()
.with_prefix("PROMO-")
.with_suffix("-2024");
// Result: PROMO-A3KF7NP2XM-2024
// Add separators
let format = CodeFormat::new()
.with_separator('-', vec![4, 8]);
// Result: A3KF-7NP2-XM
// Combined
let format = CodeFormat::new()
.with_prefix("SALE")
.with_suffix("24")
.with_separator('-', vec![4]);
// Result: SALEA3KF-7NP2XM24
use promocrypt_core::CounterMode;
// File-based counter (default)
CounterMode::File { path: "./campaign.counter".to_string() }
// In-bin counter (stored in .bin file)
CounterMode::InBin
// Manual counter (caller provides values)
CounterMode::Manual
// External counter (for database-managed counters)
CounterMode::External
The .bin file contains encrypted configuration and keys:
Offset Size Field
------ ---- -----
0 8 Magic: "PROMOCRY"
8 1 Version: 0x02
9 1 Flags
10 4 Header CRC32
14 16 Salt
30 48 MachineEncryptedKey
78 48 SecretEncryptedKey
126 4 EncryptedDataLength
130 N EncryptedData (JSON)
130+N 16 AuthTag
146+N 4 MutableLength
150+N M MutableSection (counter)
150+N+M 16 MutableAuthTag
data_key (random 32 bytes)
│
┌────────────┴────────────┐
│ │
▼ ▼
AES-GCM(machineID) AES-GCM(secret)
│ │
▼ ▼
machine_encrypted_key secret_encrypted_key
(READ-ONLY access) (FULL access)
Final ID: SHA256(components + "promocrypt-machine-id-v1")
| Component | Algorithm |
|---|---|
| Code Generation | HMAC-SHA256 |
| Key Derivation | Argon2id (m=64MB, t=3, p=1) |
| Encryption | AES-256-GCM |
| Storage Encryption | AES-256-SIV (deterministic) |
#include "promocrypt.h"
int main() {
PromocryptHandle* handle = NULL;
// Open with secret
PromocryptErrorCode err = promocrypt_open_with_secret(
"campaign.bin",
"my-secret",
&handle
);
if (err != PROMOCRYPT_SUCCESS) {
return 1;
}
// Generate a code
char code[64];
err = promocrypt_generate(handle, code, sizeof(code));
if (err == PROMOCRYPT_SUCCESS) {
printf("Generated: %s\n", code);
}
// Validate
if (promocrypt_is_valid(handle, code) == 1) {
printf("Code is valid!\n");
}
// Clean up
promocrypt_close(handle);
return 0;
}
<?php
$ffi = FFI::cdef(
file_get_contents('promocrypt.h'),
'libpromocrypt.so'
);
$handle = FFI::new('PromocryptHandle*');
$err = $ffi->promocrypt_open_with_secret(
'campaign.bin',
'my-secret',
FFI::addr($handle)
);
if ($err === 0) {
$code = FFI::new('char[64]');
$ffi->promocrypt_generate($handle, $code, 64);
echo "Generated: " . FFI::string($code) . "\n";
$ffi->promocrypt_close($handle);
}
import ctypes
lib = ctypes.CDLL('./libpromocrypt.so')
# Define types
lib.promocrypt_open_with_secret.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_void_p)]
lib.promocrypt_open_with_secret.restype = ctypes.c_int
lib.promocrypt_generate.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_uint64]
lib.promocrypt_generate.restype = ctypes.c_int
lib.promocrypt_is_valid.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
lib.promocrypt_is_valid.restype = ctypes.c_int
lib.promocrypt_close.argtypes = [ctypes.c_void_p]
# Use the library
handle = ctypes.c_void_p()
err = lib.promocrypt_open_with_secret(
b'campaign.bin',
b'my-secret',
ctypes.byref(handle)
)
if err == 0:
code = ctypes.create_string_buffer(64)
lib.promocrypt_generate(handle, code, 64)
print(f"Generated: {code.value.decode()}")
is_valid = lib.promocrypt_is_valid(handle, code.value)
print(f"Valid: {is_valid == 1}")
lib.promocrypt_close(handle)
# Debug build
cargo build
# Release build
cargo build --release
# Build with FFI support
cargo build --release --features ffi
# Build shared library
cargo build --release --lib
target/release/
├── libpromocrypt_core.so # Linux shared library
├── libpromocrypt_core.dylib # macOS shared library
├── libpromocrypt_core.a # Static library
└── libpromocrypt_core.rlib # Rust library
# Run all tests
cargo test
# Run with all features
cargo test --all-features
# Run specific test file
cargo test --test integration
# Run with verbose output
cargo test -- --nocapture
# Run benchmarks
cargo bench
use promocrypt_core::{
BinFile, create_config, CounterMode, CheckPosition, CodeFormat,
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Create configuration
let mut config = create_config("black-friday-2024");
config.counter_mode = CounterMode::InBin;
config.check_position = CheckPosition::Index(4); // Hide check digit
config.format = CodeFormat::new()
.with_prefix("BF24-")
.with_separator('-', vec![4, 8]);
// 2. Create .bin file
let mut bin = BinFile::create(
"black-friday.bin",
"super-secret-password",
config
)?;
// 3. Generate codes
let codes = bin.generate_batch(10000)?;
println!("Generated {} codes", codes.len());
println!("Sample: {}", codes[0]);
// Output: BF24-XXXX-XXXX-XX
// 4. Get statistics
let stats = bin.get_stats();
println!("Capacity: {}", stats.capacity);
println!("Generated: {}", stats.total_generated);
println!("Utilization: {:.4}%", stats.utilization_percent);
// 5. Save and reopen read-only
drop(bin);
let bin = BinFile::open_readonly("black-friday.bin")?;
// 6. Validate codes
for code in &codes[..10] {
let result = bin.validate(code);
assert!(result.is_valid(), "Code should be valid");
}
// 7. Test invalid code
let result = bin.validate("INVALID-CODE");
assert!(!result.is_valid());
Ok(())
}
use promocrypt_core::{BinFile, create_config, CounterMode};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut config = create_config("encrypted-campaign");
config.counter_mode = CounterMode::InBin;
config.storage_encryption_enabled = true;
let mut bin = BinFile::create("encrypted.bin", "secret", config)?;
// Generate and encrypt for storage
let code = bin.generate()?;
let encrypted = bin.encrypt_code(&code)?;
println!("Original: {}", code);
println!("Encrypted: {}", encrypted);
// Later: decrypt from database
let decrypted = bin.decrypt_code(&encrypted)?;
assert_eq!(code, decrypted);
Ok(())
}
use promocrypt_core::BinFile;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Open with current secret
let mut bin = BinFile::open_with_secret("campaign.bin", "old-secret")?;
// Rotate to new secret
bin.rotate_secret("old-secret", "new-secret")?;
// Save changes
drop(bin);
// Now open with new secret
let bin = BinFile::open_with_secret("campaign.bin", "new-secret")?;
// Old codes still validate!
assert!(bin.validate("EXISTINGCODE").is_valid());
Ok(())
}
| Operation | Target | Notes |
|---|---|---|
validate() |
< 10us | Hot path optimization |
generate() |
< 100us | Including counter update |
generate_batch(1000) |
< 50ms | |
generate_batch(100000) |
< 5s | |
open_readonly() |
< 50ms | Includes Argon2 |
open_with_secret() |
< 100ms | Includes Argon2 |
MIT License - see LICENSE file for details.
Contributions are welcome! Please read our contributing guidelines and submit pull requests to the main branch.