416 lines
15 KiB
C++
416 lines
15 KiB
C++
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include <zlib-ng.h>
|
|
#include "common/io_file.h"
|
|
#include "core/file_format/pkg.h"
|
|
#include "core/file_format/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;
|
|
}
|
|
|
|
PKG::PKG() = default;
|
|
|
|
PKG::~PKG() = default;
|
|
|
|
bool PKG::Open(const std::filesystem::path& filepath) {
|
|
Common::FS::IOFile file(filepath, Common::FS::FileAccessMode::Read);
|
|
if (!file.IsOpen()) {
|
|
return false;
|
|
}
|
|
pkgSize = file.GetSize();
|
|
|
|
file.Read(pkgheader);
|
|
if (pkgheader.magic != 0x7F434E54)
|
|
return false;
|
|
|
|
for (const auto& flag : flagNames) {
|
|
if (isFlagSet(pkgheader.pkg_content_flags, flag.first)) {
|
|
if (!pkgFlags.empty())
|
|
pkgFlags += (", ");
|
|
pkgFlags += (flag.second);
|
|
}
|
|
}
|
|
|
|
// 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.Read(pkgTitleID);
|
|
|
|
file.Seek(0);
|
|
pkg.resize(pkgheader.pkg_promote_size);
|
|
file.Read(pkg);
|
|
|
|
u32 offset = pkgheader.pkg_table_entry_offset;
|
|
u32 n_files = pkgheader.pkg_table_entry_count;
|
|
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 == "param.sfo") {
|
|
sfo.clear();
|
|
file.Seek(entry.offset);
|
|
sfo.resize(entry.size);
|
|
file.ReadRaw<u8>(sfo.data(), entry.size);
|
|
}
|
|
}
|
|
file.Close();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool PKG::Extract(const std::filesystem::path& 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.magic != 0x7F434E54)
|
|
return false;
|
|
|
|
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.Read(pkg);
|
|
|
|
u32 offset = pkgheader.pkg_table_entry_offset;
|
|
u32 n_files = pkgheader.pkg_table_entry_count;
|
|
|
|
std::array<u8, 64> concatenated_ivkey_dk3;
|
|
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);
|
|
const auto filepath = extract_path / "sce_sys" / name;
|
|
std::filesystem::create_directories(filepath.parent_path());
|
|
|
|
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;
|
|
}
|
|
|
|
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[i]);
|
|
}
|
|
|
|
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::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();
|
|
|
|
// Decrypt Np stuff and overwrite.
|
|
if (entry.id == 0x400 || entry.id == 0x401 || entry.id == 0x402 ||
|
|
entry.id == 0x403) { // somehow 0x401 is not decrypting
|
|
decNp.resize(entry.size);
|
|
std::span<u8> cipherNp(pkg.data() + entry.offset, entry.size);
|
|
std::array<u8, 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);
|
|
PKG::crypto.aesCbcCfb128DecryptEntry(ivKey, cipherNp, decNp);
|
|
|
|
Common::FS::IOFile out(extract_path / "sce_sys" / name,
|
|
Common::FS::FileAccessMode::Write);
|
|
out.Write(decNp);
|
|
out.Close();
|
|
}
|
|
}
|
|
|
|
// Extract trophy files
|
|
if (!trp.Extract(extract_path)) {
|
|
// Do nothing some pkg come with no trp file.
|
|
// return false;
|
|
}
|
|
|
|
// Read the seed
|
|
std::array<u8, 16> seed;
|
|
file.Seek(pkgheader.pfs_image_offset + 0x370);
|
|
file.Read(seed);
|
|
|
|
// 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.Read(pfs_encrypted);
|
|
file.Close();
|
|
// 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(§orMap[i], pfsc.data() + pfsChdr.block_offsets + i * 8, 8);
|
|
}
|
|
|
|
u32 ent_size = 0;
|
|
u32 ndinode = 0;
|
|
int ndinode_counter = 0;
|
|
bool dinode_reached = false;
|
|
bool uroot_reached = false;
|
|
std::vector<char> compressedData;
|
|
std::vector<char> decompressedData(0x10000);
|
|
|
|
// Get iNdoes and Dirents.
|
|
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);
|
|
}
|
|
}
|
|
|
|
// let's deal with the root/uroot enteries here.
|
|
// Sometimes it's more than 2 enteries (Tomb Raider Remastered)
|
|
const std::string_view flat_path_table(&decompressedData[0x10], 15);
|
|
if (flat_path_table == "flat_path_table") {
|
|
uroot_reached = true;
|
|
}
|
|
|
|
if (uroot_reached) {
|
|
for (int i = 0; i < 0x10000; i += ent_size) {
|
|
Dirent dirent;
|
|
std::memcpy(&dirent, &decompressedData[i], sizeof(dirent));
|
|
ent_size = dirent.entsize;
|
|
if (dirent.ino != 0) {
|
|
ndinode_counter++;
|
|
} else {
|
|
// Set the the folder according to the current inode.
|
|
// Can be 2 or more (rarely)
|
|
extractPaths[ndinode_counter] = extract_path.parent_path() / GetTitleID();
|
|
uroot_reached = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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_CURRENT_DIR) {
|
|
current_dir = extractPaths[table.inode];
|
|
}
|
|
extractPaths[table.inode] =
|
|
current_dir.string() / std::filesystem::path(table.name);
|
|
|
|
if (table.type == PFS_FILE || table.type == PFS_DIR) {
|
|
if (table.type == PFS_DIR) { // Create dirs.
|
|
std::filesystem::create_directory(extractPaths[table.inode]);
|
|
}
|
|
ndinode_counter++;
|
|
if ((ndinode_counter + 1) == ndinode) // 1 for the image itself (root).
|
|
end_reached = true;
|
|
}
|
|
}
|
|
if (end_reached) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
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;
|
|
|
|
Common::FS::IOFile inflated;
|
|
inflated.Open(extractPaths[inode_number].string(), 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.Read(pfsc);
|
|
|
|
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();
|
|
}
|
|
}
|