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