Only treat code/module CArchive members as PyInstaller crypto_key markers (#398)

Only code and module members named crypto_key mark PyInstaller modules as encrypted; a data file with that name no longer does. Fixes #334.
This commit is contained in:
phatloi0010
2026-11-02 23:12:43 +07:00
committed by GitHub
parent 34216be702
commit 71b0524fce
+25 -2
View File
@@ -281,6 +281,7 @@ impl PackedFormat for PyInstaller {
find_cookie(data).is_some()
}
/// List CArchive members and nested PYZ module entries with archive metadata.
fn list(&self, data: &[u8]) -> Result<PackedListing, FormatError> {
let cookie = find_cookie(data)
.ok_or_else(|| FormatError::Malformed("no sound PyInstaller cookie (its magic, sizes and Python version must all agree)".into()))?;
@@ -291,7 +292,7 @@ impl PackedFormat for PyInstaller {
notes.push(format!("{}, so the members after it aren't listed.", capitalise(&why)));
truncated = true;
}
let key_seen = members.iter().any(|m| m.entry.path.contains("crypto_key"));
let key_seen = members.iter().any(|m| matches!(m.type_code, b'm' | b'M' | b's') && m.entry.path.contains("crypto_key"));
let scripts: Vec<&str> = members.iter().filter(|m| m.type_code == b's').map(|m| m.entry.path.as_str()).collect();
let entry_scripts: Vec<&str> = scripts.iter().copied().filter(|s| !s.starts_with("pyi_rth_") && !s.starts_with("pyiboot")).collect();
let options: Vec<&str> = members.iter().filter(|m| m.type_code == b'o').map(|m| m.entry.path.as_str()).collect();
@@ -348,9 +349,15 @@ impl PackedFormat for PyInstaller {
Ok(PackedListing { facts, entries, notes, truncated })
}
/// Extract a CArchive member or PYZ module within `max_bytes`, including any `.pyc` header.
fn read(&self, data: &[u8], entry: &PackedEntry, max_bytes: u64) -> Result<Vec<u8>, FormatError> {
const PYC_HEADER_BYTES: u64 = 16;
let magic = entry.kind.as_deref().and_then(|k| k.strip_prefix(PYZ_KIND)).and_then(|m| u16::from_str_radix(m, 16).ok());
let body = read_stored(data, entry, max_bytes)?;
let body_limit = match magic {
Some(_) => max_bytes.checked_sub(PYC_HEADER_BYTES).ok_or_else(|| FormatError::TooLarge(format!("{} needs a .pyc header", entry.path)))?,
None => max_bytes,
};
let body = read_stored(data, entry, body_limit)?;
match magic {
Some(magic) => Ok(pyc::wrap_marshal(magic, &body)),
None => Ok(body),
@@ -429,6 +436,7 @@ mod tests {
assert_eq!(PyInstaller.read(&data, find(&l, "libz.so.1"), 100).unwrap(), b"native");
}
/// Verify that code/module crypto_key markers flag encryption while data members do not.
#[test]
fn encrypted_modules_are_flagged_and_listed_but_not_read() {
// A key module in the archive.
@@ -446,6 +454,15 @@ mod tests {
let z = pyz(3531, &[("a", 0, &[0xee; 40])], false, false);
let data = pyinstaller(&[("PYZ.pyz", b'z', &z, false)], true, 312);
assert_eq!(PyInstaller.list(&data).unwrap().facts["pyz_archives"][0]["encrypted"], true);
// An ordinary data member named crypto_key.txt does not mark unencrypted PYZ modules unreadable.
let z = pyz(3531, &[("a", 0, &code)], false, true);
let data = pyinstaller(&[("crypto_key.txt", b'x', b"plain", false), ("PYZ.pyz", b'z', &z, false)], true, 312);
let l = PyInstaller.list(&data).unwrap();
assert_eq!(l.facts["encrypted_modules"], false);
let a = find(&l, "PYZ.pyz.modules/a.pyc");
assert_eq!(a.compression, Compression::Zlib);
assert!(pyc::read_pyc(&PyInstaller.read(&data, a, 1000).unwrap()).unwrap().code.is_some());
}
#[test]
@@ -562,11 +579,17 @@ mod tests {
assert_eq!((kept.len(), total), (2, 3));
}
/// Verify that `read` enforces `max_bytes` including the 16-byte `.pyc` header for PYZ modules.
#[test]
fn reads_stop_at_their_limit() {
let data = pyinstaller_sample();
let l = PyInstaller.list(&data).unwrap();
assert!(matches!(PyInstaller.read(&data, find(&l, "struct"), 10), Err(FormatError::TooLarge(_))));
let util = find(&l, "PYZ-00.pyz.modules/pkg/util.pyc");
let full = PyInstaller.read(&data, util, 10_000).unwrap();
assert!(matches!(PyInstaller.read(&data, util, 15), Err(FormatError::TooLarge(_))));
assert!(matches!(PyInstaller.read(&data, util, (full.len() - 1) as u64), Err(FormatError::TooLarge(_))));
assert_eq!(PyInstaller.read(&data, util, full.len() as u64).unwrap(), full);
}
#[test]