shadPS4/src/core/file_format/pkg.cpp

376 lines
14 KiB
C++

// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <fstream>
#include <zlib-ng.h>
#include "common/io_file.h"
#include "pkg.h"
#include "pkg_type.h"
static void DecompressPFSC(std::span<const char> compressed_data,
std::span<char> decompressed_data) {
zng_stream decompressStream;
decompressStream.zalloc = Z_NULL;
decompressStream.zfree = Z_NULL;
decompressStream.opaque = Z_NULL;
if (zng_inflateInit(&decompressStream) != Z_OK) {
// std::cerr << "Error initializing zlib for deflation." << std::endl;
}
decompressStream.avail_in = compressed_data.size();
decompressStream.next_in = reinterpret_cast<const Bytef*>(compressed_data.data());
decompressStream.avail_out = decompressed_data.size();
decompressStream.next_out = reinterpret_cast<Bytef*>(decompressed_data.data());
if (zng_inflate(&decompressStream, Z_FINISH)) {
}
if (zng_inflateEnd(&decompressStream) != Z_OK) {
// std::cerr << "Error ending zlib inflate" << std::endl;
}
}
u32 GetPFSCOffset(std::span<const u8> pfs_image) {
static constexpr u32 PfscMagic = 0x43534650;
u32 value;
for (u32 i = 0x20000; i < pfs_image.size(); i += 0x10000) {
std::memcpy(&value, &pfs_image[i], sizeof(u32));
if (value == PfscMagic)
return i;
}
return -1;
}
std::filesystem::path findDirectory(const std::filesystem::path& rootFolder,
const std::filesystem::path& targetDirectory) {
for (const auto& entry : std::filesystem::recursive_directory_iterator(rootFolder)) {
if (std::filesystem::is_directory(entry) &&
entry.path().filename() == targetDirectory.filename()) {
return entry.path();
}
}
return std::filesystem::path(); // Return an empty path if not found
}
PKG::PKG() = default;
PKG::~PKG() = default;
bool PKG::Open(const std::string& filepath) {
Common::FS::IOFile file(filepath, Common::FS::FileAccessMode::Read);
if (!file.IsOpen()) {
return false;
}
pkgSize = file.GetSize();
PKGHeader pkgheader;
file.ReadRaw<u8>(&pkgheader, sizeof(PKGHeader));
// Find title id it is part of pkg_content_id starting at offset 0x40
file.Seek(0x47); // skip first 7 characters of content_id
file.ReadRaw<u8>(&pkgTitleID, sizeof(pkgTitleID));
file.Close();
return true;
}
bool PKG::Extract(const std::string& filepath, const std::filesystem::path& extract,
std::string& failreason) {
extract_path = extract;
pkgpath = filepath;
Common::FS::IOFile file(filepath, Common::FS::FileAccessMode::Read);
if (!file.IsOpen()) {
return false;
}
pkgSize = file.GetSize();
file.ReadRaw<u8>(&pkgheader, sizeof(PKGHeader));
if (pkgheader.pkg_size > pkgSize) {
failreason = "PKG file size is different";
return false;
}
if ((pkgheader.pkg_content_size + pkgheader.pkg_content_offset) > pkgheader.pkg_size) {
failreason = "Content size is bigger than pkg size";
return false;
}
file.Seek(0);
pkg.resize(pkgheader.pkg_promote_size);
file.ReadRaw<u8>(pkg.data(), pkgheader.pkg_promote_size);
u32 offset = pkgheader.pkg_table_entry_offset;
u32 n_files = pkgheader.pkg_table_entry_count;
std::array<u8, 32> seed_digest;
std::array<std::array<u8, 32>, 7> digest1;
std::array<std::array<u8, 256>, 7> key1;
std::array<u8, 256> imgkeydata;
for (int i = 0; i < n_files; i++) {
PKGEntry entry;
std::memcpy(&entry, &pkg[offset + i * 0x20], sizeof(entry));
// Try to figure out the name
const auto name = GetEntryNameByType(entry.id);
if (name.empty()) {
// Just print with id
Common::FS::IOFile out(extract_path / "sce_sys" / std::to_string(entry.id),
Common::FS::FileAccessMode::Write);
out.WriteRaw<u8>(pkg.data() + entry.offset, entry.size);
out.Close();
continue;
}
const auto filepath = extract_path / "sce_sys" / name;
std::filesystem::create_directories(filepath.parent_path());
if (entry.id == 0x1) { // DIGESTS, seek;
// file.Seek(entry.offset, fsSeekSet);
} else if (entry.id == 0x10) { // ENTRY_KEYS, seek;
file.Seek(entry.offset);
file.Read(seed_digest);
for (int i = 0; i < 7; i++) {
file.Read(digest1[i]);
}
for (int i = 0; i < 7; i++) {
file.Read(key1);
}
PKG::crypto.RSA2048Decrypt(dk3_, key1[3], true); // decrypt DK3
} else if (entry.id == 0x20) { // IMAGE_KEY, seek; IV_KEY
file.Seek(entry.offset);
file.Read(imgkeydata);
// The Concatenated iv + dk3 imagekey for HASH256
std::array<CryptoPP::byte, 64> concatenated_ivkey_dk3;
std::memcpy(concatenated_ivkey_dk3.data(), &entry, sizeof(entry));
std::memcpy(concatenated_ivkey_dk3.data() + sizeof(entry), dk3_.data(), sizeof(dk3_));
PKG::crypto.ivKeyHASH256(concatenated_ivkey_dk3, ivKey); // ivkey_
// imgkey_ to use for last step to get ekpfs
PKG::crypto.aesCbcCfb128Decrypt(ivKey, imgkeydata, imgKey);
// ekpfs key to get data and tweak keys.
PKG::crypto.RSA2048Decrypt(ekpfsKey, imgKey, false);
} else if (entry.id == 0x80) {
// GENERAL_DIGESTS, seek;
// file.Seek(entry.offset, fsSeekSet);
}
Common::FS::IOFile out(extract_path / "sce_sys" / name, Common::FS::FileAccessMode::Write);
out.WriteRaw<u8>(pkg.data() + entry.offset, entry.size);
out.Close();
}
// Read the seed
std::array<CryptoPP::byte, 16> seed;
file.Seek(pkgheader.pfs_image_offset + 0x370);
file.ReadRaw<u8>(seed.data(), seed.size());
// Get data and tweak keys.
PKG::crypto.PfsGenCryptoKey(ekpfsKey, seed, dataKey, tweakKey);
const u32 length = pkgheader.pfs_cache_size * 0x2; // Seems to be ok.
// Read encrypted pfs_image
std::vector<u8> pfs_encrypted(length);
file.Seek(pkgheader.pfs_image_offset);
file.ReadRaw<u8>(pfs_encrypted.data(), length);
// Decrypt the pfs_image.
std::vector<u8> pfs_decrypted(length);
PKG::crypto.decryptPFS(dataKey, tweakKey, pfs_encrypted, pfs_decrypted, 0);
// Retrieve PFSC from decrypted pfs_image.
pfsc_offset = GetPFSCOffset(pfs_decrypted);
std::vector<u8> pfsc(length);
std::memcpy(pfsc.data(), pfs_decrypted.data() + pfsc_offset, length - pfsc_offset);
PFSCHdr pfsChdr;
std::memcpy(&pfsChdr, pfsc.data(), sizeof(pfsChdr));
const int num_blocks = (int)(pfsChdr.data_length / pfsChdr.block_sz2);
sectorMap.resize(num_blocks + 1); // 8 bytes, need extra 1 to get the last offset.
for (int i = 0; i < num_blocks + 1; i++) {
std::memcpy(&sectorMap[i], pfsc.data() + pfsChdr.block_offsets + i * 8, 8);
}
u32 ent_size = 0;
u32 ndinode = 0;
std::vector<char> compressedData;
std::vector<char> decompressedData(0x10000);
bool dinode_reached = false;
// Get iNdoes.
for (int i = 0; i < num_blocks; i++) {
const u64 sectorOffset = sectorMap[i];
const u64 sectorSize = sectorMap[i + 1] - sectorOffset;
compressedData.resize(sectorSize);
std::memcpy(compressedData.data(), pfsc.data() + sectorOffset, sectorSize);
if (sectorSize == 0x10000) // Uncompressed data
std::memcpy(decompressedData.data(), compressedData.data(), 0x10000);
else if (sectorSize < 0x10000) // Compressed data
DecompressPFSC(compressedData, decompressedData);
if (i == 0) {
std::memcpy(&ndinode, decompressedData.data() + 0x30, 4); // number of folders and files
}
int occupied_blocks =
(ndinode * 0xA8) / 0x10000; // how many blocks(0x10000) are taken by iNodes.
if (((ndinode * 0xA8) % 0x10000) != 0)
occupied_blocks += 1;
if (i >= 1 && i <= occupied_blocks) { // Get all iNodes, gives type, file size and location.
for (int p = 0; p < 0x10000; p += 0xA8) {
Inode node;
std::memcpy(&node, &decompressedData[p], sizeof(node));
if (node.Mode == 0) {
break;
}
iNodeBuf.push_back(node);
}
}
const char dot = decompressedData[0x10];
const std::string_view dotdot(&decompressedData[0x28], 2);
if (dot == '.' && dotdot == "..") {
dinode_reached = true;
}
// Get folder and file names.
bool end_reached = false;
if (dinode_reached) {
for (int j = 0; j < 0x10000; j += ent_size) { // Skip the first parent and child.
Dirent dirent;
std::memcpy(&dirent, &decompressedData[j], sizeof(dirent));
// Stop here and continue the main loop
if (dirent.ino == 0) {
break;
}
if (dot != '.' && dotdot != "..") {
end_reached = true;
}
ent_size = dirent.entsize;
auto& table = fsTable.emplace_back();
table.name = std::string(dirent.name, dirent.namelen);
table.inode = dirent.ino;
table.type = dirent.type;
if (table.type == PFS_DIR) {
folderMap[table.inode] = table.name;
}
}
// Seems to be the last entry, at least for the games I tested. To check as we go.
const std::string_view rightsprx(&decompressedData[0x40], 10);
if (rightsprx == "right.sprx" || end_reached) {
break;
}
}
}
// Create Folders.
folderMap[2] = GetTitleID(); // Set up game path instead of calling it uroot
game_dir = extract_path.parent_path();
title_dir = game_dir / GetTitleID();
for (int i = 0; i < fsTable.size(); i++) {
const u32 inode_number = fsTable[i].inode;
const u32 inode_type = fsTable[i].type;
const auto inode_name = fsTable[i].name;
if (inode_type == PFS_CURRENT_DIR) {
current_dir = folderMap[inode_number];
} else if (inode_type == PFS_PARENT_DIR) {
parent_dir = folderMap[inode_number];
// Skip uroot folder. we create our own game uid folder
if (i > 1) {
const auto par_dir = inode_number == 2 ? findDirectory(game_dir, parent_dir)
: findDirectory(title_dir, parent_dir);
const auto cur_dir = findDirectory(par_dir, current_dir);
if (cur_dir == "") {
extract_path = par_dir / current_dir;
std::filesystem::create_directories(extract_path);
} else {
extract_path = cur_dir;
}
}
}
extractMap[inode_number] = extract_path.string();
}
return true;
}
void PKG::ExtractFiles(const int& index) {
int inode_number = fsTable[index].inode;
int inode_type = fsTable[index].type;
std::string inode_name = fsTable[index].name;
if (inode_type == PFS_FILE) {
int sector_loc = iNodeBuf[inode_number].loc;
int nblocks = iNodeBuf[inode_number].Blocks;
int bsize = iNodeBuf[inode_number].Size;
std::string file_extracted = extractMap[inode_number] + "/" + inode_name;
Common::FS::IOFile inflated;
inflated.Open(file_extracted, Common::FS::FileAccessMode::Write);
Common::FS::IOFile pkgFile; // Open the file for each iteration to avoid conflict.
pkgFile.Open(pkgpath, Common::FS::FileAccessMode::Read);
int size_decompressed = 0;
std::vector<char> compressedData;
std::vector<char> decompressedData(0x10000);
u64 pfsc_buf_size = 0x11000; // extra 0x1000
std::vector<u8> pfsc(pfsc_buf_size);
std::vector<u8> pfs_decrypted(pfsc_buf_size);
for (int j = 0; j < nblocks; j++) {
u64 sectorOffset =
sectorMap[sector_loc + j]; // offset into PFSC_image and not pfs_image.
u64 sectorSize = sectorMap[sector_loc + j + 1] -
sectorOffset; // indicates if data is compressed or not.
u64 fileOffset = (pkgheader.pfs_image_offset + pfsc_offset + sectorOffset);
u64 currentSector1 =
(pfsc_offset + sectorOffset) / 0x1000; // block size is 0x1000 for xts decryption.
int sectorOffsetMask = (sectorOffset + pfsc_offset) & 0xFFFFF000;
int previousData = (sectorOffset + pfsc_offset) - sectorOffsetMask;
pkgFile.Seek(fileOffset - previousData);
pkgFile.ReadRaw<u8>(pfsc.data(), pfsc.size());
PKG::crypto.decryptPFS(dataKey, tweakKey, pfsc, pfs_decrypted, currentSector1);
compressedData.resize(sectorSize);
std::memcpy(compressedData.data(), pfs_decrypted.data() + previousData, sectorSize);
if (sectorSize == 0x10000) // Uncompressed data
std::memcpy(decompressedData.data(), compressedData.data(), 0x10000);
else if (sectorSize < 0x10000) // Compressed data
DecompressPFSC(compressedData, decompressedData);
size_decompressed += 0x10000;
if (j < nblocks - 1) {
inflated.WriteRaw<u8>(decompressedData.data(), decompressedData.size());
} else {
// This is to remove the zeros at the end of the file.
const u32 write_size = decompressedData.size() - (size_decompressed - bsize);
inflated.WriteRaw<u8>(decompressedData.data(), write_size);
}
}
pkgFile.Close();
inflated.Close();
}
}