1use crate::anti_tamper::HardwareFingerprint;
7use crate::error::{LicenseError, Result};
8use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11use sha2::{Digest, Sha256};
12use std::collections::BTreeMap;
13use std::io::Write;
14use std::path::Path;
15use uuid::Uuid;
16
17use super::{
18 detect_format, SneakernetFormat, MAX_SNEAKERNET_JSON_PAYLOAD, REQUEST_MAGIC,
19 REQUEST_TEXT_PREFIX, REQUEST_TEXT_SUFFIX, REQUEST_VERSION,
20};
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ActivationRequest {
28 pub request_id: Uuid,
30
31 pub fingerprint: HardwareFingerprint,
33
34 pub product_id: String,
36
37 #[serde(default, skip_serializing_if = "Vec::is_empty")]
39 pub requested_features: Vec<String>,
40
41 pub timestamp: DateTime<Utc>,
43
44 pub version: u8,
46
47 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
52 pub metadata: BTreeMap<String, String>,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
57 pub checksum: Option<String>,
58}
59
60impl ActivationRequest {
61 pub fn builder() -> ActivationRequestBuilder {
63 ActivationRequestBuilder::new()
64 }
65
66 pub fn load(path: &Path) -> Result<Self> {
68 let data = std::fs::read(path)?;
69 Self::from_bytes(&data)
70 }
71
72 pub fn from_bytes(data: &[u8]) -> Result<Self> {
74 match detect_format(data) {
75 Some(SneakernetFormat::Binary) => Self::from_binary(data),
76 Some(SneakernetFormat::Text) => {
77 let text = std::str::from_utf8(data)
78 .map_err(|e| LicenseError::InvalidLicenseFormat(e.to_string()))?;
79 Self::from_base64(text)
80 }
81 None => Err(LicenseError::InvalidLicenseFormat(
82 "Unknown activation request format".to_string(),
83 )),
84 }
85 }
86
87 pub fn from_binary(data: &[u8]) -> Result<Self> {
89 if data.len() < 9 {
90 return Err(LicenseError::InvalidLicenseFormat(
91 "Activation request too short".to_string(),
92 ));
93 }
94
95 if &data[0..4] != REQUEST_MAGIC {
97 return Err(LicenseError::InvalidLicenseFormat(
98 "Invalid activation request magic header".to_string(),
99 ));
100 }
101
102 let version = data[4];
104 if version > REQUEST_VERSION {
105 return Err(LicenseError::InvalidLicenseFormat(format!(
106 "Unsupported activation request version: {} (max supported: {})",
107 version, REQUEST_VERSION
108 )));
109 }
110
111 let len = u32::from_le_bytes([data[5], data[6], data[7], data[8]]) as usize;
113
114 if len > MAX_SNEAKERNET_JSON_PAYLOAD {
115 return Err(LicenseError::InvalidLicenseFormat(format!(
116 "Activation request payload exceeds maximum of {} bytes",
117 MAX_SNEAKERNET_JSON_PAYLOAD
118 )));
119 }
120
121 if data.len() < 9 + len {
122 return Err(LicenseError::InvalidLicenseFormat(
123 "Activation request data truncated".to_string(),
124 ));
125 }
126
127 let request: Self = serde_json::from_slice(&data[9..9 + len])
129 .map_err(|e| LicenseError::InvalidLicenseFormat(e.to_string()))?;
130
131 if let Some(ref stored_checksum) = request.checksum {
133 let computed = request.compute_checksum();
134 if &computed != stored_checksum {
135 return Err(LicenseError::InvalidLicenseFormat(
136 "Activation request checksum mismatch - data may be corrupted".to_string(),
137 ));
138 }
139 }
140
141 Ok(request)
142 }
143
144 pub fn from_base64(text: &str) -> Result<Self> {
146 let trimmed = text.trim();
147
148 let base64_content = if trimmed.starts_with(REQUEST_TEXT_PREFIX) {
150 trimmed
151 .strip_prefix(REQUEST_TEXT_PREFIX)
152 .and_then(|s| s.strip_suffix(REQUEST_TEXT_SUFFIX))
153 .map(|s| s.trim())
154 .ok_or_else(|| {
155 LicenseError::InvalidLicenseFormat(
156 "Malformed activation request text format".to_string(),
157 )
158 })?
159 } else {
160 trimmed
161 };
162
163 let clean_base64: String = base64_content
165 .chars()
166 .filter(|c| !c.is_whitespace())
167 .collect();
168
169 let binary = BASE64
171 .decode(&clean_base64)
172 .map_err(|e| LicenseError::InvalidLicenseFormat(format!("Invalid base64: {}", e)))?;
173
174 Self::from_binary(&binary)
176 }
177
178 pub fn to_binary(&self) -> Result<Vec<u8>> {
180 let mut output = Vec::new();
181
182 output.write_all(REQUEST_MAGIC)?;
184
185 output.write_all(&[REQUEST_VERSION])?;
187
188 let encoded = serde_json::to_vec(self)
190 .map_err(|e| LicenseError::SerializationError(e.to_string()))?;
191
192 let len = encoded.len() as u32;
194 output.write_all(&len.to_le_bytes())?;
195
196 output.write_all(&encoded)?;
198
199 Ok(output)
200 }
201
202 pub fn to_base64(&self) -> Result<String> {
204 let binary = self.to_binary()?;
205 let base64_content = BASE64.encode(&binary);
206
207 let wrapped: Vec<&str> = base64_content
209 .as_bytes()
210 .chunks(64)
211 .map(|chunk| std::str::from_utf8(chunk).unwrap())
212 .collect();
213
214 Ok(format!(
215 "{}\n{}\n{}",
216 REQUEST_TEXT_PREFIX,
217 wrapped.join("\n"),
218 REQUEST_TEXT_SUFFIX
219 ))
220 }
221
222 pub fn save_binary(&self, path: &Path) -> Result<()> {
224 let binary = self.to_binary()?;
225 std::fs::write(path, binary)?;
226 Ok(())
227 }
228
229 pub fn save_text(&self, path: &Path) -> Result<()> {
231 let text = self.to_base64()?;
232 std::fs::write(path, text)?;
233 Ok(())
234 }
235
236 fn compute_checksum(&self) -> String {
238 let mut hasher = Sha256::new();
239
240 hasher.update(self.request_id.as_bytes());
242 hasher.update(self.product_id.as_bytes());
243 hasher.update(self.fingerprint.combined_hash.as_bytes());
244 hasher.update(self.timestamp.to_rfc3339().as_bytes());
245 hasher.update([self.version]);
246
247 for feature in &self.requested_features {
248 hasher.update(feature.as_bytes());
249 }
250
251 for (key, value) in &self.metadata {
252 hasher.update(key.as_bytes());
253 hasher.update(value.as_bytes());
254 }
255
256 hex::encode(hasher.finalize())
257 }
258
259 pub fn verify_integrity(&self) -> bool {
261 match &self.checksum {
262 Some(stored) => &self.compute_checksum() == stored,
263 None => true, }
265 }
266}
267
268#[derive(Default)]
270pub struct ActivationRequestBuilder {
271 fingerprint: Option<HardwareFingerprint>,
272 product_id: Option<String>,
273 requested_features: Vec<String>,
274 metadata: BTreeMap<String, String>,
275}
276
277impl ActivationRequestBuilder {
278 pub fn new() -> Self {
280 Self::default()
281 }
282
283 pub fn fingerprint(mut self, fingerprint: HardwareFingerprint) -> Self {
285 self.fingerprint = Some(fingerprint);
286 self
287 }
288
289 pub fn fingerprint_current(mut self) -> Self {
291 self.fingerprint = Some(HardwareFingerprint::generate());
292 self
293 }
294
295 pub fn product_id(mut self, product_id: impl Into<String>) -> Self {
297 self.product_id = Some(product_id.into());
298 self
299 }
300
301 pub fn feature(mut self, feature: impl Into<String>) -> Self {
303 self.requested_features.push(feature.into());
304 self
305 }
306
307 pub fn features(mut self, features: impl IntoIterator<Item = impl Into<String>>) -> Self {
309 self.requested_features
310 .extend(features.into_iter().map(|f| f.into()));
311 self
312 }
313
314 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
316 self.metadata.insert(key.into(), value.into());
317 self
318 }
319
320 pub fn customer_name(self, name: impl Into<String>) -> Self {
322 self.metadata("customer_name", name)
323 }
324
325 pub fn customer_email(self, email: impl Into<String>) -> Self {
327 self.metadata("customer_email", email)
328 }
329
330 pub fn order_reference(self, reference: impl Into<String>) -> Self {
332 self.metadata("order_reference", reference)
333 }
334
335 pub fn build(self) -> Result<ActivationRequest> {
337 let fingerprint = self.fingerprint.unwrap_or_default();
338 let product_id = self
339 .product_id
340 .ok_or_else(|| LicenseError::MissingField("product_id".into()))?;
341
342 let mut request = ActivationRequest {
343 request_id: Uuid::new_v4(),
344 fingerprint,
345 product_id,
346 requested_features: self.requested_features,
347 timestamp: Utc::now(),
348 version: REQUEST_VERSION,
349 metadata: self.metadata,
350 checksum: None,
351 };
352
353 request.checksum = Some(request.compute_checksum());
355
356 Ok(request)
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363
364 fn create_test_fingerprint() -> HardwareFingerprint {
365 HardwareFingerprint {
366 mac_hashes: vec!["abc123".to_string()],
367 disk_hashes: vec!["def456".to_string()],
368 hostname_hash: Some("host789".to_string()),
369 machine_guid_hash: Some("guid012".to_string()),
370 combined_hash: "combined345".to_string(),
371 }
372 }
373
374 #[test]
375 fn test_request_builder() {
376 let request = ActivationRequest::builder()
377 .product_id("TEST-PRODUCT")
378 .fingerprint(create_test_fingerprint())
379 .feature("basic")
380 .feature("premium")
381 .customer_name("John Doe")
382 .customer_email("john@example.com")
383 .build()
384 .unwrap();
385
386 assert_eq!(request.product_id, "TEST-PRODUCT");
387 assert_eq!(request.requested_features.len(), 2);
388 assert!(request.requested_features.contains(&"basic".to_string()));
389 assert!(request.requested_features.contains(&"premium".to_string()));
390 assert_eq!(
391 request.metadata.get("customer_name"),
392 Some(&"John Doe".to_string())
393 );
394 assert!(request.checksum.is_some());
395 }
396
397 #[test]
398 fn test_request_builder_missing_product_id() {
399 let result = ActivationRequest::builder()
400 .fingerprint(create_test_fingerprint())
401 .build();
402
403 assert!(result.is_err());
404 }
405
406 #[test]
407 fn test_binary_serialization_roundtrip() {
408 let request = ActivationRequest::builder()
409 .product_id("MY-APP")
410 .fingerprint(create_test_fingerprint())
411 .feature("pro")
412 .build()
413 .unwrap();
414
415 let binary = request.to_binary().unwrap();
416
417 assert_eq!(&binary[0..4], REQUEST_MAGIC);
419 assert_eq!(binary[4], REQUEST_VERSION);
420
421 let parsed = ActivationRequest::from_binary(&binary).unwrap();
423
424 assert_eq!(parsed.request_id, request.request_id);
425 assert_eq!(parsed.product_id, request.product_id);
426 assert_eq!(parsed.requested_features, request.requested_features);
427 assert_eq!(parsed.checksum, request.checksum);
428 }
429
430 #[test]
431 fn test_base64_serialization_roundtrip() {
432 let request = ActivationRequest::builder()
433 .product_id("MY-APP")
434 .fingerprint(create_test_fingerprint())
435 .feature("enterprise")
436 .customer_name("Acme Corp")
437 .build()
438 .unwrap();
439
440 let text = request.to_base64().unwrap();
441
442 assert!(text.starts_with(REQUEST_TEXT_PREFIX));
444 assert!(text.ends_with(REQUEST_TEXT_SUFFIX));
445
446 let parsed = ActivationRequest::from_base64(&text).unwrap();
448
449 assert_eq!(parsed.request_id, request.request_id);
450 assert_eq!(parsed.product_id, request.product_id);
451 assert_eq!(parsed.requested_features, request.requested_features);
452 assert_eq!(
453 parsed.metadata.get("customer_name"),
454 Some(&"Acme Corp".to_string())
455 );
456 }
457
458 #[test]
459 fn test_auto_detect_format() {
460 let request = ActivationRequest::builder()
461 .product_id("AUTO-DETECT")
462 .fingerprint(create_test_fingerprint())
463 .build()
464 .unwrap();
465
466 let binary = request.to_binary().unwrap();
468 let parsed_binary = ActivationRequest::from_bytes(&binary).unwrap();
469 assert_eq!(parsed_binary.product_id, "AUTO-DETECT");
470
471 let text = request.to_base64().unwrap();
473 let parsed_text = ActivationRequest::from_bytes(text.as_bytes()).unwrap();
474 assert_eq!(parsed_text.product_id, "AUTO-DETECT");
475 }
476
477 #[test]
478 fn test_integrity_verification() {
479 let request = ActivationRequest::builder()
480 .product_id("INTEGRITY-TEST")
481 .fingerprint(create_test_fingerprint())
482 .build()
483 .unwrap();
484
485 assert!(request.verify_integrity());
486 }
487
488 #[test]
489 fn test_invalid_magic_header() {
490 let mut bad_data = vec![b'B', b'A', b'D', b'!'];
491 bad_data.extend_from_slice(&[1, 0, 0, 0, 0]);
492
493 let result = ActivationRequest::from_binary(&bad_data);
494 assert!(result.is_err());
495 }
496
497 #[test]
498 fn test_truncated_data() {
499 let request = ActivationRequest::builder()
500 .product_id("TRUNCATE-TEST")
501 .fingerprint(create_test_fingerprint())
502 .build()
503 .unwrap();
504
505 let binary = request.to_binary().unwrap();
506
507 let truncated = &binary[0..20];
509
510 let result = ActivationRequest::from_binary(truncated);
511 assert!(result.is_err());
512 }
513}