1use base64::Engine as _;
2use base64::engine::general_purpose::URL_SAFE_NO_PAD;
3use bevy::prelude::*;
4use ring::signature::{ED25519, Ed25519KeyPair, KeyPair, UnparsedPublicKey};
5
6#[allow(unused_imports)]
7use secure_gate::{CloneableSecret, RevealSecret, dynamic_alias, fixed_alias};
8
9mod asset_loader;
10pub mod encrypt_key_registry;
11mod ext;
12
13#[macro_use]
14mod macros;
15
16mod pack_format;
17
18pub use asset_loader::{
19 DlcLoader, DlcPack, DlcPackLoader, DlcPackLoaderSettings, EncryptedAsset, parse_encrypted,
20};
21
22pub use pack_format::{
23 BlockMetadata, CompressionLevel, DEFAULT_BLOCK_SIZE, DLC_PACK_MAGIC, DLC_PACK_VERSION_LATEST,
24 ManifestEntry, PackMetadata, ParsedDlcPack, V4ManifestEntry, V5ManifestEntry,
25 is_data_executable, pack_encrypted_pack, pack_encrypted_pack_with_metadata,
26 parse_encrypted_pack, parse_encrypted_pack_info,
27};
28
29use serde::{Deserialize, Serialize};
30
31use thiserror::Error;
32
33use crate::asset_loader::DlcPackLoaded;
34
35#[doc(hidden)]
36pub use crate::macros::__decode_embedded_signed_license_aes;
37#[allow(unused_imports)]
38pub use bevy_dlc_macro::include_signed_license_aes;
39
40pub use crate::ext::AppExt;
41
42pub fn register_encryption_key(dlc_id: &str, key: EncryptionKey) {
49 encrypt_key_registry::insert(dlc_id, key);
50}
51
52pub mod prelude {
53 pub use crate::ext::*;
54 pub use crate::include_dlc_key_and_license_aes;
55 pub use crate::{
56 DlcError, DlcId, DlcKey, DlcLoader, DlcPack, DlcPackLoader, DlcPackLoaderSettings,
57 DlcPackMetadataError, DlcPlugin, EncryptedAsset, PackItem, PackMetadata, Product,
58 SignedLicense, asset_loader::DlcPackEntry, asset_loader::DlcPackLoaded,
59 is_dlc_entry_loaded, is_dlc_loaded,
60 };
61 pub use bevy_dlc_macro::include_signed_license_aes;
62}
63
64pub struct DlcPlugin {
65 dlc_key: DlcKey,
66
67 signed_license: SignedLicense,
68}
69
70impl DlcPlugin {
71 pub fn new(dlc_key: DlcKey, signed_license: SignedLicense) -> Self {
76 Self {
77 dlc_key,
78 signed_license,
79 }
80 }
81}
82
83impl From<(DlcKey, SignedLicense)> for DlcPlugin {
84 fn from(tuple: (DlcKey, SignedLicense)) -> Self {
86 DlcPlugin::new(tuple.0, tuple.1)
87 }
88}
89
90impl Plugin for DlcPlugin {
91 fn build(&self, app: &mut App) {
92 if let Some(encrypt_key) = extract_encrypt_key_from_license(&self.signed_license) {
93 let dlcs = extract_dlc_ids_from_license(&self.signed_license);
94 for dlc_id in dlcs {
95 let key_for_dlc = encrypt_key.with_secret(|kb| EncryptionKey::new(*kb));
99 encrypt_key_registry::insert(&dlc_id, key_for_dlc);
100 }
101 }
102
103 app.init_resource::<asset_loader::DlcPackRegistrarFactories>();
104
105 app.insert_resource(self.dlc_key.clone())
106 .init_asset_loader::<asset_loader::DlcLoader<Image>>()
107 .init_asset_loader::<asset_loader::DlcLoader<Scene>>()
108 .init_asset_loader::<asset_loader::DlcLoader<Mesh>>()
109 .init_asset_loader::<asset_loader::DlcLoader<Font>>()
110 .init_asset_loader::<asset_loader::DlcLoader<AudioSource>>()
111 .init_asset_loader::<asset_loader::DlcLoader<ColorMaterial>>()
112 .init_asset_loader::<asset_loader::DlcLoader<StandardMaterial>>()
113 .init_asset_loader::<asset_loader::DlcLoader<Gltf>>()
114 .init_asset_loader::<asset_loader::DlcLoader<bevy::gltf::GltfMesh>>()
115 .init_asset_loader::<asset_loader::DlcLoader<Shader>>()
116 .init_asset_loader::<asset_loader::DlcLoader<DynamicScene>>()
117 .init_asset_loader::<asset_loader::DlcLoader<AnimationClip>>()
118 .init_asset_loader::<asset_loader::DlcLoader<AnimationGraph>>();
119
120 let factories = app
121 .world()
122 .get_resource::<asset_loader::DlcPackRegistrarFactories>()
123 .cloned();
124 let pack_loader = asset_loader::DlcPackLoader {
125 registrars: asset_loader::collect_pack_registrars(factories.as_ref()),
126 factories,
127 };
128
129 app.register_asset_loader(pack_loader);
130 app.init_asset::<asset_loader::DlcPack>();
131
132 app.add_systems(Update, trigger_dlc_events);
133 }
134}
135
136fn trigger_dlc_events(
138 mut events: MessageReader<AssetEvent<DlcPack>>,
139 packs: Res<Assets<DlcPack>>,
140 mut commands: Commands,
141) {
142 for event in events.read() {
143 match event {
144 AssetEvent::Added { id } => {
145 if let Some(pack) = packs.get(*id) {
146 let dlc_id = pack.id().clone();
147 commands.trigger(DlcPackLoaded::new(dlc_id.clone(), pack.clone()));
148 }
149 }
150 _ => {}
151 }
152 }
153}
154
155pub fn is_dlc_loaded(dlc_id: impl Into<DlcId>) -> impl Fn() -> bool + Send + Sync + 'static {
174 let id_string = dlc_id.into().0;
175 move || !encrypt_key_registry::asset_path_for(&id_string).is_none()
176}
177
178pub fn is_dlc_entry_loaded(
180 dlc_id: impl Into<DlcId>,
181 entry: impl Into<String>,
182) -> impl Fn(Res<Assets<DlcPack>>) -> bool + Send + Sync + 'static {
183 let id_string = dlc_id.into().0;
184 let entry_name = entry.into();
185 move |dlc_packs: Res<Assets<DlcPack>>| {
186 if !encrypt_key_registry::asset_path_for(&id_string).is_none() {
187 dlc_packs
188 .iter()
189 .filter(|p| p.1.id() == &DlcId::from(id_string.clone()))
190 .any(|pack| pack.1.find_entry(&entry_name).is_some())
191 } else {
192 false
193 }
194 }
195}
196
197#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
199#[serde(transparent)]
200pub struct DlcId(pub String);
201
202impl std::fmt::Display for DlcId {
203 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204 self.0.fmt(f)
205 }
206}
207
208impl Clone for DlcId {
209 fn clone(&self) -> Self {
210 DlcId(self.0.clone())
211 }
212}
213
214impl DlcId {
215 pub fn as_str(&self) -> &str {
217 self.0.as_str()
218 }
219}
220
221impl From<&str> for DlcId {
222 fn from(s: &str) -> Self {
223 DlcId(s.to_owned())
224 }
225}
226
227impl From<String> for DlcId {
228 fn from(s: String) -> Self {
229 DlcId(s)
230 }
231}
232
233impl AsRef<str> for DlcId {
234 fn as_ref(&self) -> &str {
235 &self.0
236 }
237}
238
239fixed_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.");
240
241#[derive(Clone, Copy, PartialEq, Eq)]
243pub struct PublicKey(pub [u8; 32]);
244
245impl AsRef<[u8]> for PublicKey {
246 fn as_ref(&self) -> &[u8] {
247 &self.0
248 }
249}
250
251impl std::fmt::Debug for PublicKey {
252 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253 write!(f, "PublicKey({} bytes)", 32)
254 }
255}
256
257dynamic_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 ");
258
259pub fn extract_encrypt_key_from_license(license: &SignedLicense) -> Option<EncryptionKey> {
262 license.with_secret(|token_str| {
263 let parts: Vec<&str> = token_str.split('.').collect();
264 if parts.len() != 2 {
265 return None;
266 }
267 let payload = URL_SAFE_NO_PAD.decode(parts[0].as_bytes()).ok()?;
268 let payload_json: serde_json::Value = serde_json::from_slice(&payload).ok()?;
269 let key_b64 = payload_json.get("encrypt_key").and_then(|v| v.as_str())?;
270 URL_SAFE_NO_PAD
271 .decode(key_b64.as_bytes())
272 .ok()
273 .and_then(|key_bytes| Some(EncryptionKey::new(key_bytes.try_into().ok()?)))
274 })
275}
276
277pub fn extract_dlc_ids_from_license(license: &SignedLicense) -> Vec<String> {
280 license.with_secret(|token_str| {
281 let parts: Vec<&str> = token_str.split('.').collect();
282 if parts.len() != 2 {
283 return Vec::new();
284 }
285 if let Ok(payload) = URL_SAFE_NO_PAD.decode(parts[0].as_bytes()) {
286 if let Ok(payload_json) = serde_json::from_slice::<serde_json::Value>(&payload) {
287 if let Some(dlcs_array) = payload_json.get("dlcs").and_then(|v| v.as_array()) {
288 let mut dlcs = Vec::new();
289 for dlc in dlcs_array {
290 if let Some(dlc_id) = dlc.as_str() {
291 dlcs.push(dlc_id.to_string());
292 }
293 }
294 return dlcs;
295 }
296 }
297 }
298 Vec::new()
299 })
300}
301
302pub fn extract_product_from_license(license: &SignedLicense) -> Option<String> {
305 license.with_secret(|token_str| {
306 let parts: Vec<&str> = token_str.split('.').collect();
307 if parts.len() != 2 {
308 return None;
309 }
310 if let Ok(payload) = URL_SAFE_NO_PAD.decode(parts[0].as_bytes()) {
311 if let Ok(payload_json) = serde_json::from_slice::<serde_json::Value>(&payload) {
312 if let Some(prod) = payload_json.get("product").and_then(|v| v.as_str()) {
313 return Some(prod.to_string());
314 }
315 }
316 }
317 None
318 })
319}
320#[derive(Resource, Clone, PartialEq, Eq, Debug)]
322pub struct Product(String);
323
324impl std::fmt::Display for Product {
325 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
326 self.0.fmt(f)
327 }
328}
329
330impl AsRef<str> for Product {
331 fn as_ref(&self) -> &str {
332 &self.0
333 }
334}
335
336impl Product {
337 pub fn as_str(&self) -> &str {
339 self.0.as_str()
340 }
341}
342
343impl From<String> for Product {
344 fn from(s: String) -> Self {
345 Product(s)
346 }
347}
348impl From<&str> for Product {
349 fn from(s: &str) -> Self {
350 Product(s.to_owned())
351 }
352}
353
354fixed_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.");
355
356#[derive(Resource)]
359pub enum DlcKey {
360 Private {
362 privkey: PrivateKey,
364 pubkey: PublicKey,
366 },
367
368 Public {
370 pubkey: PublicKey,
372 },
373}
374
375impl DlcKey {
376 pub fn new(pubkey: &str, privkey: &str) -> Result<Self, DlcError> {
377 let decoded_pub = URL_SAFE_NO_PAD
378 .decode(pubkey.as_bytes())
379 .map_err(|e| DlcError::CryptoError(format!("invalid pubkey base64: {}", e)))?;
380 if decoded_pub.len() != 32 {
381 return Err(DlcError::CryptoError("public key must be 32 bytes".into()));
382 }
383 let mut pub_bytes = [0u8; 32];
384 pub_bytes.copy_from_slice(&decoded_pub);
385
386 let decoded_priv = URL_SAFE_NO_PAD
387 .decode(privkey.as_bytes())
388 .map_err(|e| DlcError::CryptoError(format!("invalid privkey base64: {}", e)))?;
389 if decoded_priv.len() != 32 {
390 return Err(DlcError::CryptoError("private key must be 32 bytes".into()));
391 }
392 let mut priv_bytes = [0u8; 32];
393 priv_bytes.copy_from_slice(&decoded_priv);
394
395 Self::from_priv_and_pub(PrivateKey::from(priv_bytes), PublicKey(pub_bytes))
396 }
397
398 pub fn public(pubkey: &str) -> Result<Self, DlcError> {
400 let decoded_pub = URL_SAFE_NO_PAD
401 .decode(pubkey.as_bytes())
402 .map_err(|e| DlcError::CryptoError(format!("invalid pubkey base64: {}", e)))?;
403 if decoded_pub.len() != 32 {
404 return Err(DlcError::CryptoError("public key must be 32 bytes".into()));
405 }
406 let mut pub_bytes = [0u8; 32];
407 pub_bytes.copy_from_slice(&decoded_pub);
408
409 Ok(DlcKey::Public {
410 pubkey: PublicKey(pub_bytes),
411 })
412 }
413
414 pub(crate) fn from_priv_and_pub(
416 privkey: PrivateKey,
417 publickey: PublicKey,
418 ) -> Result<Self, DlcError> {
419 let kp = privkey
420 .with_secret(|priv_bytes| {
421 Ed25519KeyPair::from_seed_and_public_key(priv_bytes, &publickey.0)
422 })
423 .map_err(|e| DlcError::CryptoError(format!("invalid seed: {:?}", e)))?;
424 let mut pub_bytes = [0u8; 32];
425 pub_bytes.copy_from_slice(kp.public_key().as_ref());
426
427 privkey
428 .with_secret(|priv_bytes| {
429 Ed25519KeyPair::from_seed_and_public_key(priv_bytes, &pub_bytes)
430 })
431 .map_err(|e| DlcError::CryptoError(format!("keypair validation failed: {:?}", e)))?;
432
433 Ok(DlcKey::Private {
434 privkey,
435 pubkey: PublicKey(pub_bytes),
436 })
437 }
438
439 pub fn generate_random() -> Self {
443 let privkey: PrivateKey = PrivateKey::new(rand::random());
444
445 let pair = privkey
446 .with_secret(|priv_bytes| Ed25519KeyPair::from_seed_unchecked(priv_bytes))
447 .expect("derive public key from seed");
448 let mut pub_bytes = [0u8; 32];
449 pub_bytes.copy_from_slice(pair.public_key().as_ref());
450
451 Self::from_priv_and_pub(privkey, PublicKey(pub_bytes))
452 .unwrap_or_else(|e| panic!("generate_complete failed: {:?}", e))
453 }
454
455 pub fn get_public_key(&self) -> &PublicKey {
456 match self {
457 DlcKey::Private { pubkey, .. } => pubkey,
458 DlcKey::Public { pubkey: public } => public,
459 }
460 }
461
462 pub fn sign(&self, data: &[u8]) -> Result<[u8; 64], DlcError> {
465 match self {
466 DlcKey::Private { privkey, pubkey } => privkey.with_secret(|seed| {
467 let pair = Ed25519KeyPair::from_seed_and_public_key(seed, pubkey.as_ref())
468 .map_err(|e| DlcError::CryptoError(e.to_string()))?;
469 let sig = pair.sign(data);
470 let mut sig_bytes = [0u8; 64];
471 sig_bytes.copy_from_slice(sig.as_ref());
472 Ok(sig_bytes)
473 }),
474 DlcKey::Public { .. } => Err(DlcError::PrivateKeyRequired),
475 }
476 }
477
478 pub fn verify(&self, data: &[u8], signature: &[u8; 64]) -> Result<(), DlcError> {
480 let pubkey = self.get_public_key();
481 ring::signature::UnparsedPublicKey::new(&ring::signature::ED25519, pubkey.as_ref())
482 .verify(data, signature.as_ref())
483 .map_err(|_| DlcError::SignatureInvalid)
484 }
485
486 pub fn create_signed_license<D>(
492 &self,
493 dlcs: impl IntoIterator<Item = D>,
494 product: Product,
495 ) -> Result<SignedLicense, DlcError>
496 where
497 D: std::fmt::Display,
498 {
499 let mut payload = serde_json::Map::new();
500 payload.insert(
501 "dlcs".to_string(),
502 serde_json::Value::Array(
503 dlcs.into_iter()
504 .map(|s| serde_json::Value::String(s.to_string()))
505 .collect(),
506 ),
507 );
508
509 payload.insert(
510 "product".to_string(),
511 serde_json::Value::String(product.as_ref().to_string()),
512 );
513
514 match self {
515 DlcKey::Private { privkey, .. } => {
516 let sig_token = privkey.with_secret(
517 |encrypt_key_bytes| -> Result<SignedLicense, DlcError> {
518 payload.insert(
519 "encrypt_key".to_string(),
520 serde_json::Value::String(URL_SAFE_NO_PAD.encode(encrypt_key_bytes)),
521 );
522
523 let payload_value = serde_json::Value::Object(payload);
524 let payload_bytes = serde_json::to_vec(&payload_value)
525 .map_err(|e| DlcError::TokenCreationFailed(e.to_string()))?;
526
527 let sig = self.sign(&payload_bytes)?;
528 Ok(SignedLicense::from(format!(
529 "{}.{}",
530 URL_SAFE_NO_PAD.encode(&payload_bytes),
531 URL_SAFE_NO_PAD.encode(sig.as_ref())
532 )))
533 },
534 )?;
535 Ok(sig_token)
536 }
537 DlcKey::Public { .. } => Err(DlcError::PrivateKeyRequired),
538 }
539 }
540
541 pub fn extend_signed_license<D>(
567 &self,
568 existing: &SignedLicense,
569 new_dlcs: impl IntoIterator<Item = D>,
570 product: Product,
571 ) -> Result<SignedLicense, DlcError>
572 where
573 D: std::fmt::Display,
574 {
575 let mut combined_dlcs: Vec<String> = existing.with_secret(|token_str| {
576 let parts: Vec<&str> = token_str.split('.').collect();
577 if parts.len() != 2 {
578 return Vec::new();
579 }
580 if let Ok(payload) = URL_SAFE_NO_PAD.decode(parts[0].as_bytes()) {
581 if let Ok(payload_json) = serde_json::from_slice::<serde_json::Value>(&payload) {
582 if let Some(dlcs_array) = payload_json.get("dlcs").and_then(|v| v.as_array()) {
583 let mut dlcs = Vec::new();
584 for dlc in dlcs_array {
585 if let Some(dlc_id) = dlc.as_str() {
586 dlcs.push(dlc_id.to_string());
587 }
588 }
589 return dlcs;
590 }
591 }
592 }
593 Vec::new()
594 });
595
596 for new_dlc in new_dlcs {
597 let dlc_str = new_dlc.to_string();
598 if !combined_dlcs.contains(&dlc_str) {
599 combined_dlcs.push(dlc_str);
600 }
601 }
602
603 self.create_signed_license(combined_dlcs, product)
604 }
605
606 pub fn verify_signed_license(&self, license: &SignedLicense) -> bool {
612 license.with_secret(|full_token| {
613 let parts: Vec<&str> = full_token.split('.').collect();
614 if parts.len() != 2 {
615 return false;
616 }
617
618 let payload = URL_SAFE_NO_PAD.decode(parts[0]);
619 let sig_bytes = URL_SAFE_NO_PAD.decode(parts[1]);
620 if payload.is_err() || sig_bytes.is_err() {
621 return false;
622 }
623 let payload = payload.unwrap();
624 let sig_bytes = sig_bytes.unwrap();
625
626 if sig_bytes.len() != 64 {
627 return false;
628 }
629
630 let public = self.get_public_key().0;
631 let public_key = UnparsedPublicKey::new(&ED25519, public);
632 public_key.verify(&payload, &sig_bytes).is_ok()
633 })
634 }
635}
636
637impl Clone for DlcKey {
638 fn clone(&self) -> Self {
639 match self {
640 DlcKey::Private { privkey, pubkey } => privkey.with_secret(|s| DlcKey::Private {
641 privkey: PrivateKey::new(*s),
642 pubkey: *pubkey,
643 }),
644 DlcKey::Public { pubkey } => DlcKey::Public { pubkey: *pubkey },
645 }
646 }
647}
648
649impl std::fmt::Display for DlcKey {
650 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
651 write!(f, "{}", URL_SAFE_NO_PAD.encode(self.get_public_key().0))
652 }
653}
654
655impl From<&DlcKey> for String {
656 fn from(k: &DlcKey) -> Self {
657 k.to_string()
658 }
659}
660
661#[derive(Clone, Debug)]
664pub struct PackItem {
665 path: String,
666 original_extension: Option<String>,
667 type_path: Option<String>,
668 plaintext: Vec<u8>,
669}
670
671#[allow(dead_code)]
672impl PackItem {
673 pub fn new(path: impl Into<String>, plaintext: impl Into<Vec<u8>>) -> Result<Self, DlcError> {
674 let path = path.into();
675 let bytes = plaintext.into();
676
677 if bytes.len() >= 4 && bytes.starts_with(DLC_PACK_MAGIC) {
678 return Err(DlcError::Other(format!(
679 "cannot pack existing dlcpack container as an item: {}",
680 path
681 )));
682 }
683
684 if pack_format::is_data_executable(&bytes) {
685 return Err(DlcError::Other(format!(
686 "input data looks like an executable payload, which is not allowed: {}",
687 path
688 )));
689 }
690
691 let ext_str = std::path::Path::new(&path)
692 .extension()
693 .and_then(|e| e.to_str());
694
695 Ok(Self {
696 path: path.clone(),
697 original_extension: ext_str.map(|s| s.to_string()),
698 type_path: None,
699 plaintext: bytes,
700 })
701 }
702
703 pub fn with_extension(mut self, ext: impl Into<String>) -> Result<Self, DlcError> {
704 let ext_s = ext.into();
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
814#[derive(Error, Debug)]
815pub enum DlcPackMetadataError {
816 #[error("pack metadata is encrypted and unavailable without the DLC encryption key")]
817 Locked,
818
819 #[error("failed to deserialize metadata key '{key}': {source}")]
820 Deserialize {
821 key: String,
822 #[source]
823 source: serde_json::Error,
824 },
825}
826
827impl From<std::io::Error> for DlcError {
829 fn from(e: std::io::Error) -> Self {
830 DlcError::Other(e.to_string())
831 }
832}
833
834impl From<ring::error::Unspecified> for DlcError {
835 fn from(e: ring::error::Unspecified) -> Self {
836 DlcError::CryptoError(format!("crypto error: {:?}", e))
837 }
838}
839
840#[cfg(test)]
841mod tests {
842 use crate::ext::*;
843
844 use super::*;
845
846 #[test]
847 fn pack_encrypted_pack_rejects_nested_dlc() {
848 let mut v = Vec::new();
849 v.extend_from_slice(DLC_PACK_MAGIC);
850 v.extend_from_slice(b"inner");
851 let err = PackItem::new("a.txt", v).unwrap_err();
852 assert!(err.to_string().contains("cannot pack existing dlcpack"));
853 }
854
855 #[test]
856 fn pack_encrypted_pack_rejects_nested_dlcpack() {
857 let mut v = Vec::new();
858 v.extend_from_slice(DLC_PACK_MAGIC);
859 v.extend_from_slice(b"innerpack");
860 let err = PackItem::new("b.dlcpack", v);
861 assert!(err.is_err());
862 }
863
864 #[test]
865 fn is_data_executable_detects_pe_header() {
866 assert!(is_data_executable(&[0x4D, 0x5A, 0, 0]));
867 }
868
869 #[test]
870 fn is_data_executable_detects_shebang() {
871 assert!(is_data_executable(b"#! /bin/sh"));
872 }
873
874 #[test]
875 fn is_data_executable_ignores_plain_text() {
876 assert!(!is_data_executable(b"hello"));
877 }
878
879 #[test]
880 fn packitem_rejects_binary_data() {
881 let mut v = Vec::new();
882 v.extend_from_slice(&[0x4D, 0x5A, 0, 0]);
883
884 let pack_item = PackItem::new("evil.dat", v);
885 assert!(pack_item.is_err());
886 }
887
888 #[test]
889 fn dlc_id_serde_roundtrip() {
890 let id = DlcId::from("expansion_serde");
891 let s = serde_json::to_string(&id).expect("serialize dlc id");
892 assert_eq!(s, "\"expansion_serde\"");
893 let decoded: DlcId = serde_json::from_str(&s).expect("deserialize dlc id");
894 assert_eq!(decoded.to_string(), "expansion_serde");
895 }
896
897 #[test]
898 fn extend_signed_license_merges_dlc_ids() {
899 let product = Product::from("test_product");
900 let dlc_key = DlcKey::generate_random();
901
902 let initial = dlc_key
903 .create_signed_license(&["expansion_a", "expansion_b"], product.clone())
904 .expect("create initial license");
905
906 let extended = dlc_key
907 .extend_signed_license(&initial, &["expansion_c"], product.clone())
908 .expect("extend license");
909
910 assert!(dlc_key.verify_signed_license(&extended));
911 let verified_dlcs = extract_dlc_ids_from_license(&extended);
912 assert_eq!(verified_dlcs.len(), 3);
913 assert!(verified_dlcs.contains(&"expansion_a".to_string()));
914 assert!(verified_dlcs.contains(&"expansion_b".to_string()));
915 assert!(verified_dlcs.contains(&"expansion_c".to_string()));
916 }
917
918 #[test]
919 fn extend_signed_license_deduplicates() {
920 let product = Product::from("test_product");
921 let dlc_key = DlcKey::generate_random();
922
923 let initial = dlc_key
924 .create_signed_license(&["expansion_a"], product.clone())
925 .expect("create initial license");
926
927 let extended = dlc_key
928 .extend_signed_license(&initial, &["expansion_a"], product.clone())
929 .expect("extend license");
930
931 assert!(dlc_key.verify_signed_license(&extended));
932 let verified_dlcs = extract_dlc_ids_from_license(&extended);
933 let count = verified_dlcs.iter().filter(|d| d == &"expansion_a").count();
934 assert_eq!(count, 1, "Should not duplicate dlc_ids");
935 }
936
937 #[test]
938 #[serial_test::serial]
939 fn register_dlc_type_adds_pack_registrar_factory() {
940 let mut app = App::new();
941 app.add_plugins(AssetPlugin::default());
942 #[derive(Asset, TypePath)]
943 struct TestAsset;
944 app.init_asset::<TestAsset>();
945 app.register_dlc_type::<TestAsset>();
946
947 let factories = app
948 .world()
949 .get_resource::<asset_loader::DlcPackRegistrarFactories>()
950 .expect("should have factories resource");
951 assert!(
952 factories
953 .0
954 .read()
955 .unwrap()
956 .iter()
957 .any(|f| asset_loader::fuzzy_type_path_match(
958 f.type_name(),
959 TestAsset::type_path()
960 ))
961 );
962 }
963
964 #[test]
965 #[serial_test::serial]
966 fn register_dlc_type_is_idempotent_for_pack_factories() {
967 let mut app = App::new();
968 app.add_plugins(AssetPlugin::default());
969 #[derive(Asset, TypePath)]
970 struct TestAsset2;
971 app.init_asset::<TestAsset2>();
972 app.register_dlc_type::<TestAsset2>();
973 app.register_dlc_type::<TestAsset2>();
974
975 let factories = app
976 .world()
977 .get_resource::<asset_loader::DlcPackRegistrarFactories>()
978 .expect("should have factories resource");
979 let count = factories
980 .0
981 .read()
982 .unwrap()
983 .iter()
984 .filter(|f| asset_loader::fuzzy_type_path_match(f.type_name(), TestAsset2::type_path()))
985 .count();
986 assert_eq!(count, 1);
987 }
988}
989
990#[cfg(test)]
997#[allow(dead_code)]
998pub mod test_helpers {
999 use crate::{EncryptionKey, encrypt_key_registry};
1000
1001 pub fn register_test_encryption_key(dlc_id: &str, key: EncryptionKey) {
1006 encrypt_key_registry::insert(dlc_id, key);
1007 }
1008
1009 pub fn register_test_asset_path(dlc_id: &str, path: &str) {
1013 encrypt_key_registry::register_asset_path(dlc_id, path);
1014 }
1015
1016 pub fn clear_test_registry() {
1020 encrypt_key_registry::clear_all();
1021 }
1022
1023 pub fn is_malicious_data(data: &[u8]) -> bool {
1024 crate::pack_format::is_data_executable(data)
1025 }
1026}