Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,22 @@ if(PSPRECOMP_BUILD_TESTS)
add_test(NAME portablekit_rtc_tests COMMAND portablekit_rtc_tests)
endif()

# PGD containers with dummy keys: no console key and no game data.
if(PSPRECOMP_BUILD_TESTS)
add_executable(portablekit_pgd_tests
"${PORTABLEKIT_ROOT}/tests/pgd_tests.cpp"
"${PORTABLEKIT_ROOT}/host/crypto/pgd.cpp"
"${PORTABLEKIT_ROOT}/host/save_data/aes128.cpp"
"${PORTABLEKIT_ROOT}/host/save_data/savedata_crypto.cpp"
"${PORTABLEKIT_ROOT}/tests/pgd_test_keys.cpp"
"${PORTABLEKIT_ROOT}/third_party/tiny_aes/aes.c")
set_source_files_properties("${PORTABLEKIT_ROOT}/third_party/tiny_aes/aes.c" PROPERTIES LANGUAGE CXX)
target_include_directories(portablekit_pgd_tests PRIVATE "${PORTABLEKIT_ROOT}/host" "${PORTABLEKIT_ROOT}/third_party" "${PORTABLEKIT_ROOT}/include")
target_link_libraries(portablekit_pgd_tests PRIVATE psprecomp_core)
set_target_properties(portablekit_pgd_tests PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
add_test(NAME portablekit_pgd_tests COMMAND portablekit_pgd_tests)
endif()

# Each platform's settings defaults.
if(PSPRECOMP_BUILD_TESTS)
add_executable(portablekit_settings_tests
Expand Down
46 changes: 45 additions & 1 deletion apps/portablekit/src/app_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
#include "text_format.hpp"

#include "app_paths.hpp"
#include "crypto/pgd.hpp"
#include "kernel/iso_image.hpp"
#include "corpus_abi.hpp"
#include "hle/hle_extensions.hpp"
#include "install/user_data.hpp"
Expand All @@ -40,12 +42,14 @@

#include "psprecomp/interpreter.hpp"

#include <algorithm>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <thread>
#include <vector>
Expand Down Expand Up @@ -428,7 +432,45 @@ int command_keys(const Arguments &args) {
}
return ok ? 0 : kKeysRejected;
}
if (action != "status") return report_error(kUsage, "keys takes import or status");
// keys pgd-check <game> <path on the disc> <key, 32 hex digits>: whether
// this PGD file (crypto/pgd.hpp) opens with the keys file's keys: its
// header MAC matches and its description is valid. The key is the one the
// game passes to sceIoIoctl 0x04100001 (<prefix>_TRACE_IO shows it).
// Prints the description and how much of the first data block is zero,
// never a key.
if (action == "pgd-check") {
if (args.positional.size() < 5) return report_error(kUsage, "keys pgd-check needs <game> <path on the disc> <key>");
const auto game = find_game(args.positional[2]);
if (!game) return report_error(kNoSuchGame, "no such game: " + args.positional[2]);
std::vector<std::uint8_t> key_bytes;
const std::string hex = args.positional[4];
for (std::size_t i = 0; i + 1 < hex.size(); i += 2) key_bytes.push_back(static_cast<std::uint8_t>(std::stoul(hex.substr(i, 2), nullptr, 16)));
if (key_bytes.size() != 16u) return report_error(kUsage, "the key is 16 bytes, 32 hex digits");
pgd::Block vkey{};
std::copy(key_bytes.begin(), key_bytes.end(), vkey.begin());
IsoImage iso(game->iso);
const auto entry = iso.find(args.positional[3]);
if (!entry) return report_error(kUsage, "no such file on the disc: " + args.positional[3]);
const std::uint64_t base = static_cast<std::uint64_t>(entry->lba) * IsoImage::kSectorSize;
std::vector<std::uint8_t> header(pgd::kHeaderSize);
iso.read(base, header);
std::string error;
auto file = pgd::PgdFile::open(header, vkey, entry->size, pgd::player_keys(), error);
if (!file) {
std::cout << "does not open: " << error << "\n";
return 1;
}
std::vector<std::uint8_t> first(file->desc().block_size);
const std::size_t got = file->read(0, first, [&iso, base](std::uint64_t offset, std::span<std::uint8_t> out) {
return iso.read(base + offset, out);
});
first.resize(got);
std::cout << "header MAC matches; data size " << file->size() << ", blocks of " << file->desc().block_size
<< " from " << file->desc().data_offset << "; first block: "
<< std::count(first.begin(), first.end(), std::uint8_t{0}) << " of " << got << " bytes zero\n";
return 0;
}
if (action != "status") return report_error(kUsage, "keys takes import, status or pgd-check");
const KeysReport &report = active_keys();
if (g_json) {
std::vector<Json> problems;
Expand All @@ -441,6 +483,7 @@ int command_keys(const Arguments &args) {
std::cout << Json().field("ok", true).field("path", path_text(report.path)).field("found", report.file_found)
.field("executables", report.can_decrypt_executables)
.field("saves", report.can_encrypt_saves)
.field("pgd", report.can_decrypt_pgd)
.field("tags", static_cast<std::uint64_t>(report.tag_count))
.array("problems", problems).array("warnings", warnings)
.array("extension_keys", declared).str() << "\n";
Expand All @@ -449,6 +492,7 @@ int command_keys(const Arguments &args) {
std::cout << "Keys file: " << path_text(report.path) << (report.file_found ? "" : " (none)") << "\n"
<< " decrypt executables: " << (report.can_decrypt_executables ? "yes" : "no") << "\n"
<< " encrypt saves: " << (report.can_encrypt_saves ? "yes" : "no (saves are kept unencrypted)") << "\n"
<< " decrypt PGD data: " << (report.can_decrypt_pgd ? "yes" : "no") << "\n"
<< " tag keys: " << report.tag_count << "\n";
for (const std::string &problem : report.problems) std::cout << " problem: " << problem << "\n";
for (const std::string &warning : report.warnings) std::cout << " warning: " << warning << "\n";
Expand Down
40 changes: 36 additions & 4 deletions apps/portablekit/src/keys_file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,33 @@ constexpr Fingerprint kFingerprints[] = {
{"savedata.5", "9f7bfa1f5136d4dfdd3a8322578dbbe5243bf4eb70428b101f27eb1144027461"},
{"savedata.6", "b58242561b16a8925126dbae5558dda53d6c275d712ee69f5bebce37c577af2d"},
{"savedata.7", "a0bf478c1471e4115011e5e0f70d5e2d43c95e5ef2645d164b76ec3cd118ec10"},
{"kirk.aes.38", "b5e2a84dbaaf99015874182a8609494d3c8e38f1837755ee29858c78b986ad17"},
{"kirk.aes.39", "be3798d2e2a46c0018a4f7e41f5557e6cc2aeb64b0b15ae6b7063a8fa41c8559"},
{"kirk.aes.63", "16df5079b5e31b07045b9d117c9db8bf9c45e606dbe4a9c71bb763f12d61be83"},
{"amctrl.1CD4", "dbe5dd77c4a5f3b930b30f520ae71064a4b48161ba43d79252351effc0ecb403"},
{"amctrl.1CE4", "4aae0892755232421ac3077d844110b0c0334eeadc9cef6093e55a3fb8e56ee4"},
{"amctrl.1CF4", "e0942366cf4a2142ac1369df3d404198ac8d9bf9df73ccde6e540f2554dbcf4c"},
{"amctrl.dnas.1A90", "3f238f8d5e24ce9402f00be5bb869155d83bcf05457948faa36822a20af04057"},
{"amctrl.dnas.1AA0", "47d6a491c1dc0424e79d60b28fd7f9fc9fb6f7be0552cf4e77dfdcbea3260869"},
};

// The keys the PGD format needs (host/crypto/pgd.hpp), with fingerprints
// above. The DNAS ones are taken but not used yet: no PGD file of that kind
// has been checked.
int amctrl_index(const std::string &name) {
if (name == "amctrl.1cd4") return 1;
if (name == "amctrl.1ce4") return 2;
if (name == "amctrl.1cf4") return 3;
if (name == "amctrl.dnas.1a90") return 4;
if (name == "amctrl.dnas.1aa0") return 5;
return 0;
}

bool pgd_key_name(const std::string &name) {
return name == "kirk.aes.38" || name == "kirk.aes.39" || name == "kirk.aes.63" ||
amctrl_index(name) != 0;
}

const char *expected_fingerprint(const std::string &name) {
for (const Fingerprint &entry : kFingerprints)
if (lower(name) == lower(entry.name)) return entry.sha256;
Expand Down Expand Up @@ -154,11 +179,11 @@ KeysReport read_keys_file(const std::filesystem::path &path) {
continue;
}
const char *expected = expected_fingerprint(name);
if (expected == nullptr) {
if (expected == nullptr && !pgd_key_name(name)) {
report.problems.push_back(raw_name + " is not a key this program uses.");
continue;
}
if (sha256_of(*bytes) != expected) {
if (expected != nullptr && sha256_of(*bytes) != expected) {
report.problems.push_back(raw_name + " is not the right key: its fingerprint does not match.");
continue;
}
Expand All @@ -169,6 +194,8 @@ KeysReport read_keys_file(const std::filesystem::path &path) {
report.keys.kirk_cmd1 = to_key(*bytes);
} else if (name.starts_with("savedata.")) {
report.keys.savedata[std::atoi(name.substr(9).c_str())] = to_key(*bytes);
} else if (const int index = amctrl_index(name); index != 0) {
report.keys.amctrl[index] = to_key(*bytes);
}
}
sort_out_unused_names(report, declared_found);
Expand All @@ -180,6 +207,11 @@ KeysReport read_keys_file(const std::filesystem::path &path) {
if (report.keys.kirk(slot) == nullptr) report.can_encrypt_saves = false;
for (int index = 2; index <= 7; ++index)
if (report.keys.savedata_key(index) == nullptr) report.can_encrypt_saves = false;
report.can_decrypt_pgd = true;
for (const std::uint8_t slot : {0x38, 0x39, 0x63})
if (report.keys.kirk(slot) == nullptr) report.can_decrypt_pgd = false;
for (int index = 1; index <= 3; ++index)
if (report.keys.amctrl.find(index) == report.keys.amctrl.end()) report.can_decrypt_pgd = false;
return report;
}

Expand All @@ -202,7 +234,7 @@ bool import_keys_file(const std::filesystem::path &source, KeysReport &report) {
if (!report.problems.empty()) return false;
if (!report.keys.named.empty()) return copy_to_home(source, report); // keys only modules use
if (report.keys.kirk_aes.empty() && !report.keys.kirk_cmd1 && report.keys.savedata.empty() &&
report.keys.tags.empty()) {
report.keys.tags.empty() && report.keys.amctrl.empty()) {
report.problems.push_back("The file holds no keys.");
return false;
}
Expand All @@ -225,7 +257,7 @@ const CryptoKeys *crypto_keys() {
const app::KeysReport &report = app::active_keys();
if (!report.keys.named.empty()) return &report.keys;
if (report.keys.kirk_aes.empty() && !report.keys.kirk_cmd1 && report.keys.savedata.empty() &&
report.keys.tags.empty())
report.keys.tags.empty() && report.keys.amctrl.empty())
return nullptr;
return &report.keys;
}
Expand Down
5 changes: 5 additions & 0 deletions apps/portablekit/src/keys_file.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
// savedata.2 = <16 bytes> save-data keys 2 to 7
// tag.C0CB167C = <16 or 0x90 bytes> what an executable's tag selects
// tag.C0CB167C.slot = 5D (optional) the KIRK slot of that tag
// amctrl.1CD4 = <16 bytes> the DRM library's fixed keys, which PGD data
// needs with kirk.aes.38, .39 and .63; also
// amctrl.1CE4, .1CF4, amctrl.dnas.1A90 and
// .1AA0 (host/crypto/pgd.hpp)
//
// HLE extension modules may declare more names (Registry::declare_key), each
// with its length and optionally a fingerprint; they are read and checked the
Expand Down Expand Up @@ -48,6 +52,7 @@ struct KeysReport {
// What the keys that passed allow.
bool can_decrypt_executables{}; // kirk.aes.5D and kirk.cmd1
bool can_encrypt_saves{}; // every save-data key
bool can_decrypt_pgd{}; // kirk.aes.38/39/63 and amctrl.1CD4/1CE4/1CF4
std::size_t tag_count{};
};

Expand Down
1 change: 1 addition & 0 deletions cmake/PortableKit.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ set(PORTABLEKIT_HOST_SOURCES
host/save_data/aes128.cpp
host/save_data/param_sfo.cpp
host/save_data/savedata_crypto.cpp
host/crypto/pgd.cpp
host/save_data/savedata_store.cpp
host/save_data/save_transfer.cpp
host/install/executable_preparation.cpp
Expand Down
10 changes: 10 additions & 0 deletions docs/DESKTOP_APP.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,12 @@ kirk.cmd1 = <16 bytes> # the KIRK command 1 AES key
savedata.2 = <16 bytes> # save-data keys 2 to 7
tag.C0CB167C = <16 bytes, or the 0x90-byte table of an old header layout>
tag.C0CB167C.slot = 5D # optional: the KIRK slot the tag's header uses
amctrl.1CD4 = <16 bytes> # the DRM library's fixed keys, for PGD data (also .1CE4, .1CF4, amctrl.dnas.1A90, .1AA0)
```

- `portablekit keys import <file>` checks the file and copies it into the data folder. Every fixed key is checked against a SHA-256 fingerprint compiled into the program, so the player is told "`savedata.4` is not the right key" instead of getting garbage later. Tag keys have no fingerprint: an executable that decrypts to an ELF the runtime can load is the check.
- What each group enables: `kirk.aes.5D` + `kirk.cmd1` + the game's `tag.*` decrypt that game's `EBOOT.BIN`; the nine save slots and `savedata.2`鈥揱7` encrypt and decrypt saves. `portablekit keys status` says which of these the file provides.
- PGD data (below) uses `kirk.aes.38`, `kirk.aes.39`, `kirk.aes.63`, `amctrl.1CD4`, `amctrl.1CE4`, `amctrl.1CF4`, `amctrl.dnas.1A90` and `amctrl.dnas.1AA0` ; `keys status` says "decrypt PGD data: yes" when the first six are there. All eight have fingerprints, checked against PSP2i's file; the two DNAS keys are accepted but not used yet, since no PGD file of that kind has been checked.
- Without keys, a game can still be added when its disc carries an unencrypted `BOOT.BIN` (LocoRoco 2 does), or when the player gives an executable they decrypted (`--executable`). The program never names a source of keys.
- A name the program does not use is a warning ("... is not a key this program or its extension modules use; it is ignored.") and is left out; it does not refuse the file. HLE extension modules the build links may declare more names, which are read and checked like the program's own ([HLE_EXTENSIONS.md](HLE_EXTENSIONS.md#keys)); `keys status` lists them by module.

Expand All @@ -174,6 +176,14 @@ Messages when something is missing, as the prototype prints them:
- *"The save is encrypted, as a PSP writes it. Importing it needs the console's keys: add a keys file."* (the save menu's import)
- In the log, for an encrypted save found in `ms0`: *"the save is encrypted, and reading it needs the console's keys (add a keys file)"*.

### PGD data

Some games keep large data files in PGD containers, "Protected Game Data" (Phantasy Star Portable 2 Infinity's `PSP_GAME/INSDIR/MEDIA.FPB`, 275 MB). The game opens the file with flag `0x40000000` and gives the library the file's key with `sceIoIoctl(fd, 0x04100001, key, 16)`; after that it reads and seeks in the decrypted data. [`host/crypto/pgd.hpp`](../host/crypto/pgd.hpp) implements the container from public descriptions of the format: the 0x90-byte header, the encrypted description (data key, size, 0x400-byte blocks from 0x90), and block-by-block decryption for random access. The keys come only from the player's file.

The public descriptions do not say which KIRK slots, masks and counter conventions the console's cipher uses; they were found by trying combinations against PSP2i's file (key index 1, DRM type 1) until its header MAC matched and its description decrypted to known values, and the data blocks to structured bytes. The result is written out in `pgd.hpp`. Other key indices and DRM types are refused with a log line, because none has been checked. The decrypted view is a file filter (`hle_extension::set_file_filter`), set by the ioctl. `portablekit keys pgd-check <game> <path on the disc> <key>` says whether a file opens with the player's keys: the MAC, the description, and how much of the first block is zero. It never prints a key.

Without the keys, the file is refused with the I/O error (what a PSP answers for a file it cannot decrypt has not been traced), and the log names the file and the missing key once: `[pgd] <file>: cannot decrypt: the keys file has no kirk.aes.39`.

## Saves

- **Without keys** the game's key is still read from its save request and remembered, but the save is written unencrypted (`SAVEDATA_PARAMS` flags 0), as early PPSSPP versions wrote them. The game sees exactly the same data. Such saves load in PPSSPP; a PSP does not accept them.
Expand Down
Loading