use crate::error::{Error, Result};
pub const LICENSE_BLOCK_FIXED_SIZE: usize = 71;
pub mod flags {
pub const SEATS_ENFORCED: u8 = 0b0000_0001;
pub const EXPIRATION_ENFORCED: u8 = 0b0000_0010;
pub const QUERY_LIMITED: u8 = 0b0000_0100;
pub const WATERMARKED: u8 = 0b0000_1000;
pub const REVOCABLE: u8 = 0b0001_0000;
pub const TRANSFERABLE: u8 = 0b0010_0000;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LicenseBlock {
pub license_id: [u8; 16],
pub licensee_hash: [u8; 32],
pub issued_at: u64,
pub expires_at: u64,
pub flags: u8,
pub seat_limit: u16,
pub query_limit: u32,
pub custom_terms: Vec<u8>,
}
impl LicenseBlock {
#[must_use]
pub fn new(license_id: [u8; 16], licensee_hash: [u8; 32]) -> Self {
Self {
license_id,
licensee_hash,
issued_at: current_unix_time(),
expires_at: 0,
flags: 0,
seat_limit: 0,
query_limit: 0,
custom_terms: Vec::new(),
}
}
#[must_use]
pub fn with_expiration(mut self, expires_at: u64) -> Self {
self.expires_at = expires_at;
self.flags |= flags::EXPIRATION_ENFORCED;
self
}
#[must_use]
pub fn with_seat_limit(mut self, limit: u16) -> Self {
self.seat_limit = limit;
self.flags |= flags::SEATS_ENFORCED;
self
}
#[must_use]
pub fn with_query_limit(mut self, limit: u32) -> Self {
self.query_limit = limit;
self.flags |= flags::QUERY_LIMITED;
self
}
#[must_use]
pub fn with_watermark(mut self) -> Self {
self.flags |= flags::WATERMARKED;
self
}
#[must_use]
pub fn with_revocable(mut self) -> Self {
self.flags |= flags::REVOCABLE;
self
}
#[must_use]
pub fn with_transferable(mut self) -> Self {
self.flags |= flags::TRANSFERABLE;
self
}
#[must_use]
pub fn with_custom_terms(mut self, terms: Vec<u8>) -> Self {
self.custom_terms = terms;
self
}
#[must_use]
pub fn size(&self) -> usize {
LICENSE_BLOCK_FIXED_SIZE + self.custom_terms.len()
}
#[must_use]
pub fn to_bytes(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(self.size() + 4);
buf.extend_from_slice(&self.license_id);
buf.extend_from_slice(&self.licensee_hash);
buf.extend_from_slice(&self.issued_at.to_le_bytes());
buf.extend_from_slice(&self.expires_at.to_le_bytes());
buf.push(self.flags);
buf.extend_from_slice(&self.seat_limit.to_le_bytes());
buf.extend_from_slice(&self.query_limit.to_le_bytes());
#[allow(clippy::cast_possible_truncation)]
let terms_len = self.custom_terms.len() as u32;
buf.extend_from_slice(&terms_len.to_le_bytes());
buf.extend_from_slice(&self.custom_terms);
buf
}
pub fn from_bytes(buf: &[u8]) -> Result<Self> {
const MIN_SIZE: usize = LICENSE_BLOCK_FIXED_SIZE + 4;
if buf.len() < MIN_SIZE {
return Err(Error::Format(format!(
"License block too small: {} bytes, expected at least {}",
buf.len(),
MIN_SIZE
)));
}
let mut offset = 0;
let mut license_id = [0u8; 16];
license_id.copy_from_slice(&buf[offset..offset + 16]);
offset += 16;
let mut licensee_hash = [0u8; 32];
licensee_hash.copy_from_slice(&buf[offset..offset + 32]);
offset += 32;
let issued_at = u64::from_le_bytes([
buf[offset],
buf[offset + 1],
buf[offset + 2],
buf[offset + 3],
buf[offset + 4],
buf[offset + 5],
buf[offset + 6],
buf[offset + 7],
]);
offset += 8;
let expires_at = u64::from_le_bytes([
buf[offset],
buf[offset + 1],
buf[offset + 2],
buf[offset + 3],
buf[offset + 4],
buf[offset + 5],
buf[offset + 6],
buf[offset + 7],
]);
offset += 8;
let flags = buf[offset];
offset += 1;
let seat_limit = u16::from_le_bytes([buf[offset], buf[offset + 1]]);
offset += 2;
let query_limit = u32::from_le_bytes([
buf[offset],
buf[offset + 1],
buf[offset + 2],
buf[offset + 3],
]);
offset += 4;
let terms_len = u32::from_le_bytes([
buf[offset],
buf[offset + 1],
buf[offset + 2],
buf[offset + 3],
]) as usize;
offset += 4;
if buf.len() < offset + terms_len {
return Err(Error::Format(format!(
"License block truncated: expected {} bytes for custom terms",
terms_len
)));
}
let custom_terms = buf[offset..offset + terms_len].to_vec();
Ok(Self {
license_id,
licensee_hash,
issued_at,
expires_at,
flags,
seat_limit,
query_limit,
custom_terms,
})
}
#[must_use]
pub const fn is_expiration_enforced(&self) -> bool {
self.flags & flags::EXPIRATION_ENFORCED != 0
}
#[must_use]
pub const fn is_seats_enforced(&self) -> bool {
self.flags & flags::SEATS_ENFORCED != 0
}
#[must_use]
pub const fn is_query_limited(&self) -> bool {
self.flags & flags::QUERY_LIMITED != 0
}
#[must_use]
pub const fn is_watermarked(&self) -> bool {
self.flags & flags::WATERMARKED != 0
}
#[must_use]
pub const fn is_revocable(&self) -> bool {
self.flags & flags::REVOCABLE != 0
}
#[must_use]
pub const fn is_transferable(&self) -> bool {
self.flags & flags::TRANSFERABLE != 0
}
pub fn verify(&self) -> Result<()> {
if self.is_expiration_enforced() && self.expires_at > 0 {
let now = current_unix_time();
if now > self.expires_at {
return Err(Error::LicenseExpired {
expired_at: self.expires_at,
current_time: now,
});
}
}
Ok(())
}
#[must_use]
pub fn license_id_string(&self) -> String {
format!(
"{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
u32::from_be_bytes([
self.license_id[0],
self.license_id[1],
self.license_id[2],
self.license_id[3]
]),
u16::from_be_bytes([self.license_id[4], self.license_id[5]]),
u16::from_be_bytes([self.license_id[6], self.license_id[7]]),
u16::from_be_bytes([self.license_id[8], self.license_id[9]]),
u64::from_be_bytes([
0,
0,
self.license_id[10],
self.license_id[11],
self.license_id[12],
self.license_id[13],
self.license_id[14],
self.license_id[15]
])
)
}
}
fn current_unix_time() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
#[cfg(feature = "format-encryption")]
pub fn generate_license_id() -> Result<[u8; 16]> {
let mut id = [0u8; 16];
getrandom::getrandom(&mut id).map_err(|e| Error::Format(format!("RNG error: {e}")))?;
id[6] = (id[6] & 0x0F) | 0x40; id[8] = (id[8] & 0x3F) | 0x80;
Ok(id)
}
#[cfg(feature = "format-encryption")]
pub fn hash_licensee(identifier: &str) -> [u8; 32] {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(identifier.as_bytes());
let result = hasher.finalize();
let mut hash = [0u8; 32];
hash.copy_from_slice(&result);
hash
}
#[derive(Debug)]
pub struct LicenseBuilder {
license_id: [u8; 16],
licensee_hash: [u8; 32],
expires_at: Option<u64>,
seat_limit: Option<u16>,
query_limit: Option<u32>,
watermarked: bool,
revocable: bool,
transferable: bool,
custom_terms: Option<Vec<u8>>,
}
impl LicenseBuilder {
#[must_use]
pub fn new(license_id: [u8; 16], licensee_hash: [u8; 32]) -> Self {
Self {
license_id,
licensee_hash,
expires_at: None,
seat_limit: None,
query_limit: None,
watermarked: false,
revocable: false,
transferable: false,
custom_terms: None,
}
}
#[must_use]
pub fn expires_at(mut self, timestamp: u64) -> Self {
self.expires_at = Some(timestamp);
self
}
#[must_use]
pub fn expires_in(mut self, seconds: u64) -> Self {
self.expires_at = Some(current_unix_time() + seconds);
self
}
#[must_use]
pub fn seat_limit(mut self, limit: u16) -> Self {
self.seat_limit = Some(limit);
self
}
#[must_use]
pub fn query_limit(mut self, limit: u32) -> Self {
self.query_limit = Some(limit);
self
}
#[must_use]
pub fn watermarked(mut self) -> Self {
self.watermarked = true;
self
}
#[must_use]
pub fn revocable(mut self) -> Self {
self.revocable = true;
self
}
#[must_use]
pub fn transferable(mut self) -> Self {
self.transferable = true;
self
}
#[must_use]
pub fn custom_terms(mut self, terms: Vec<u8>) -> Self {
self.custom_terms = Some(terms);
self
}
#[must_use]
pub fn build(self) -> LicenseBlock {
let mut license = LicenseBlock::new(self.license_id, self.licensee_hash);
if let Some(expires) = self.expires_at {
license = license.with_expiration(expires);
}
if let Some(seats) = self.seat_limit {
license = license.with_seat_limit(seats);
}
if let Some(queries) = self.query_limit {
license = license.with_query_limit(queries);
}
if self.watermarked {
license = license.with_watermark();
}
if self.revocable {
license = license.with_revocable();
}
if self.transferable {
license = license.with_transferable();
}
if let Some(terms) = self.custom_terms {
license = license.with_custom_terms(terms);
}
license
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_license_block_roundtrip() {
let license_id = [1u8; 16];
let licensee_hash = [2u8; 32];
let license = LicenseBlock::new(license_id, licensee_hash)
.with_expiration(1_800_000_000)
.with_seat_limit(5)
.with_query_limit(10_000)
.with_watermark()
.with_custom_terms(b"Custom license terms".to_vec());
let bytes = license.to_bytes();
let restored = LicenseBlock::from_bytes(&bytes).expect("parse failed");
assert_eq!(restored.license_id, license_id);
assert_eq!(restored.licensee_hash, licensee_hash);
assert_eq!(restored.expires_at, 1_800_000_000);
assert_eq!(restored.seat_limit, 5);
assert_eq!(restored.query_limit, 10_000);
assert!(restored.is_expiration_enforced());
assert!(restored.is_seats_enforced());
assert!(restored.is_query_limited());
assert!(restored.is_watermarked());
assert!(!restored.is_revocable());
assert!(!restored.is_transferable());
assert_eq!(restored.custom_terms, b"Custom license terms");
}
#[test]
fn test_license_block_minimal() {
let license_id = [0u8; 16];
let licensee_hash = [0u8; 32];
let license = LicenseBlock::new(license_id, licensee_hash);
let bytes = license.to_bytes();
let restored = LicenseBlock::from_bytes(&bytes).expect("parse failed");
assert_eq!(restored.flags, 0);
assert_eq!(restored.seat_limit, 0);
assert_eq!(restored.query_limit, 0);
assert!(restored.custom_terms.is_empty());
}
#[test]
fn test_license_expiration_check() {
let license_id = [1u8; 16];
let licensee_hash = [2u8; 32];
let future_time = current_unix_time() + 3600; let valid_license =
LicenseBlock::new(license_id, licensee_hash).with_expiration(future_time);
assert!(valid_license.verify().is_ok());
let past_time = current_unix_time() - 3600; let expired_license =
LicenseBlock::new(license_id, licensee_hash).with_expiration(past_time);
assert!(expired_license.verify().is_err());
}
#[test]
fn test_license_no_expiration() {
let license_id = [1u8; 16];
let licensee_hash = [2u8; 32];
let license = LicenseBlock::new(license_id, licensee_hash);
assert!(license.verify().is_ok());
}
#[test]
fn test_license_builder() {
let license_id = [3u8; 16];
let licensee_hash = [4u8; 32];
let license = LicenseBuilder::new(license_id, licensee_hash)
.expires_in(86400) .seat_limit(10)
.query_limit(50_000)
.watermarked()
.revocable()
.build();
assert!(license.is_expiration_enforced());
assert!(license.is_seats_enforced());
assert!(license.is_query_limited());
assert!(license.is_watermarked());
assert!(license.is_revocable());
assert!(!license.is_transferable());
assert_eq!(license.seat_limit, 10);
assert_eq!(license.query_limit, 50_000);
}
#[test]
fn test_license_id_string() {
let license_id = [
0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, ];
let licensee_hash = [0u8; 32];
let license = LicenseBlock::new(license_id, licensee_hash);
let uuid_str = license.license_id_string();
assert_eq!(uuid_str, "12345678-9abc-def0-1234-56789abcdef0");
}
#[test]
fn test_all_flags() {
let license_id = [5u8; 16];
let licensee_hash = [6u8; 32];
let license = LicenseBlock::new(license_id, licensee_hash)
.with_expiration(1_900_000_000)
.with_seat_limit(1)
.with_query_limit(1)
.with_watermark()
.with_revocable()
.with_transferable();
let expected_flags = flags::EXPIRATION_ENFORCED
| flags::SEATS_ENFORCED
| flags::QUERY_LIMITED
| flags::WATERMARKED
| flags::REVOCABLE
| flags::TRANSFERABLE;
assert_eq!(license.flags, expected_flags);
assert!(license.is_expiration_enforced());
assert!(license.is_seats_enforced());
assert!(license.is_query_limited());
assert!(license.is_watermarked());
assert!(license.is_revocable());
assert!(license.is_transferable());
}
#[test]
fn test_buffer_too_small() {
let small_buf = [0u8; 10];
let result = LicenseBlock::from_bytes(&small_buf);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("too small"));
}
#[cfg(feature = "format-encryption")]
#[test]
fn test_generate_license_id() {
let id1 = generate_license_id().expect("generate failed");
let id2 = generate_license_id().expect("generate failed");
assert_ne!(id1, id2);
assert_eq!((id1[6] >> 4) & 0x0F, 4);
assert_eq!((id2[6] >> 4) & 0x0F, 4);
assert_eq!((id1[8] >> 6) & 0x03, 2);
assert_eq!((id2[8] >> 6) & 0x03, 2);
}
#[cfg(feature = "format-encryption")]
#[test]
fn test_hash_licensee() {
let hash1 = hash_licensee("user@example.com");
let hash2 = hash_licensee("user@example.com");
let hash3 = hash_licensee("other@example.com");
assert_eq!(hash1, hash2);
assert_ne!(hash1, hash3);
assert_eq!(hash1.len(), 32);
}
}