From 02bd7128f7e72956b57af2700273f29d5a3b01d5 Mon Sep 17 00:00:00 2001 From: Invictus-potens Date: Wed, 4 Nov 2026 11:01:17 -0300 Subject: [PATCH] Call a dump encrypted with the original key encrypted, not one whose ExeFS has no .code Crypto method 0 without NoCrypto is the original key, the one most games are encrypted with, so such a dump got through the check and failed later with "the ExeFS has no .code". It can also be a dump decrypted with its flags left as they were, which reads fine, so it still isn't turned away up front: only the message for a missing .code now says the dump is encrypted when NoCrypto is clear. --- src/rom.rs | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/src/rom.rs b/src/rom.rs index eccab18..f45a1c8 100644 --- a/src/rom.rs +++ b/src/rom.rs @@ -292,9 +292,13 @@ impl Title { return error("the executable partition has no NCCH header"); } let flags = &header[0x188..0x190]; - // the partition is readable as it is when it says so, or when it was - // never encrypted - if flags[7] & 0x04 == 0 && flags[3] != 0 { + // NoCrypto says the partition is readable as it is. without it, a + // crypto method (flags[3]) other than 0 is one of the newer keys; + // 0 is the original key, which most games use, or a dump decrypted + // with the flags left as they were, so that one is only called + // encrypted when its ExeFS turns out unreadable, below + let no_crypto = flags[7] & 0x04 != 0; + if !no_crypto && flags[3] != 0 { return error("the dump is encrypted, it has to be decrypted first"); } let program_id = u64_at(&header, 0x118); @@ -313,7 +317,13 @@ impl Title { .take(10) .find(|entry| ascii(&entry[..8]) == ".code") .map(|entry| (exefs + EXEFS_HEADER_SIZE + u32_at(entry, 8) as u64, u32_at(entry, 12) as u64)) - .ok_or(Error("the ExeFS has no .code".into()))?; + .ok_or_else(|| { + Error(if no_crypto { + "the ExeFS has no .code".into() + } else { + "the dump is encrypted, it has to be decrypted first (the ExeFS has no .code)".into() + }) + })?; let romfs = if has_romfs { read_romfs(&mut file, romfs_offset, read).ok() } else { None }; Ok(Title { file: Mutex::new(file), program_id, exheader, code, romfs }) @@ -576,6 +586,28 @@ pub(crate) mod tests { std::fs::remove_file(path).unwrap(); } + /// a dump encrypted with the original key, crypto method 0 and no + /// NoCrypto, is called encrypted, and one decrypted without its flags + /// being updated still loads. + #[test] + fn an_encrypted_dump_says_so() { + let path = rom("encrypted", &[1, 2, 3, 4], &[]); + let mut bytes = std::fs::read(&path).unwrap(); + bytes[0x18F] = 0; + std::fs::write(&path, &bytes).unwrap(); + assert_eq!(Title::load(&path).unwrap().code().unwrap(), [1, 2, 3, 4]); + // the ExeFS as encryption leaves it, garbage where the names were + put(&mut bytes, 0x600, &[0x8F, 0x13, 0xA2, 0x07, 0x51, 0xE4, 0x3C, 0x90]); + std::fs::write(&path, &bytes).unwrap(); + let message = Title::load(&path).err().unwrap().to_string(); + assert!(message.starts_with("the dump is encrypted"), "{message}"); + // decrypted and said so, but with no .code, is told as it is + bytes[0x18F] = 0x04; + std::fs::write(&path, &bytes).unwrap(); + assert_eq!(Title::load(&path).err().unwrap().to_string(), "the ExeFS has no .code"); + std::fs::remove_file(path).unwrap(); + } + /// a literal and a run, the smallest stream that exercises both. #[test] fn backwards_lzss_decompresses() {