Date: May 23, 2025
Analysis Author: SeregonWar
Primary Source: C++ source code fragments (from the shadPS4 Emulator project) shadPS4 and associated header files (pkg.h, pfs.h, crypto.h, keys.h, etc.).
This document outlines an in-depth technical analysis of the parsing and decryption mechanisms employed for the PKG (Package) and PFS (PlayStation File System) file formats specific to the PlayStation 4 console. Based on an examination of the provided source code, this paper details the fundamental data structures, the sequence of cryptographic operations, and the logical workflow required for the extraction and access of protected content. It highlights a multi-layered security system leveraging standard algorithms such as RSA-2048, AES-128-CBC, and AES-128-XTS, along with platform-specific key derivation processes. Python code examples, derived from the translation of the C++ logic, will be used to illustrate crucial steps.
PKG files serve as the primary distribution container for software on the PlayStation 4 platform, encapsulating games, applications, patches, and downloadable content. Within these containers, application data is frequently organized into a PFS filesystem image, which is itself subject to encryption. Accessing this content necessitates a detailed understanding of the PKG file format, the identification and decryption of a hierarchy of cryptographic keys, and the subsequent interpretation and decryption of the PFS image. This paper aims to dissect this process as it emerges from the analysis of the shadPS4 Emulator project's source code.
The PKG file is a structured container format, its layout primarily defined by its header and a table of entries describing internal metadata.
The PKG file header, conforming to the definition in pkg.h, typically occupies the first 4096 bytes (0x1000) of the file. It contains metadata crucial for interpreting the rest of the package. Among the most significant fields, expressed in Big Endian format, are:
magic(u32_be): A fixed identifier,0x7F434E54(corresponding to the ASCII string ".CNT"), which validates the file format.pkg_table_entry_offset(u32_be) andpkg_table_entry_count(u32_be): Indicate the offset and number of entries in the PKG's file table, respectively. This table primarily lists metadata files (e.g.,param.sfo, license files).pkg_content_id(u8[0x24]): A 36-byte unique content identifier. From this string, typically at file offset0x47(skipping the first 7 characters of thecontent_idread from the header, which itself starts at file offset0x30), thepkgTitleID(9 characters, e.g., "CUSAXXXXX") is extracted.pfs_image_offset(u64_be) andpfs_image_size(u64_be): Locate the encrypted PFS filesystem image within the PKG file.pfs_cache_size(u32_be): A parameter used to size intermediate buffers during PFS processing, particularly for its initial decryption and the localization of the PFSC substructure.- Various SHA256 Digests: The header contains SHA256 hashes of different PKG sections (e.g.,
digest_table_digest,digest_body_digest,pfs_image_digest), used for integrity verification. pkg_content_flags(u32_be): Flags providing contextual information about the content type (e.g.,PKGContentFlag.FIRST_PATCH,PKGContentFlag.REMASTER).
# Example of PKGHeader class definition in Python (simplified)
@dataclass
class PKGHeader:
# ... (full field definition as per previous Python code) ...
_FORMAT_FULL = ">IIIIHHI..." # Big Endian notation for struct.unpack
magic: int
pkg_table_entry_offset: int
pkg_table_entry_count: int
pkg_content_id: bytes # 36 bytes
pfs_image_offset: int
pfs_image_size: int
pfs_cache_size: int
# ... other fields ...
@classmethod
def from_bytes(cls, data: bytes):
# ... (unpacking logic) ...
return cls(*values)Located at the offset specified by pkg_header.pkg_table_entry_offset, this table is an array of PKGEntry structures. Each PKGEntry (32 bytes, Big Endian, defined in pkg.h) describes a metadata file, generally destined for the virtual sce_sys/ directory upon extraction.
id(u32_be): A numerical identifier for the file type. A mapping, such as the one provided inpkg_type.cpp, translates these IDs into standard filenames (e.g.,0x1000-> "param.sfo").filename_offset(u32_be): If applicable, an offset into a filename table (often an entry withid = 0x0200, "entry_names").offset(u32_be): The absolute offset, from the beginning of the PKG file, where the data for this entry is located.size(u32_be): The size in bytes of the entry's data.flags1(u32_be),flags2(u32_be): Contain flags that, among other things, can indicate if the entry is encrypted and which key index to use for decryption.
# Example of PKGEntry class definition in Python
@dataclass
class PKGEntry:
_FORMAT = ">IIIIIIQ" # id, filename_offset, flags1, flags2, offset, size, padding
_SIZE = struct.calcsize(_FORMAT)
id: int
filename_offset: int
flags1: int
flags2: int
offset: int
size: int
padding: int # u64_be
name: str = "" # Added for convenience
# ... (from_bytes method) ...Several entries within the PKG, particularly those related to NPDRM (e.g., nptitle.dat, npbind.dat, with IDs like 0x0400, 0x0401, 0x0402, 0x0403), are encrypted. Their decryption follows these key steps, as implemented in PKG::Extract and Crypto (from crypto.cpp and keys.h):
- Accessing "entry_keys": The PKG entry with
id = 0x0010(identified as "entry_keys") is read. - Reading Encrypted Keys: From this entry's offset, various digests and an array of seven keys (
key1inpkg.cpp), each 256 bytes, are read. - RSA Decryption of
key1[3]: The fourth key in this array (key1[3]) is decrypted using the RSA-2048 algorithm with the PKCS#1 v1.5 padding scheme. The RSA private key employed for this operation is specified asPkgDerivedKey3Keysetin thekeys.hfile.# Conceptual example of RSA decryption in Python # self.dk3_ is a bytearray(32) # key1_list[3] are the 256 encrypted bytes # self.crypto._key_pkg_derived_key3 is the PyCryptodome RSA key object # In RealCrypto.RSA2048Decrypt: # key_to_use = self._key_pkg_derived_key3 if is_dk3 else self._key_fake # cipher_rsa = Cipher_PKCS1_v1_5.new(key_to_use) # decrypted_data = cipher_rsa.decrypt(ciphertext, None) # output_key_buffer[:bytes_to_copy] = decrypted_data[:bytes_to_copy]
- Resulting
dk3_: The output of this RSA decryption is a 32-byte key, denoteddk3_.
- Accessing "image_key": The PKG entry with
id = 0x0020("image_key") is read. - Reading Encrypted Data: From this entry's offset, 256 bytes of encrypted data, termed
imgkeydata, are read. - Generating
ivKey:- A 64-byte buffer is prepared. The first 32 bytes are a copy of the raw bytes of the
PKGEntry(ID0x0020) read from the table. The subsequent 32 bytes consist of thedk3_key (obtained in the previous step). - A SHA256 hash is computed over this 64-byte buffer. The resulting 32-byte digest is the
ivKey.
# In RealCrypto.ivKeyHASH256: # h = SHA256.new() # h.update(cipher_input_64_bytes) # digest = h.digest() # 32 bytes # ivkey_result_buffer[:] = digest
- A 64-byte buffer is prepared. The first 32 bytes are a copy of the raw bytes of the
- AES Decryption of
imgkeydata:- The
ivKey(32 bytes) is split: the first 16 bytes serve as the AES Initialization Vector (IV), while the next 16 bytes constitute the AES-128 key. - The 256 bytes of
imgkeydataare decrypted using AES-128 in CBC (Cipher Block Chaining) mode with the newly derived key and IV. The result of this operation is theimgKey(256 bytes).
# In RealCrypto.aesCbcCfb128Decrypt: # key_aes = ivkey[16:32] # iv_aes = ivkey[0:16] # cipher_aes = AES.new(key_aes, AES.MODE_CBC, iv_aes) # decrypted_data = cipher_aes.decrypt(ciphertext_256_bytes) # decrypted_buffer[:] = decrypted_data # self.imgKey
- The
- RSA Decryption of
imgKey:- The
imgKey(256 bytes) is further processed. It is decrypted using RSA-2048 (PKCS#1 v1.5), but this time with a different RSA private key, namedFakeKeysetinkeys.h. - The output of this second RSA decryption is the
ekpfsKey(Entitlement Key for PFS), a 32-byte key crucial for the subsequent PFS filesystem decryption.
- The
For NPDRM type entries (e.g., nptitle.dat, npbind.dat):
- Specific
ivKeyGeneration: Similar to step 3.2.3, a 64-byte buffer is constructed by concatenating the bytes of the current NPDRMPKGEntrywithdk3_. A SHA256 hash of this buffer produces anivKeyspecific to this entry. - AES Decryption: The encrypted data of the NPDRM entry is decrypted using AES-128-CBC, with the key and IV derived from this specific
ivKey. The decrypted result overwrites the original entry's data in the extraction path.
The PFS image, located via pkg_header.pfs_image_offset and pkg_header.pfs_image_size, contains the actual filesystem of the game/application.
- Reading the PFS Seed: A 16-byte cryptographic
seedis read from a fixed offset (0x370) relative to the start of the PFS image (i.e.,pkg_header.pfs_image_offset + 0x370). - Key Generation via HMAC-SHA256: The
PfsGenCryptoKeyfunction (inCrypto) uses theekpfsKey(32 bytes, obtained in step 3.2.5) and the PFSseed(16 bytes) to generate two 16-byte keys:dataKeyandtweakKey.- A 20-byte payload is constructed by concatenating a fixed index (
1, as au32) with theseed. - An HMAC-SHA256 of this payload is computed, using
ekpfsKeyas the HMAC key. - The resulting HMAC digest (32 bytes) is split: the first 16 bytes become the
tweakKey, and the subsequent 16 bytes become thedataKey.
# In RealCrypto.PfsGenCryptoKey: # hmac_sha256 = HMAC.new(ekpfs_32_bytes, digestmod=SHA256) # index_bytes = struct.pack("<I", 1) # Little Endian u32 for index = 1 # d_payload = index_bytes + seed_16_bytes # Total 20 bytes # hmac_sha256.update(d_payload) # data_tweak_key_digest = hmac_sha256.digest() # 32 bytes # tweakKey_buffer[:] = data_tweak_key_digest[0:16] # dataKey_buffer[:] = data_tweak_key_digest[16:32]
- A 20-byte payload is constructed by concatenating a fixed index (
- Partial Read and Decryption: A portion of the encrypted PFS image is read from the PKG file. The size of this portion, according to the analyzed C++ code, is
pkg_header.pfs_cache_size * 2. Ifpfs_cache_sizeis zero, this phase and subsequent PFS parsing are typically skipped. - AES-XTS Decryption: This portion is decrypted using the AES-128-XTS algorithm with the
dataKeyandtweakKey. XTS decryption operates on 0x1000-byte (4 KiB) blocks, and the sector number (starting from 0 for this initial decryption) is used to calculate the initial tweak for each XTS block. PFSC_MAGICSearch: Within the (partially) decrypted PFS image buffer, the magic numberPFSC_MAGIC(0x43534650, ASCII "PFSC", Little Endian) is searched to determine thepfsc_offset_in_pfs_image. This offset is relative to the start of the decrypted PFS image buffer and indicates the beginning of the PFSC data structure. The search typically occurs at 0x10000-byte intervals, starting from offset 0x20000.
Starting from pfsc_offset_in_pfs_image (within the decrypted PFS buffer), the PFSCHdrPFS structure (defined as PFSCHdr in pfs.h and used in pkg.cpp) is read and interpreted:
magic(s32): Should matchPFSC_MAGIC.data_length(s64): Total length of the data managed by this PFSC structure.block_sz2(s64): Size of the logical, decompressed data blocks (typically 0x10000 bytes or 64 KiB).block_offsets(s64): Offset (relative to the start of the PFSC structure) of the table (sectorMap) that maps logical blocks to their (compressed or uncompressed) data within the PFSC data area.
The number of logical data blocks is calculated: num_data_blocks_in_pfsc = pfs_chdr_obj.data_length // pfs_chdr_obj.block_sz2.
The sectorMap is then read: it is an array of num_data_blocks_in_pfsc + 1 offsets (u64). Each sectorMap[i] indicates the start offset of logical block i within the PFSC data area, and sectorMap[i+1] - sectorMap[i] gives its (potentially compressed) size.
An iteration is performed from 0 to num_data_blocks_in_pfsc - 1. For each logical block:
- The offset (
sector_offset_in_pfsc_data) and size (sector_data_size) of the data block are retrieved from thesectorMap. This data is relative to the start of the PFSC data area (i.e., afterpfsc_offset_in_pfs_imagein the decrypted PFS buffer, and after thePFSCHdrPFSheader ifblock_offsetspoints after it, within thepfsc_content_actual_bytesbuffer). - The data block is extracted.
- If its size (
sector_data_size) is less thanpfs_chdr_obj.block_sz2(e.g., < 0x10000), the data block is considered compressed and is decompressed using zlib (functionDecompressPFSC) into a temporary buffer of sizeblock_sz2. Otherwise, it is copied directly.
The first decompressed logical PFSC block (i_block_pfsc = 0) acts as the PFS "superblock" and contains, among other information, the total number of inodes (ndinode_total_count) at offset 0x30. Subsequent logical blocks (from 1 up to a calculated occupied_inode_blocks based on ndinode_total_count and sizeof(Inode)) contain the inode table.
Each Inode structure (defined in pfs.h, 0xA8 bytes in size) provides metadata for a file or directory:
Mode(u16): File type (directory, regular file, etc., as specified by bits inInodeModePfs) and permissions.Size(s64): Actual size of the file in bytes.Blocks(u32): Number of logical data blocks (of sizeblock_sz2) occupied by the file.loc(u32): Index of the file's first data block in thesectorMap. Thislocis an index relative to the start of the data area managed by PFSC, not an absolute index intosectorMap.
Logical PFSC blocks following the inode blocks contain directory entries (Dirent, from pfs.h). The presence of "." and ".." entries is a common indicator of a dirent block.
Each Dirent provides:
ino(s32): The inode number to which this entry refers.type(s32): Entry type, with values mapping toPFSFileType(e.g.,PFSFileType.PFS_FILE = 2,PFSFileType.PFS_DIR = 3).namelen(s32): Length of the file/directory name.entsize(s32): Total size of thisDirentstructure, used to advance to the next entry.name(char[512]): The null-terminated file/directory name (actual length given bynamelen).
During this parsing stage, an fs_table (list of FSTableEntry) and an extract_paths map (from inode number to pathlib.Path) are constructed to rebuild the filesystem hierarchy. The management of current_dir_pfs (the current PFS directory during parsing) and the correct determination of the PFS root path (influenced by the uroot_reached logic or the first "." entry) are crucial. Directories are created on disk as they are identified.
Once the PFS structure has been parsed (inodes and dirents are available), individual files are extracted:
- For each
FSTableEntryrepresenting a file:- The corresponding
Inodeobject is retrieved (the indexing ofiNodeBufusingfs_entry.inodeis a critical point requiring correct mapping or assumptions about inode compactness). - From the inode,
loc(index of the file's first block insectorMap) andBlocks(number of blocks composing the file) are obtained.
- The corresponding
- An iteration is performed for
Blockstimes, processing one logical block of the file at a time:- Locating Encrypted Data: The current block's index in
sectorMapisloc + j_block_in_file. From this,sector_offset_in_pfsc_img(offset of the data block within the PFSC data area) andsector_data_actual_size(size of this block, compressed or not) are obtained. - Calculating PKG Offset: The absolute offset (
absolute_sector_data_offset_in_pkg) of this data block within the original PKG file is determined. - Identifying XTS Block: The XTS block number (0x1000 bytes,
xts_block_num_in_pfs_image) containing the start of this sector's data, and the offset (offset_of_sector_in_its_xts_block) of the sector's data within that XTS block, are calculated. - Reading and XTS Decryption: A chunk of data (0x11000 bytes in the C++ code,
read_chunk_from_pkg) is read from the PKG file starting at the identified XTS block's beginning. This read chunk is decrypted usingdecryptPFS(AES-128-XTS) withdataKey,tweakKey, andxts_block_num_in_pfs_imageas the initial XTS sector number. The result is stored indecrypted_chunk_from_pkg. - Extracting and Decompressing Sector Data: The actual sector data (of size
sector_data_actual_size) is extracted fromdecrypted_chunk_from_pkgusingoffset_of_sector_in_its_xts_block. If compressed, it is decompressed using zlib. - Writing File: The decompressed block (0x10000 bytes or
block_sz2) is written to the output file. For the final block of the file, only the amount of data needed to reach the total file size specified in the inode is written.
- Locating Encrypted Data: The current block's index in
# Concept of PFS file extraction (simplified)
# In PKG.extract_pfs_files:
# for fs_entry in self.fs_table:
# if fs_entry.type == PFSFileType.PFS_FILE:
# inode_obj = self.iNodeBuf[fs_entry.inode - 1] # Assuming 1-based compact
# output_path = self.extract_paths[fs_entry.inode]
#
# with open(output_path, "wb") as out_f:
# for j_block in range(inode_obj.Blocks):
# # ... locate sector_offset_in_pfsc_img, sector_data_actual_size from sectorMap ...
# # ... calculate read_start_pos_in_pkg, xts_block_num_in_pfs_image, offset_of_sector_in_its_xts_block ...
#
# pkg_file.seek(read_start_pos_in_pkg)
# read_chunk = pkg_file.read(0x11000)
# decrypted_chunk = bytearray(len(read_chunk)) # Ensure multiple of 0x1000 or handle in decryptPFS
# C++ uses fixed 0x11000 buffers for decryptPFS.
# Make read_chunk & decrypted_chunk sizes
# a multiple of 0x1000, padding if necessary.
# Pad read_chunk if it's not a multiple of 0x1000 for decryptPFS
# effective_read_chunk_len = (len(read_chunk) + 0xFFF) & ~0xFFF if len(read_chunk) % 0x1000 != 0 else len(read_chunk)
# padded_read_chunk = bytearray(effective_read_chunk_len)
# padded_read_chunk[:len(read_chunk)] = read_chunk
# decrypted_chunk = bytearray(effective_read_chunk_len)
# self.crypto.decryptPFS(self.dataKey, self.tweakKey,
# padded_read_chunk, # Or unpadded chunk if decryptPFS handles it
# decrypted_chunk,
# xts_block_num_in_pfs_image)
#
# sector_data = decrypted_chunk[offset_of_sector_in_its_xts_block :
# offset_of_sector_in_its_xts_block + sector_data_actual_size]
#
# if sector_data_actual_size == 0x10000: # block_sz2
# decompressed_data = sector_data
# else:
# decompressed_data = decompress_pfsc(sector_data, 0x10000) # block_sz2
#
# # ... write decompressed_data to out_f, handling the last block ...The security system employs a combination of standard algorithms:
- RSA-2048 (PKCS#1 v1.5 Padding): Used for the asymmetric decryption of critical intermediate keys. The private keys (
PkgDerivedKey3Keysetfordk3_,FakeKeysetforekpfsKey) are defined via their components (n, e, d, p, q, coefficient). - SHA256: Employed as a hash function for deriving
ivKeyand for integrity digests in the PKG header. - AES-128-CBC: Used for symmetric decryption of
imgKey(usingivKey) and NPDRM entries (using an entry-specificivKey). The key and IV (both 16 bytes) are typically derived from a 32-byte SHA256 hash. - HMAC-SHA256: Utilized in the
PfsGenCryptoKeyfunction to derive PFSdataKeyandtweakKeyfromekpfsKeyand aseed. - AES-128-XTS (Advanced Encryption Standard with Xor-Encrypt-Xor based Tweaked CodeBook Mode): The primary algorithm for sector-level decryption of the PFS image and files within it. It requires a
dataKeyand atweakKey. The "tweak" (a value that modifies the cipher for each block, based on the sector number) is encrypted withtweakKey(AES-ECB). This encrypted tweak is then iteratively updated for each 16-byte AES block within the XTS sector via a polynomial multiplication in the Galois Field GF(2^128) (polynomialx^128 + x^7 + x^2 + x + 1, or0x87if the highest bit is carried).
The implementation of what is described can be found in ShadPKG, This technical paper was written to explain the technical vulnerabilities of pkg files and to give more technical information to those who have asked me in the last year if the files can be decrypted and modified, I am still implementing this system in PkgToolBox, once implemented anyone will be able to decrypt most of the and games fpkg currently available in the ps4 landscape, ShadPKG should be considered as an alternative to the old extractor included in ShadPS4.