Skip to main content

alimentar/format/
license.rs

1//! Commercial licensing support for .ald format (§9)
2//!
3//! Provides license blocks for commercial dataset distribution with:
4//! - Expiration enforcement
5//! - Seat limits
6//! - Query limits
7//! - Revocation support
8
9use crate::error::{Error, Result};
10
11/// License block size (fixed portion, 71 bytes)
12pub const LICENSE_BLOCK_FIXED_SIZE: usize = 71;
13
14/// License flags (§9.2)
15pub mod flags {
16    /// Limit concurrent installations
17    pub const SEATS_ENFORCED: u8 = 0b0000_0001;
18    /// Dataset stops loading after expires_at
19    pub const EXPIRATION_ENFORCED: u8 = 0b0000_0010;
20    /// Count-based usage cap
21    pub const QUERY_LIMITED: u8 = 0b0000_0100;
22    /// Contains buyer-specific fingerprint (watermarked)
23    pub const WATERMARKED: u8 = 0b0000_1000;
24    /// Can be remotely revoked (requires network)
25    pub const REVOCABLE: u8 = 0b0001_0000;
26    /// License can be resold
27    pub const TRANSFERABLE: u8 = 0b0010_0000;
28}
29
30/// Commercial license block
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct LicenseBlock {
33    /// Unique license identifier (UUID as 16 bytes)
34    pub license_id: [u8; 16],
35    /// SHA-256 hash of licensee identifier
36    pub licensee_hash: [u8; 32],
37    /// Issue timestamp (Unix epoch seconds)
38    pub issued_at: u64,
39    /// Expiration timestamp (Unix epoch seconds, 0 = never)
40    pub expires_at: u64,
41    /// License flags
42    pub flags: u8,
43    /// Maximum concurrent seats (0 = unlimited)
44    pub seat_limit: u16,
45    /// Maximum queries (0 = unlimited)
46    pub query_limit: u32,
47    /// Custom terms (optional, serialized)
48    pub custom_terms: Vec<u8>,
49}
50
51impl LicenseBlock {
52    /// Create a new license block
53    #[must_use]
54    pub fn new(license_id: [u8; 16], licensee_hash: [u8; 32]) -> Self {
55        Self {
56            license_id,
57            licensee_hash,
58            issued_at: current_unix_time(),
59            expires_at: 0,
60            flags: 0,
61            seat_limit: 0,
62            query_limit: 0,
63            custom_terms: Vec::new(),
64        }
65    }
66
67    /// Set expiration timestamp
68    #[must_use]
69    pub fn with_expiration(mut self, expires_at: u64) -> Self {
70        self.expires_at = expires_at;
71        self.flags |= flags::EXPIRATION_ENFORCED;
72        self
73    }
74
75    /// Set seat limit
76    #[must_use]
77    pub fn with_seat_limit(mut self, limit: u16) -> Self {
78        self.seat_limit = limit;
79        self.flags |= flags::SEATS_ENFORCED;
80        self
81    }
82
83    /// Set query limit
84    #[must_use]
85    pub fn with_query_limit(mut self, limit: u32) -> Self {
86        self.query_limit = limit;
87        self.flags |= flags::QUERY_LIMITED;
88        self
89    }
90
91    /// Mark as watermarked
92    #[must_use]
93    pub fn with_watermark(mut self) -> Self {
94        self.flags |= flags::WATERMARKED;
95        self
96    }
97
98    /// Mark as revocable
99    #[must_use]
100    pub fn with_revocable(mut self) -> Self {
101        self.flags |= flags::REVOCABLE;
102        self
103    }
104
105    /// Mark as transferable
106    #[must_use]
107    pub fn with_transferable(mut self) -> Self {
108        self.flags |= flags::TRANSFERABLE;
109        self
110    }
111
112    /// Set custom terms
113    #[must_use]
114    pub fn with_custom_terms(mut self, terms: Vec<u8>) -> Self {
115        self.custom_terms = terms;
116        self
117    }
118
119    /// Total serialized size
120    #[must_use]
121    pub fn size(&self) -> usize {
122        LICENSE_BLOCK_FIXED_SIZE + self.custom_terms.len()
123    }
124
125    /// Serialize to bytes
126    ///
127    /// Format:
128    /// - license_id (16 bytes)
129    /// - licensee_hash (32 bytes)
130    /// - issued_at (8 bytes, LE)
131    /// - expires_at (8 bytes, LE)
132    /// - flags (1 byte)
133    /// - seat_limit (2 bytes, LE)
134    /// - query_limit (4 bytes, LE)
135    /// - custom_terms_len (4 bytes, LE) -- added for deserialization
136    /// - custom_terms (variable)
137    #[must_use]
138    pub fn to_bytes(&self) -> Vec<u8> {
139        let mut buf = Vec::with_capacity(self.size() + 4); // +4 for custom_terms_len
140
141        // license_id (16 bytes)
142        buf.extend_from_slice(&self.license_id);
143
144        // licensee_hash (32 bytes)
145        buf.extend_from_slice(&self.licensee_hash);
146
147        // issued_at (8 bytes)
148        buf.extend_from_slice(&self.issued_at.to_le_bytes());
149
150        // expires_at (8 bytes)
151        buf.extend_from_slice(&self.expires_at.to_le_bytes());
152
153        // flags (1 byte)
154        buf.push(self.flags);
155
156        // seat_limit (2 bytes)
157        buf.extend_from_slice(&self.seat_limit.to_le_bytes());
158
159        // query_limit (4 bytes)
160        buf.extend_from_slice(&self.query_limit.to_le_bytes());
161
162        // custom_terms_len (4 bytes)
163        // Note: terms > 4GB are not supported (reasonable for license terms)
164        #[allow(clippy::cast_possible_truncation)]
165        let terms_len = self.custom_terms.len() as u32;
166        buf.extend_from_slice(&terms_len.to_le_bytes());
167
168        // custom_terms (variable)
169        buf.extend_from_slice(&self.custom_terms);
170
171        buf
172    }
173
174    /// Deserialize from bytes
175    ///
176    /// # Errors
177    ///
178    /// Returns error if buffer is too small or malformed.
179    pub fn from_bytes(buf: &[u8]) -> Result<Self> {
180        // Need at least fixed size + 4 bytes for terms_len
181        const MIN_SIZE: usize = LICENSE_BLOCK_FIXED_SIZE + 4;
182
183        if buf.len() < MIN_SIZE {
184            return Err(Error::Format(format!(
185                "License block too small: {} bytes, expected at least {}",
186                buf.len(),
187                MIN_SIZE
188            )));
189        }
190
191        let mut offset = 0;
192
193        // license_id (16 bytes)
194        let mut license_id = [0u8; 16];
195        license_id.copy_from_slice(&buf[offset..offset + 16]);
196        offset += 16;
197
198        // licensee_hash (32 bytes)
199        let mut licensee_hash = [0u8; 32];
200        licensee_hash.copy_from_slice(&buf[offset..offset + 32]);
201        offset += 32;
202
203        // issued_at (8 bytes)
204        let issued_at = u64::from_le_bytes([
205            buf[offset],
206            buf[offset + 1],
207            buf[offset + 2],
208            buf[offset + 3],
209            buf[offset + 4],
210            buf[offset + 5],
211            buf[offset + 6],
212            buf[offset + 7],
213        ]);
214        offset += 8;
215
216        // expires_at (8 bytes)
217        let expires_at = u64::from_le_bytes([
218            buf[offset],
219            buf[offset + 1],
220            buf[offset + 2],
221            buf[offset + 3],
222            buf[offset + 4],
223            buf[offset + 5],
224            buf[offset + 6],
225            buf[offset + 7],
226        ]);
227        offset += 8;
228
229        // flags (1 byte)
230        let flags = buf[offset];
231        offset += 1;
232
233        // seat_limit (2 bytes)
234        let seat_limit = u16::from_le_bytes([buf[offset], buf[offset + 1]]);
235        offset += 2;
236
237        // query_limit (4 bytes)
238        let query_limit = u32::from_le_bytes([
239            buf[offset],
240            buf[offset + 1],
241            buf[offset + 2],
242            buf[offset + 3],
243        ]);
244        offset += 4;
245
246        // custom_terms_len (4 bytes)
247        let terms_len = u32::from_le_bytes([
248            buf[offset],
249            buf[offset + 1],
250            buf[offset + 2],
251            buf[offset + 3],
252        ]) as usize;
253        offset += 4;
254
255        // custom_terms (variable)
256        if buf.len() < offset + terms_len {
257            return Err(Error::Format(format!(
258                "License block truncated: expected {} bytes for custom terms",
259                terms_len
260            )));
261        }
262
263        let custom_terms = buf[offset..offset + terms_len].to_vec();
264
265        Ok(Self {
266            license_id,
267            licensee_hash,
268            issued_at,
269            expires_at,
270            flags,
271            seat_limit,
272            query_limit,
273            custom_terms,
274        })
275    }
276
277    /// Check if expiration is enforced
278    #[must_use]
279    pub const fn is_expiration_enforced(&self) -> bool {
280        self.flags & flags::EXPIRATION_ENFORCED != 0
281    }
282
283    /// Check if seat limit is enforced
284    #[must_use]
285    pub const fn is_seats_enforced(&self) -> bool {
286        self.flags & flags::SEATS_ENFORCED != 0
287    }
288
289    /// Check if query limit is enforced
290    #[must_use]
291    pub const fn is_query_limited(&self) -> bool {
292        self.flags & flags::QUERY_LIMITED != 0
293    }
294
295    /// Check if watermarked
296    #[must_use]
297    pub const fn is_watermarked(&self) -> bool {
298        self.flags & flags::WATERMARKED != 0
299    }
300
301    /// Check if revocable
302    #[must_use]
303    pub const fn is_revocable(&self) -> bool {
304        self.flags & flags::REVOCABLE != 0
305    }
306
307    /// Check if transferable
308    #[must_use]
309    pub const fn is_transferable(&self) -> bool {
310        self.flags & flags::TRANSFERABLE != 0
311    }
312
313    /// Verify the license is currently valid
314    ///
315    /// # Errors
316    ///
317    /// Returns error if license has expired.
318    pub fn verify(&self) -> Result<()> {
319        if self.is_expiration_enforced() && self.expires_at > 0 {
320            let now = current_unix_time();
321            if now > self.expires_at {
322                return Err(Error::LicenseExpired {
323                    expired_at: self.expires_at,
324                    current_time: now,
325                });
326            }
327        }
328        Ok(())
329    }
330
331    /// Get license ID as UUID string
332    #[must_use]
333    pub fn license_id_string(&self) -> String {
334        format!(
335            "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
336            u32::from_be_bytes([
337                self.license_id[0],
338                self.license_id[1],
339                self.license_id[2],
340                self.license_id[3]
341            ]),
342            u16::from_be_bytes([self.license_id[4], self.license_id[5]]),
343            u16::from_be_bytes([self.license_id[6], self.license_id[7]]),
344            u16::from_be_bytes([self.license_id[8], self.license_id[9]]),
345            u64::from_be_bytes([
346                0,
347                0,
348                self.license_id[10],
349                self.license_id[11],
350                self.license_id[12],
351                self.license_id[13],
352                self.license_id[14],
353                self.license_id[15]
354            ])
355        )
356    }
357}
358
359/// Get current Unix timestamp in seconds
360fn current_unix_time() -> u64 {
361    use std::time::{SystemTime, UNIX_EPOCH};
362
363    SystemTime::now()
364        .duration_since(UNIX_EPOCH)
365        .map(|d| d.as_secs())
366        .unwrap_or(0)
367}
368
369/// Generate a random UUID v4 for license IDs
370///
371/// # Errors
372///
373/// Returns error if random number generation fails.
374#[cfg(feature = "format-encryption")]
375pub fn generate_license_id() -> Result<[u8; 16]> {
376    let mut id = [0u8; 16];
377    getrandom::getrandom(&mut id).map_err(|e| Error::Format(format!("RNG error: {e}")))?;
378
379    // Set version 4 (random) and variant bits
380    id[6] = (id[6] & 0x0F) | 0x40; // Version 4
381    id[8] = (id[8] & 0x3F) | 0x80; // Variant 1
382
383    Ok(id)
384}
385
386/// Hash licensee identifier using SHA-256
387///
388/// This creates a consistent hash for identifying the licensee without
389/// storing the raw identifier.
390#[cfg(feature = "format-encryption")]
391pub fn hash_licensee(identifier: &str) -> [u8; 32] {
392    use sha2::{Digest, Sha256};
393
394    let mut hasher = Sha256::new();
395    hasher.update(identifier.as_bytes());
396    let result = hasher.finalize();
397
398    let mut hash = [0u8; 32];
399    hash.copy_from_slice(&result);
400    hash
401}
402
403/// License builder for convenient construction
404#[derive(Debug)]
405pub struct LicenseBuilder {
406    license_id: [u8; 16],
407    licensee_hash: [u8; 32],
408    expires_at: Option<u64>,
409    seat_limit: Option<u16>,
410    query_limit: Option<u32>,
411    watermarked: bool,
412    revocable: bool,
413    transferable: bool,
414    custom_terms: Option<Vec<u8>>,
415}
416
417impl LicenseBuilder {
418    /// Create a new license builder
419    #[must_use]
420    pub fn new(license_id: [u8; 16], licensee_hash: [u8; 32]) -> Self {
421        Self {
422            license_id,
423            licensee_hash,
424            expires_at: None,
425            seat_limit: None,
426            query_limit: None,
427            watermarked: false,
428            revocable: false,
429            transferable: false,
430            custom_terms: None,
431        }
432    }
433
434    /// Set expiration (Unix timestamp)
435    #[must_use]
436    pub fn expires_at(mut self, timestamp: u64) -> Self {
437        self.expires_at = Some(timestamp);
438        self
439    }
440
441    /// Set expiration relative to now (in seconds)
442    #[must_use]
443    pub fn expires_in(mut self, seconds: u64) -> Self {
444        self.expires_at = Some(current_unix_time() + seconds);
445        self
446    }
447
448    /// Set seat limit
449    #[must_use]
450    pub fn seat_limit(mut self, limit: u16) -> Self {
451        self.seat_limit = Some(limit);
452        self
453    }
454
455    /// Set query limit
456    #[must_use]
457    pub fn query_limit(mut self, limit: u32) -> Self {
458        self.query_limit = Some(limit);
459        self
460    }
461
462    /// Mark as watermarked
463    #[must_use]
464    pub fn watermarked(mut self) -> Self {
465        self.watermarked = true;
466        self
467    }
468
469    /// Mark as revocable
470    #[must_use]
471    pub fn revocable(mut self) -> Self {
472        self.revocable = true;
473        self
474    }
475
476    /// Mark as transferable
477    #[must_use]
478    pub fn transferable(mut self) -> Self {
479        self.transferable = true;
480        self
481    }
482
483    /// Set custom terms
484    #[must_use]
485    pub fn custom_terms(mut self, terms: Vec<u8>) -> Self {
486        self.custom_terms = Some(terms);
487        self
488    }
489
490    /// Build the license block
491    #[must_use]
492    pub fn build(self) -> LicenseBlock {
493        let mut license = LicenseBlock::new(self.license_id, self.licensee_hash);
494
495        if let Some(expires) = self.expires_at {
496            license = license.with_expiration(expires);
497        }
498
499        if let Some(seats) = self.seat_limit {
500            license = license.with_seat_limit(seats);
501        }
502
503        if let Some(queries) = self.query_limit {
504            license = license.with_query_limit(queries);
505        }
506
507        if self.watermarked {
508            license = license.with_watermark();
509        }
510
511        if self.revocable {
512            license = license.with_revocable();
513        }
514
515        if self.transferable {
516            license = license.with_transferable();
517        }
518
519        if let Some(terms) = self.custom_terms {
520            license = license.with_custom_terms(terms);
521        }
522
523        license
524    }
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530
531    #[test]
532    fn test_license_block_roundtrip() {
533        let license_id = [1u8; 16];
534        let licensee_hash = [2u8; 32];
535
536        let license = LicenseBlock::new(license_id, licensee_hash)
537            .with_expiration(1_800_000_000)
538            .with_seat_limit(5)
539            .with_query_limit(10_000)
540            .with_watermark()
541            .with_custom_terms(b"Custom license terms".to_vec());
542
543        let bytes = license.to_bytes();
544        let restored = LicenseBlock::from_bytes(&bytes).expect("parse failed");
545
546        assert_eq!(restored.license_id, license_id);
547        assert_eq!(restored.licensee_hash, licensee_hash);
548        assert_eq!(restored.expires_at, 1_800_000_000);
549        assert_eq!(restored.seat_limit, 5);
550        assert_eq!(restored.query_limit, 10_000);
551        assert!(restored.is_expiration_enforced());
552        assert!(restored.is_seats_enforced());
553        assert!(restored.is_query_limited());
554        assert!(restored.is_watermarked());
555        assert!(!restored.is_revocable());
556        assert!(!restored.is_transferable());
557        assert_eq!(restored.custom_terms, b"Custom license terms");
558    }
559
560    #[test]
561    fn test_license_block_minimal() {
562        let license_id = [0u8; 16];
563        let licensee_hash = [0u8; 32];
564
565        let license = LicenseBlock::new(license_id, licensee_hash);
566        let bytes = license.to_bytes();
567        let restored = LicenseBlock::from_bytes(&bytes).expect("parse failed");
568
569        assert_eq!(restored.flags, 0);
570        assert_eq!(restored.seat_limit, 0);
571        assert_eq!(restored.query_limit, 0);
572        assert!(restored.custom_terms.is_empty());
573    }
574
575    #[test]
576    fn test_license_expiration_check() {
577        let license_id = [1u8; 16];
578        let licensee_hash = [2u8; 32];
579
580        // Non-expired license
581        let future_time = current_unix_time() + 3600; // 1 hour from now
582        let valid_license =
583            LicenseBlock::new(license_id, licensee_hash).with_expiration(future_time);
584        assert!(valid_license.verify().is_ok());
585
586        // Expired license
587        let past_time = current_unix_time() - 3600; // 1 hour ago
588        let expired_license =
589            LicenseBlock::new(license_id, licensee_hash).with_expiration(past_time);
590        assert!(expired_license.verify().is_err());
591    }
592
593    #[test]
594    fn test_license_no_expiration() {
595        let license_id = [1u8; 16];
596        let licensee_hash = [2u8; 32];
597
598        // License without expiration enforcement should always be valid
599        let license = LicenseBlock::new(license_id, licensee_hash);
600        assert!(license.verify().is_ok());
601    }
602
603    #[test]
604    fn test_license_builder() {
605        let license_id = [3u8; 16];
606        let licensee_hash = [4u8; 32];
607
608        let license = LicenseBuilder::new(license_id, licensee_hash)
609            .expires_in(86400) // 24 hours
610            .seat_limit(10)
611            .query_limit(50_000)
612            .watermarked()
613            .revocable()
614            .build();
615
616        assert!(license.is_expiration_enforced());
617        assert!(license.is_seats_enforced());
618        assert!(license.is_query_limited());
619        assert!(license.is_watermarked());
620        assert!(license.is_revocable());
621        assert!(!license.is_transferable());
622        assert_eq!(license.seat_limit, 10);
623        assert_eq!(license.query_limit, 50_000);
624    }
625
626    #[test]
627    fn test_license_id_string() {
628        let license_id = [
629            0x12, 0x34, 0x56, 0x78, // time_low
630            0x9A, 0xBC, // time_mid
631            0xDE, 0xF0, // time_hi_and_version
632            0x12, 0x34, // clock_seq
633            0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, // node
634        ];
635        let licensee_hash = [0u8; 32];
636
637        let license = LicenseBlock::new(license_id, licensee_hash);
638        let uuid_str = license.license_id_string();
639
640        assert_eq!(uuid_str, "12345678-9abc-def0-1234-56789abcdef0");
641    }
642
643    #[test]
644    fn test_all_flags() {
645        let license_id = [5u8; 16];
646        let licensee_hash = [6u8; 32];
647
648        let license = LicenseBlock::new(license_id, licensee_hash)
649            .with_expiration(1_900_000_000)
650            .with_seat_limit(1)
651            .with_query_limit(1)
652            .with_watermark()
653            .with_revocable()
654            .with_transferable();
655
656        let expected_flags = flags::EXPIRATION_ENFORCED
657            | flags::SEATS_ENFORCED
658            | flags::QUERY_LIMITED
659            | flags::WATERMARKED
660            | flags::REVOCABLE
661            | flags::TRANSFERABLE;
662
663        assert_eq!(license.flags, expected_flags);
664
665        // Verify all flag checks
666        assert!(license.is_expiration_enforced());
667        assert!(license.is_seats_enforced());
668        assert!(license.is_query_limited());
669        assert!(license.is_watermarked());
670        assert!(license.is_revocable());
671        assert!(license.is_transferable());
672    }
673
674    #[test]
675    fn test_buffer_too_small() {
676        let small_buf = [0u8; 10];
677        let result = LicenseBlock::from_bytes(&small_buf);
678        assert!(result.is_err());
679        assert!(result.unwrap_err().to_string().contains("too small"));
680    }
681
682    #[cfg(feature = "format-encryption")]
683    #[test]
684    fn test_generate_license_id() {
685        let id1 = generate_license_id().expect("generate failed");
686        let id2 = generate_license_id().expect("generate failed");
687
688        // IDs should be different
689        assert_ne!(id1, id2);
690
691        // Version should be 4
692        assert_eq!((id1[6] >> 4) & 0x0F, 4);
693        assert_eq!((id2[6] >> 4) & 0x0F, 4);
694
695        // Variant should be 1 (bits 10xx)
696        assert_eq!((id1[8] >> 6) & 0x03, 2);
697        assert_eq!((id2[8] >> 6) & 0x03, 2);
698    }
699
700    #[cfg(feature = "format-encryption")]
701    #[test]
702    fn test_hash_licensee() {
703        let hash1 = hash_licensee("user@example.com");
704        let hash2 = hash_licensee("user@example.com");
705        let hash3 = hash_licensee("other@example.com");
706
707        // Same input produces same hash
708        assert_eq!(hash1, hash2);
709
710        // Different input produces different hash
711        assert_ne!(hash1, hash3);
712
713        // Hash is 32 bytes
714        assert_eq!(hash1.len(), 32);
715    }
716}