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