1use base64::Engine as _;
2use base64::engine::general_purpose::URL_SAFE_NO_PAD;
3use bevy::prelude::*;
4use ring::signature::{ED25519, Ed25519KeyPair, KeyPair, UnparsedPublicKey};
5
6use secure_gate::{ExposeSecret, dynamic_alias, fixed_alias};
7
8mod asset_loader;
9pub mod encrypt_key_registry;
10mod ext;
11
12#[macro_use]
13mod macros;
14
15mod pack_format;
16
17pub use asset_loader::{
18 DlcLoader, DlcPack, DlcPackLoader, DlcPackLoaderSettings, EncryptedAsset, parse_encrypted,
19};
20
21pub use pack_format::{
22 BlockMetadata, CompressionLevel, DEFAULT_BLOCK_SIZE, DLC_PACK_MAGIC, DLC_PACK_VERSION_LATEST,
23 ManifestEntry, V4ManifestEntry, is_data_executable,
24 pack_encrypted_pack, parse_encrypted_pack,
25};
26
27use serde::{Deserialize, Serialize};
28
29use thiserror::Error;
30
31use crate::asset_loader::DlcPackLoaded;
32
33#[allow(unused_imports)]
34pub use bevy_dlc_macro::include_signed_license_aes;
35#[doc(hidden)]
36pub use crate::macros::__decode_embedded_signed_license_aes;
37
38pub use crate::ext::AppExt;
39
40pub fn register_encryption_key(dlc_id: &str, key: EncryptionKey) {
47 encrypt_key_registry::insert(dlc_id, key);
48}
49
50pub mod prelude {
51 pub use crate::ext::*;
52 pub use crate::{
53 DlcError, DlcId, DlcKey, DlcLoader, DlcPack, DlcPackLoader, DlcPackLoaderSettings,
54 DlcPlugin, EncryptedAsset, PackItem, Product, SignedLicense,
55 asset_loader::DlcPackEntry, asset_loader::DlcPackLoaded, is_dlc_entry_loaded,
56 is_dlc_loaded,
57 };
58 pub use bevy_dlc_macro::include_signed_license_aes;
59 pub use crate::include_dlc_key_and_license_aes;
60}
61
62pub struct DlcPlugin {
63 dlc_key: DlcKey,
64
65 signed_license: SignedLicense,
66}
67
68impl DlcPlugin {
69 pub(crate) fn new(dlc_key: DlcKey, signed_license: SignedLicense) -> Self {
74 Self {
75 dlc_key,
76 signed_license,
77 }
78 }
79}
80
81impl From<(DlcKey, SignedLicense)> for DlcPlugin {
82 fn from(tuple: (DlcKey, SignedLicense)) -> Self {
83 DlcPlugin::new(tuple.0, tuple.1)
84 }
85}
86
87impl Plugin for DlcPlugin {
88 fn build(&self, app: &mut App) {
89 if let Some(encrypt_key) = extract_encrypt_key_from_license(&self.signed_license) {
90 let dlcs = extract_dlc_ids_from_license(&self.signed_license);
91 for dlc_id in dlcs {
92 let key_for_dlc = encrypt_key.with_secret(|kb| EncryptionKey::new(*kb));
96 encrypt_key_registry::insert(&dlc_id, key_for_dlc);
97 }
98 }
99
100 app.init_resource::<asset_loader::DlcPackRegistrarFactories>();
101
102 app.insert_resource(self.dlc_key.clone())
103 .init_asset_loader::<asset_loader::DlcLoader<Image>>()
104 .init_asset_loader::<asset_loader::DlcLoader<Scene>>()
105 .init_asset_loader::<asset_loader::DlcLoader<Mesh>>()
106 .init_asset_loader::<asset_loader::DlcLoader<Font>>()
107 .init_asset_loader::<asset_loader::DlcLoader<AudioSource>>()
108 .init_asset_loader::<asset_loader::DlcLoader<ColorMaterial>>()
109 .init_asset_loader::<asset_loader::DlcLoader<StandardMaterial>>()
110 .init_asset_loader::<asset_loader::DlcLoader<Gltf>>()
111 .init_asset_loader::<asset_loader::DlcLoader<bevy::gltf::GltfMesh>>()
112 .init_asset_loader::<asset_loader::DlcLoader<Shader>>()
113 .init_asset_loader::<asset_loader::DlcLoader<DynamicScene>>()
114 .init_asset_loader::<asset_loader::DlcLoader<AnimationClip>>()
115 .init_asset_loader::<asset_loader::DlcLoader<AnimationGraph>>();
116
117 let factories = app
118 .world()
119 .get_resource::<asset_loader::DlcPackRegistrarFactories>()
120 .cloned();
121 let pack_loader = asset_loader::DlcPackLoader {
122 registrars: asset_loader::collect_pack_registrars(factories.as_ref()),
123 factories,
124 };
125
126 app.register_asset_loader(pack_loader);
127 app.init_asset::<asset_loader::DlcPack>();
128
129 app.add_systems(Update, trigger_dlc_events);
130 }
131}
132
133
134fn trigger_dlc_events(
136 mut events: MessageReader<AssetEvent<DlcPack>>,
137 packs: Res<Assets<DlcPack>>,
138 mut commands: Commands,
139) {
140 for event in events.read() {
141 match event {
142 AssetEvent::Added { id } => {
143 if let Some(pack) = packs.get(*id) {
144 let dlc_id = pack.id().clone();
145 commands.trigger(DlcPackLoaded::new(dlc_id.clone(), pack.clone()));
146 }
147 }
148 _ => {}
149 }
150 }
151}
152
153pub fn is_dlc_loaded(dlc_id: impl Into<DlcId>) -> impl Fn() -> bool + Send + Sync + 'static {
172 let id_string = dlc_id.into().0;
173 move || !encrypt_key_registry::asset_path_for(&id_string).is_none()
174}
175
176pub fn is_dlc_entry_loaded(
178 dlc_id: impl Into<DlcId>,
179 entry: impl Into<String>,
180) -> impl Fn(Res<Assets<DlcPack>>) -> bool + Send + Sync + 'static {
181 let id_string = dlc_id.into().0;
182 let entry_name = entry.into();
183 move |dlc_packs: Res<Assets<DlcPack>>| {
184 if !encrypt_key_registry::asset_path_for(&id_string).is_none() {
185 dlc_packs
186 .iter()
187 .filter(|p| p.1.id() == &DlcId::from(id_string.clone()))
188 .any(|pack| pack.1.find_entry(&entry_name).is_some())
189 } else {
190 false
191 }
192 }
193}
194
195#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
197#[serde(transparent)]
198pub struct DlcId(pub String);
199
200impl std::fmt::Display for DlcId {
201 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202 self.0.fmt(f)
203 }
204}
205
206impl Clone for DlcId {
207 fn clone(&self) -> Self {
208 DlcId(self.0.clone())
209 }
210}
211
212impl DlcId {
213 pub fn as_str(&self) -> &str {
215 self.0.as_str()
216 }
217}
218
219impl From<&str> for DlcId {
220 fn from(s: &str) -> Self {
221 DlcId(s.to_owned())
222 }
223}
224
225impl From<String> for DlcId {
226 fn from(s: String) -> Self {
227 DlcId(s)
228 }
229}
230
231impl AsRef<str> for DlcId {
232 fn as_ref(&self) -> &str {
233 &self.0
234 }
235}
236
237fixed_alias!(pub PrivateKey, 32, "A secure wrapper for a 32-byte Ed25519 signing seed (private key) used to create signed licenses. This should be protected and never exposed in logs or error messages.");
238
239#[derive(Clone, Copy, PartialEq, Eq)]
241pub struct PublicKey(pub [u8; 32]);
242
243impl AsRef<[u8]> for PublicKey {
244 fn as_ref(&self) -> &[u8] {
245 &self.0
246 }
247}
248
249impl std::fmt::Debug for PublicKey {
250 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251 write!(f, "PublicKey({} bytes)", 32)
252 }
253}
254
255dynamic_alias!(pub SignedLicense, String, "A compact offline-signed token containing DLC ids and an optional embedded encrypt key. Treat as sensitive and do not leak raw secrets in logs. This ");
256
257pub fn extract_encrypt_key_from_license(license: &SignedLicense) -> Option<EncryptionKey> {
260 license.with_secret(|token_str| {
261 let parts: Vec<&str> = token_str.split('.').collect();
262 if parts.len() != 2 {
263 return None;
264 }
265 let payload = URL_SAFE_NO_PAD.decode(parts[0].as_bytes()).ok()?;
266 let payload_json: serde_json::Value = serde_json::from_slice(&payload).ok()?;
267 let key_b64 = payload_json.get("encrypt_key").and_then(|v| v.as_str())?;
268 URL_SAFE_NO_PAD
269 .decode(key_b64.as_bytes())
270 .ok()
271 .and_then(|key_bytes| Some(EncryptionKey::new(key_bytes.try_into().ok()?)))
272 })
273}
274
275pub fn extract_dlc_ids_from_license(license: &SignedLicense) -> Vec<String> {
278 license.with_secret(|token_str| {
279 let parts: Vec<&str> = token_str.split('.').collect();
280 if parts.len() != 2 {
281 return Vec::new();
282 }
283 if let Ok(payload) = URL_SAFE_NO_PAD.decode(parts[0].as_bytes()) {
284 if let Ok(payload_json) = serde_json::from_slice::<serde_json::Value>(&payload) {
285 if let Some(dlcs_array) = payload_json.get("dlcs").and_then(|v| v.as_array()) {
286 let mut dlcs = Vec::new();
287 for dlc in dlcs_array {
288 if let Some(dlc_id) = dlc.as_str() {
289 dlcs.push(dlc_id.to_string());
290 }
291 }
292 return dlcs;
293 }
294 }
295 }
296 Vec::new()
297 })
298}
299
300pub fn extract_product_from_license(license: &SignedLicense) -> Option<String> {
303 license.with_secret(|token_str| {
304 let parts: Vec<&str> = token_str.split('.').collect();
305 if parts.len() != 2 {
306 return None;
307 }
308 if let Ok(payload) = URL_SAFE_NO_PAD.decode(parts[0].as_bytes()) {
309 if let Ok(payload_json) = serde_json::from_slice::<serde_json::Value>(&payload) {
310 if let Some(prod) = payload_json.get("product").and_then(|v| v.as_str()) {
311 return Some(prod.to_string());
312 }
313 }
314 }
315 None
316 })
317}
318#[derive(Resource, Clone, PartialEq, Eq, Debug)]
320pub struct Product(String);
321
322impl std::fmt::Display for Product {
323 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
324 self.0.fmt(f)
325 }
326}
327
328impl AsRef<str> for Product {
329 fn as_ref(&self) -> &str {
330 &self.0
331 }
332}
333
334impl Product {
335 pub fn as_str(&self) -> &str {
337 self.0.as_str()
338 }
339}
340
341impl From<String> for Product {
342 fn from(s: String) -> Self {
343 Product(s)
344 }
345}
346impl From<&str> for Product {
347 fn from(s: &str) -> Self {
348 Product(s.to_owned())
349 }
350}
351
352fixed_alias!(pub EncryptionKey, 32, "A secure encrypt key (symmetric key for encrypting DLC pack entries). This should be protected and never exposed in logs or error messages.");
353
354#[derive(Resource)]
357pub enum DlcKey {
358 Private {
360 privkey: PrivateKey,
362 pubkey: PublicKey,
364 },
365
366 Public {
368 pubkey: PublicKey,
370 },
371}
372
373impl DlcKey {
374 pub fn new(pubkey: &str, privkey: &str) -> Result<Self, DlcError> {
375 let decoded_pub = URL_SAFE_NO_PAD
376 .decode(pubkey.as_bytes())
377 .map_err(|e| DlcError::CryptoError(format!("invalid pubkey base64: {}", e)))?;
378 if decoded_pub.len() != 32 {
379 return Err(DlcError::CryptoError("public key must be 32 bytes".into()));
380 }
381 let mut pub_bytes = [0u8; 32];
382 pub_bytes.copy_from_slice(&decoded_pub);
383
384 let decoded_priv = URL_SAFE_NO_PAD
385 .decode(privkey.as_bytes())
386 .map_err(|e| DlcError::CryptoError(format!("invalid privkey base64: {}", e)))?;
387 if decoded_priv.len() != 32 {
388 return Err(DlcError::CryptoError("private key must be 32 bytes".into()));
389 }
390 let mut priv_bytes = [0u8; 32];
391 priv_bytes.copy_from_slice(&decoded_priv);
392
393 Self::from_priv_and_pub(PrivateKey::from(priv_bytes), PublicKey(pub_bytes))
394 }
395
396 pub fn public(pubkey: &str) -> Result<Self, DlcError> {
398 let decoded_pub = URL_SAFE_NO_PAD
399 .decode(pubkey.as_bytes())
400 .map_err(|e| DlcError::CryptoError(format!("invalid pubkey base64: {}", e)))?;
401 if decoded_pub.len() != 32 {
402 return Err(DlcError::CryptoError("public key must be 32 bytes".into()));
403 }
404 let mut pub_bytes = [0u8; 32];
405 pub_bytes.copy_from_slice(&decoded_pub);
406
407 Ok(DlcKey::Public {
408 pubkey: PublicKey(pub_bytes),
409 })
410 }
411
412 pub(crate) fn from_priv_and_pub(
414 privkey: PrivateKey,
415 publickey: PublicKey,
416 ) -> Result<Self, DlcError> {
417 let kp = privkey
418 .with_secret(|priv_bytes| {
419 Ed25519KeyPair::from_seed_and_public_key(priv_bytes, &publickey.0)
420 })
421 .map_err(|e| DlcError::CryptoError(format!("invalid seed: {:?}", e)))?;
422 let mut pub_bytes = [0u8; 32];
423 pub_bytes.copy_from_slice(kp.public_key().as_ref());
424
425 privkey
426 .with_secret(|priv_bytes| {
427 Ed25519KeyPair::from_seed_and_public_key(priv_bytes, &pub_bytes)
428 })
429 .map_err(|e| DlcError::CryptoError(format!("keypair validation failed: {:?}", e)))?;
430
431 Ok(DlcKey::Private {
432 privkey,
433 pubkey: PublicKey(pub_bytes),
434 })
435 }
436
437 pub fn generate_random() -> Self {
441 let privkey: PrivateKey = PrivateKey::from_random();
442
443 let pair = privkey
444 .with_secret(|priv_bytes| Ed25519KeyPair::from_seed_unchecked(priv_bytes))
445 .expect("derive public key from seed");
446 let mut pub_bytes = [0u8; 32];
447 pub_bytes.copy_from_slice(pair.public_key().as_ref());
448
449 Self::from_priv_and_pub(privkey, PublicKey(pub_bytes))
450 .unwrap_or_else(|e| panic!("generate_complete failed: {:?}", e))
451 }
452
453 pub fn get_public_key(&self) -> &PublicKey {
454 match self {
455 DlcKey::Private { pubkey, .. } => pubkey,
456 DlcKey::Public { pubkey: public } => public,
457 }
458 }
459
460 pub fn sign(&self, data: &[u8]) -> Result<[u8; 64], DlcError> {
463 match self {
464 DlcKey::Private { privkey, pubkey } => privkey.with_secret(|seed| {
465 let pair = Ed25519KeyPair::from_seed_and_public_key(seed, pubkey.as_ref())
466 .map_err(|e| DlcError::CryptoError(e.to_string()))?;
467 let sig = pair.sign(data);
468 let mut sig_bytes = [0u8; 64];
469 sig_bytes.copy_from_slice(sig.as_ref());
470 Ok(sig_bytes)
471 }),
472 DlcKey::Public { .. } => Err(DlcError::PrivateKeyRequired),
473 }
474 }
475
476 pub fn verify(&self, data: &[u8], signature: &[u8; 64]) -> Result<(), DlcError> {
478 let pubkey = self.get_public_key();
479 ring::signature::UnparsedPublicKey::new(&ring::signature::ED25519, pubkey.as_ref())
480 .verify(data, signature.as_ref())
481 .map_err(|_| DlcError::SignatureInvalid)
482 }
483
484 pub fn create_signed_license<D>(
490 &self,
491 dlcs: impl IntoIterator<Item = D>,
492 product: Product,
493 ) -> Result<SignedLicense, DlcError>
494 where
495 D: std::fmt::Display,
496 {
497 let mut payload = serde_json::Map::new();
498 payload.insert(
499 "dlcs".to_string(),
500 serde_json::Value::Array(
501 dlcs.into_iter()
502 .map(|s| serde_json::Value::String(s.to_string()))
503 .collect(),
504 ),
505 );
506
507 payload.insert(
508 "product".to_string(),
509 serde_json::Value::String(product.as_ref().to_string()),
510 );
511
512 match self {
513 DlcKey::Private { privkey, .. } => {
514 let sig_token = privkey.with_secret(
515 |encrypt_key_bytes| -> Result<SignedLicense, DlcError> {
516 payload.insert(
517 "encrypt_key".to_string(),
518 serde_json::Value::String(URL_SAFE_NO_PAD.encode(encrypt_key_bytes)),
519 );
520
521 let payload_value = serde_json::Value::Object(payload);
522 let payload_bytes = serde_json::to_vec(&payload_value)
523 .map_err(|e| DlcError::TokenCreationFailed(e.to_string()))?;
524
525 let sig = self.sign(&payload_bytes)?;
526 Ok(SignedLicense::from(format!(
527 "{}.{}",
528 URL_SAFE_NO_PAD.encode(&payload_bytes),
529 URL_SAFE_NO_PAD.encode(sig.as_ref())
530 )))
531 },
532 )?;
533 Ok(sig_token)
534 }
535 DlcKey::Public { .. } => Err(DlcError::PrivateKeyRequired),
536 }
537 }
538
539 pub fn extend_signed_license<D>(
565 &self,
566 existing: &SignedLicense,
567 new_dlcs: impl IntoIterator<Item = D>,
568 product: Product,
569 ) -> Result<SignedLicense, DlcError>
570 where
571 D: std::fmt::Display,
572 {
573 let mut combined_dlcs: Vec<String> = existing.with_secret(|token_str| {
574 let parts: Vec<&str> = token_str.split('.').collect();
575 if parts.len() != 2 {
576 return Vec::new();
577 }
578 if let Ok(payload) = URL_SAFE_NO_PAD.decode(parts[0].as_bytes()) {
579 if let Ok(payload_json) = serde_json::from_slice::<serde_json::Value>(&payload) {
580 if let Some(dlcs_array) = payload_json.get("dlcs").and_then(|v| v.as_array()) {
581 let mut dlcs = Vec::new();
582 for dlc in dlcs_array {
583 if let Some(dlc_id) = dlc.as_str() {
584 dlcs.push(dlc_id.to_string());
585 }
586 }
587 return dlcs;
588 }
589 }
590 }
591 Vec::new()
592 });
593
594 for new_dlc in new_dlcs {
595 let dlc_str = new_dlc.to_string();
596 if !combined_dlcs.contains(&dlc_str) {
597 combined_dlcs.push(dlc_str);
598 }
599 }
600
601 self.create_signed_license(combined_dlcs, product)
602 }
603
604 pub fn verify_signed_license(&self, license: &SignedLicense) -> bool {
610 license.with_secret(|full_token| {
611 let parts: Vec<&str> = full_token.split('.').collect();
612 if parts.len() != 2 {
613 return false;
614 }
615
616 let payload = URL_SAFE_NO_PAD.decode(parts[0]);
617 let sig_bytes = URL_SAFE_NO_PAD.decode(parts[1]);
618 if payload.is_err() || sig_bytes.is_err() {
619 return false;
620 }
621 let payload = payload.unwrap();
622 let sig_bytes = sig_bytes.unwrap();
623
624 if sig_bytes.len() != 64 {
625 return false;
626 }
627
628 let public = self.get_public_key().0;
629 let public_key = UnparsedPublicKey::new(&ED25519, public);
630 public_key.verify(&payload, &sig_bytes).is_ok()
631 })
632 }
633}
634
635impl Clone for DlcKey {
636 fn clone(&self) -> Self {
637 match self {
638 DlcKey::Private { privkey, pubkey } => privkey.with_secret(|s| DlcKey::Private {
639 privkey: PrivateKey::new(*s),
640 pubkey: *pubkey,
641 }),
642 DlcKey::Public { pubkey } => DlcKey::Public { pubkey: *pubkey },
643 }
644 }
645}
646
647impl std::fmt::Display for DlcKey {
648 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
649 write!(f, "{}", URL_SAFE_NO_PAD.encode(self.get_public_key().0))
650 }
651}
652
653impl From<&DlcKey> for String {
654 fn from(k: &DlcKey) -> Self {
655 k.to_string()
656 }
657}
658
659#[derive(Clone, Debug)]
662pub struct PackItem {
663 path: String,
664 original_extension: Option<String>,
665 type_path: Option<String>,
666 plaintext: Vec<u8>,
667}
668
669#[allow(dead_code)]
670impl PackItem {
671 pub fn new(path: impl Into<String>, plaintext: impl Into<Vec<u8>>) -> Result<Self, DlcError> {
672 let path = path.into();
673 let bytes = plaintext.into();
674
675 if bytes.len() >= 4 && bytes.starts_with(DLC_PACK_MAGIC) {
676 return Err(DlcError::Other(format!(
677 "cannot pack existing dlcpack container as an item: {}",
678 path
679 )));
680 }
681
682 if pack_format::is_data_executable(&bytes) {
683 return Err(DlcError::Other(format!(
684 "input data looks like an executable payload, which is not allowed: {}",
685 path
686 )));
687 }
688
689 let ext_str = std::path::Path::new(&path)
690 .extension()
691 .and_then(|e| e.to_str());
692
693 Ok(Self {
694 path: path.clone(),
695 original_extension: ext_str.map(|s| s.to_string()),
696 type_path: None,
697 plaintext: bytes,
698 })
699 }
700
701 pub fn with_extension(mut self, ext: impl Into<String>) -> Result<Self, DlcError> {
702 let ext_s = ext.into();
703 self.original_extension = Some(ext_s);
704 Ok(self)
705 }
706
707 pub fn with_type_path(mut self, type_path: impl Into<String>) -> Self {
708 self.type_path = Some(type_path.into());
709 self
710 }
711
712 pub fn with_type<T: Asset>(self) -> Self {
713 self.with_type_path(T::type_path())
714 }
715
716 pub fn path(&self) -> &str {
718 &self.path
719 }
720
721 pub fn plaintext(&self) -> &[u8] {
723 &self.plaintext
724 }
725
726 pub fn ext(&self) -> Option<String> {
727 if let Some(ext) = std::path::Path::new(&self.path)
728 .extension()
729 .and_then(|e| e.to_str())
730 .and_then(|s| {
731 if s.is_empty() {
732 None
733 } else {
734 Some(s.to_string())
735 }
736 })
737 {
738 Some(ext)
739 } else {
740 self.original_extension.clone()
741 }
742 }
743
744 pub fn type_path(&self) -> Option<String> {
745 self.type_path.clone()
746 }
747}
748
749impl From<PackItem> for (String, Option<String>, Option<String>, Vec<u8>) {
750 fn from(item: PackItem) -> Self {
751 (
752 item.path,
753 item.original_extension,
754 item.type_path,
755 item.plaintext,
756 )
757 }
758}
759
760#[derive(Error, Debug, Clone)]
767pub enum DlcError {
768 #[error("invalid public key: {0}")]
769 InvalidPublicKey(String),
770 #[error("malformed private key: {0}")]
771 MalformedLicense(String),
772 #[error("signature verification failed")]
773 SignatureInvalid,
774 #[error("payload parse failed: {0}")]
775 PayloadInvalid(String),
776
777 #[error("private key creation failed: {0}")]
778 TokenCreationFailed(String),
779 #[error("private key required for this operation")]
780 PrivateKeyRequired,
781 #[error("invalid public key")]
782 InvalidPassphrase,
783 #[error("crypto error: {0}")]
784 CryptoError(String),
785
786 #[error("encryption failed: {0}")]
787 EncryptionFailed(String),
788 #[error("{0}")]
789 DecryptionFailed(String),
790 #[error("invalid encrypt key: {0}")]
791 InvalidEncryptKey(String),
792 #[error("invalid nonce: {0}")]
793 InvalidNonce(String),
794
795 #[error("dlc locked: {0}")]
796 DlcLocked(String),
797 #[error("no encrypt key for dlc: {0}")]
798 NoEncryptKey(String),
799
800 #[error("private key product does not match")]
802 TokenProductMismatch,
803
804 #[error("deprecated version: v{0}")]
806 DeprecatedVersion(String),
807
808 #[error("{0}")]
809 Other(String),
810}
811
812impl From<std::io::Error> for DlcError {
814 fn from(e: std::io::Error) -> Self {
815 DlcError::Other(e.to_string())
816 }
817}
818
819impl From<ring::error::Unspecified> for DlcError {
820 fn from(e: ring::error::Unspecified) -> Self {
821 DlcError::CryptoError(format!("crypto error: {:?}", e))
822 }
823}
824
825#[cfg(test)]
826mod tests {
827 use crate::ext::*;
828
829 use super::*;
830
831 #[test]
832 fn pack_encrypted_pack_rejects_nested_dlc() {
833 let mut v = Vec::new();
834 v.extend_from_slice(DLC_PACK_MAGIC);
835 v.extend_from_slice(b"inner");
836 let err = PackItem::new("a.txt", v).unwrap_err();
837 assert!(err.to_string().contains("cannot pack existing dlcpack"));
838 }
839
840 #[test]
841 fn pack_encrypted_pack_rejects_nested_dlcpack() {
842 let mut v = Vec::new();
843 v.extend_from_slice(DLC_PACK_MAGIC);
844 v.extend_from_slice(b"innerpack");
845 let err = PackItem::new("b.dlcpack", v);
846 assert!(err.is_err());
847 }
848
849 #[test]
850 fn is_data_executable_detects_pe_header() {
851 assert!(is_data_executable(&[0x4D, 0x5A, 0, 0]));
852 }
853
854 #[test]
855 fn is_data_executable_detects_shebang() {
856 assert!(is_data_executable(b"#! /bin/sh"));
857 }
858
859 #[test]
860 fn is_data_executable_ignores_plain_text() {
861 assert!(!is_data_executable(b"hello"));
862 }
863
864 #[test]
865 fn packitem_rejects_binary_data() {
866 let mut v = Vec::new();
867 v.extend_from_slice(&[0x4D, 0x5A, 0, 0]);
868
869 let pack_item = PackItem::new("evil.dat", v);
870 assert!(pack_item.is_err());
871 }
872
873 #[test]
874 fn dlc_id_serde_roundtrip() {
875 let id = DlcId::from("expansion_serde");
876 let s = serde_json::to_string(&id).expect("serialize dlc id");
877 assert_eq!(s, "\"expansion_serde\"");
878 let decoded: DlcId = serde_json::from_str(&s).expect("deserialize dlc id");
879 assert_eq!(decoded.to_string(), "expansion_serde");
880 }
881
882 #[test]
883 fn extend_signed_license_merges_dlc_ids() {
884 let product = Product::from("test_product");
885 let dlc_key = DlcKey::generate_random();
886
887 let initial = dlc_key
888 .create_signed_license(&["expansion_a", "expansion_b"], product.clone())
889 .expect("create initial license");
890
891 let extended = dlc_key
892 .extend_signed_license(&initial, &["expansion_c"], product.clone())
893 .expect("extend license");
894
895 assert!(dlc_key.verify_signed_license(&extended));
896 let verified_dlcs = extract_dlc_ids_from_license(&extended);
897 assert_eq!(verified_dlcs.len(), 3);
898 assert!(verified_dlcs.contains(&"expansion_a".to_string()));
899 assert!(verified_dlcs.contains(&"expansion_b".to_string()));
900 assert!(verified_dlcs.contains(&"expansion_c".to_string()));
901 }
902
903 #[test]
904 fn extend_signed_license_deduplicates() {
905 let product = Product::from("test_product");
906 let dlc_key = DlcKey::generate_random();
907
908 let initial = dlc_key
909 .create_signed_license(&["expansion_a"], product.clone())
910 .expect("create initial license");
911
912 let extended = dlc_key
913 .extend_signed_license(&initial, &["expansion_a"], product.clone())
914 .expect("extend license");
915
916 assert!(dlc_key.verify_signed_license(&extended));
917 let verified_dlcs = extract_dlc_ids_from_license(&extended);
918 let count = verified_dlcs.iter().filter(|d| d == &"expansion_a").count();
919 assert_eq!(count, 1, "Should not duplicate dlc_ids");
920 }
921
922 #[test]
923 #[serial_test::serial]
924 fn register_dlc_type_adds_pack_registrar_factory() {
925 let mut app = App::new();
926 app.add_plugins(AssetPlugin::default());
927 #[derive(Asset, TypePath)]
928 struct TestAsset;
929 app.init_asset::<TestAsset>();
930 app.register_dlc_type::<TestAsset>();
931
932 let factories = app
933 .world()
934 .get_resource::<asset_loader::DlcPackRegistrarFactories>()
935 .expect("should have factories resource");
936 assert!(
937 factories
938 .0
939 .read()
940 .unwrap()
941 .iter()
942 .any(|f| asset_loader::fuzzy_type_path_match(
943 f.type_name(),
944 TestAsset::type_path()
945 ))
946 );
947 }
948
949 #[test]
950 #[serial_test::serial]
951 fn register_dlc_type_is_idempotent_for_pack_factories() {
952 let mut app = App::new();
953 app.add_plugins(AssetPlugin::default());
954 #[derive(Asset, TypePath)]
955 struct TestAsset2;
956 app.init_asset::<TestAsset2>();
957 app.register_dlc_type::<TestAsset2>();
958 app.register_dlc_type::<TestAsset2>();
959
960 let factories = app
961 .world()
962 .get_resource::<asset_loader::DlcPackRegistrarFactories>()
963 .expect("should have factories resource");
964 let count = factories
965 .0
966 .read()
967 .unwrap()
968 .iter()
969 .filter(|f| asset_loader::fuzzy_type_path_match(f.type_name(), TestAsset2::type_path()))
970 .count();
971 assert_eq!(count, 1);
972 }
973}
974
975#[cfg(test)]
982#[allow(dead_code)]
983pub mod test_helpers {
984 use crate::{EncryptionKey, encrypt_key_registry};
985
986 pub fn register_test_encryption_key(dlc_id: &str, key: EncryptionKey) {
991 encrypt_key_registry::insert(dlc_id, key);
992 }
993
994 pub fn register_test_asset_path(dlc_id: &str, path: &str) {
998 encrypt_key_registry::register_asset_path(dlc_id, path);
999 }
1000
1001 pub fn clear_test_registry() {
1005 encrypt_key_registry::clear_all();
1006 }
1007
1008 pub fn is_malicious_data(data: &[u8]) -> bool {
1009 crate::pack_format::is_data_executable(data)
1010 }
1011}