From 36d0ef6f94906ffdda00a6f35e1a320ed27037c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Sj=C3=B6blom?= Date: Mon, 5 Oct 2026 22:25:17 +0200 Subject: [PATCH] List the encryption keys of files with unknown names --- src/mpq/list.cpp | 17 +++++++++++++++++ test/test_list.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/src/mpq/list.cpp b/src/mpq/list.cpp index ee174fd..18f0583 100644 --- a/src/mpq/list.cpp +++ b/src/mpq/list.cpp @@ -41,6 +41,11 @@ void ListFiles(HANDLE archive, const std::optional &listf // Named properties are only printed by the detailed listing list_detailed = true; } + const bool prints_key = + std::find(properties_to_print.begin(), properties_to_print.end(), "encryption-key") != + properties_to_print.end() || + std::find(properties_to_print.begin(), properties_to_print.end(), "encryption-key-raw") != + properties_to_print.end(); static const std::map property_info_class = { {"hash-index", SFileInfoHashIndex}, @@ -108,6 +113,18 @@ void ListFiles(HANDLE archive, const std::optional &listf continue; } + // The key of an encrypted file opened without its name isn't known when the + // file is opened - StormLib works it out on the first read. Read a byte so that + // encryption-key and encryption-key-raw show it. Files opened by name already + // have their key, and unencrypted files have none. + if (prints_key && + (GetFileInfo(file, SFileInfoFlags) & MPQ_FILE_ENCRYPTED) && + GetFileInfo(file, SFileInfoEncryptionKey) == 0) { + char first_byte; + DWORD bytes_read; + SFileReadFile(file, &first_byte, 1, &bytes_read, nullptr); + } + for (const auto &prop : properties_to_print) { auto it = property_info_class.find(prop); if (it == property_info_class.end()) diff --git a/test/test_list.py b/test/test_list.py index 83e3dc8..ed4069b 100644 --- a/test/test_list.py +++ b/test/test_list.py @@ -519,3 +519,45 @@ def test_list_mpq_property_locale(binary_path): output_lines = set(result.stdout.splitlines()) assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" assert output_lines == expected_output, f"Unexpected output: {output_lines}" + + +def test_list_encryption_keys_of_files_listed_without_names(binary_path, generate_mpq_without_internal_listfile, tmp_path): + """ + Test MPQ file listing of encryption keys, for encrypted files whose names aren't known. + + This test checks: + - That an encrypted file listed without its name shows the same encryption-key and + encryption-key-raw as when it is listed by name, and not zero. + """ + _ = generate_mpq_without_internal_listfile + script_dir = Path(__file__).parent + test_file = script_dir / "data" / "mpq_without_internal_listfile.mpq" + listfile = tmp_path / "listfile.txt" + listfile.write_text("capybaras.txt\ncats.txt\ndogs.txt\n", newline="\n") + + def keys_by_file_index(*extra_args): + result = subprocess.run( + [str(binary_path), "list", "-d", str(test_file), *extra_args, + "-p", "file-index", "-p", "flags", "-p", "encryption-key", "-p", "encryption-key-raw"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + keys = {} + for line in result.stdout.splitlines(): + file_index, flags, key, key_raw, name = line.split(maxsplit=4) + if "e" in flags: # Only encrypted files have a key - (signature) isn't encrypted + keys[file_index] = (flags, key, key_raw, name) + return keys + + without_names = keys_by_file_index() + with_names = keys_by_file_index("-l", str(listfile)) + + assert len(without_names) == 3 + assert without_names.keys() == with_names.keys() + for file_index, (flags, key, key_raw, name) in without_names.items(): + assert name.startswith("File") and name.endswith(".xxx"), f"Unexpected name: {name}" + assert key != "00000000", f"No encryption key found for file {file_index}" + assert (key, key_raw) == with_names[file_index][1:3], \ + f"Keys of file {file_index} differ: {(key, key_raw)} vs {with_names[file_index][1:3]}"