PKG files: Difference between revisions

From PS4 Developer wiki
Jump to navigation Jump to search
m (CelesteBlue moved page Package Files to PKG files: keep compatibility with PS3 dev wiki)
 
(5 intermediate revisions by the same user not shown)
Line 1: Line 1:
See also [https://www.psdevwiki.com/ps5/PKG_files PS5 PKG files], [https://www.psdevwiki.com/ps3/PKG_files PSP, PS3 and PS Vita PKG files], [https://wiki.henkaku.xyz/vita/Packages PS Vita PKG files on henkaku wiki].
{{wikify}}
{{wikify}}
For more information, see {{talk}}


== Package Structure ==
== Structure ==
 
=== File Header ===
=== File Header ===
While most of the PS4 is little endian, the package file header still uses big endianness as the headers are based on their PS3 predecessors.
 
While most of the PS4 files are in little-endian, the package file header still uses big endianness as the headers are based on their PSP, PS3 and PS Vita predecessors.


   typedef struct {
   typedef struct {
Line 59: Line 62:


=== Files ===
=== Files ===
The file table is a list of file entries:
The file table is a list of file entries:


Line 85: Line 89:


There are also files without plaintext filenames. These are identified by their ID in the file entry table.
There are also files without plaintext filenames. These are identified by their ID in the file entry table.
=== Table Entry Hashes (SHA-256) ===
The first entry in the index table points to a block of hashes.
Here is an example using an Amazon Instant Video package file (UP2064-CUSA00130_00-AIV00000000000US.pkg).
0x2A80  <span style="background:#ffff66;">00 00 00 01 00 00 00 00 40 00 00 00 00 00 00 00</span>  ........@....... <span style="background:#ffff66;">First Entry in Index Table</span>.
0x2A90  <span style="background:#ffff66;">00 00 2C A0 00 00 02 20 00 00 00 00 00 00 00 00</span>  .., ... ........
0x2AA0  00 00 00 10 00 00 00 00 60 00 00 00 00 00 00 00  ........`.......
0x2AB0  00 00 20 00 00 00 08 00 00 00 00 00 00 00 00 00  .. .............
0x2AC0  00 00 00 20 00 00 00 00 E0 00 00 00 00 00 30 00  ... ....à.....0.
0x2AD0  00 00 28 00 00 00 01 00 00 00 00 00 00 00 00 00  ..(.............
...
Offset: 0x00002CA0 Length: 0x00000220
0x2CA0  <span style="background:#ff6666;">00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span>  ................
0x2CB0  <span style="background:#ff6666;">00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span>  ................
0x2CC0  <span style="background:#6666ff;">B4 34 DD 9C B3 C7 91 96 EC F7 D1 F6 8F 8A 2D 18</span>  ´4Ýœ³Ç‘–ì÷Ñö.Š-.
0x2CD0  <span style="background:#6666ff;">8D 07 8F 2D 33 E9 09 6B 1D 22 B5 4E 7A F2 6D 6A</span>  ...-3é.k."µNzòmj
0x2CE0  F0 F5 9E 95 F4 74 13 FE 9F 35 DB 44 57 76 DE 49  ðõž•ôt.þŸ5ÛDWvÞI
0x2CF0  90 B1 68 20 97 8F 70 79 9D 62 95 CD 97 67 5D B0  .±h —.py.b•Í—g]°
0x2D00  1D 6E EE E7 67 3D 7E B4 2F 78 F1 26 2C EE EC 7A  .nîçg=~´/xñ&,îìz
0x2D10  10 40 90 BA FD 0F F9 AF BE ED F1 BC DE 84 30 55  .@.ºý.ù¯¾íñ¼Þ„0U
0x2D20  57 65 D8 7B DD 85 5E D0 73 1B 78 4D A6 EE 00 CF  WeØ{Ý…^Ðs.xM¦î.Ï
0x2D30  A1 0C 3F C4 03 E0 19 5A 0B 36 E1 64 33 7A D1 C6  ¡.?Ä.à.Z.6ád3zÑÆ
0x2D40  1A 4E E9 FA 4F DD AD F4 63 FF 73 8F 9F 24 6F 0E  .NéúOÝ.ôcÿs.Ÿ$o.
0x2D50  DF 22 EB 3D 43 F1 A3 7D C6 D0 BD 97 49 03 EC C2  ß"ë=Cñ£}Æн—I.ìÂ
0x2D60  DB 04 17 61 81 6A 14 9B 0F A3 B6 D7 6D AA 48 5A  Û..a.j.›.£¶×mªHZ
0x2D70  1F 3E 95 6B 63 BD AE B2 A2 E0 AE 44 8D D0 05 EA  .>•kc½®²¢à®D.Ð.ê
0x2D80  93 BB 8F 3E 60 72 F8 0C BD BA DB 0E 4D 01 AA AA  “».>`rø.½ºÛ.M.ªª
0x2D90  65 C0 97 E3 89 18 BB A2 17 6E 49 EE 3A 36 CA 91  eÀ—ã‰.»¢.nIî:6Ê‘
0x2DA0  5B EE 4F 1B 1B 7F 52 17 04 99 DD 8C 19 3A 31 BB  [îO...R..™ÝŒ.:1»
0x2DB0  79 9D F4 70 38 D5 F6 DD FF AA 76 5E 10 F2 CC 8F  y.ôp8ÕöÝÿªv^.òÌ.
0x2DC0  0A D9 DC 1C BA 98 EB B3 4A 74 02 E9 F1 0A 0A 90  .ÙÜ.º˜ë³Jt.éñ...
0x2DD0  69 AC D0 29 9F 93 DF 45 80 35 6E FB AF D6 B1 A5  i¬Ð)Ÿ“ßE€5nû¯Ö±¥
0x2DE0  C6 13 74 C9 51 F7 BA A5 CF 0D DE 13 E3 BB 02 0D  Æ.tÉQ÷º¥Ï.Þ.ã»..
0x2DF0  06 6E 44 64 FF 2A CA 37 B0 20 4C 03 44 CA 5E C9  .nDdÿ*Ê7° L.DÊ^É
0x2E00  B4 D0 03 6B 54 4A 66 ED C7 32 CB D2 E0 34 CF 5F  ´Ð.kTJfíÇ2ËÒà4Ï_
0x2E10  5B 1F 46 B5 81 72 09 D3 33 B3 3E 5E FC 01 6B 11  [.Fµ.r.Ó3³>^ü.k.
0x2E20  9A DF 99 EE A2 2B 5E E2 72 B9 32 02 6B B7 E8 D1  šß™î¢+^âr¹2.k·èÑ
0x2E30  5A 9D B8 A9 97 17 47 4F 11 75 FA 41 6E 79 7A 1B  Z.¸©—.GO.uúAnyz.
0x2E40  94 A5 62 30 EA E0 99 89 3D BB 34 5D 0B F5 E3 17  ”¥b0êà™‰=»4].õã.
0x2E50  BE 2C EE 7B D5 EA 8F 05 FB 0E 07 A2 40 FF 7A 59  ¾,î{Õê..û..¢@ÿzY
0x2E60  6B FE F8 0B 1E 61 85 83 18 9A 53 3A F0 91 46 B7  kþø..a…ƒ.šS:ð‘F·
0x2E70  86 83 38 B8 C1 3E E8 74 C5 4F 4E E6 B6 28 7F 52  †ƒ8¸Á>ètÅONæ¶(.R
0x2E80  55 FE CE EA F4 9E 98 2A BC A5 C4 21 D6 44 17 C9  UþÎêôž˜*¼¥Ä!ÖD.É
0x2E90  76 D9 1C 02 FD 75 BB 37 C3 96 1A C3 1C 3E 5B 5F  vÙ..ýu»7Ö.Ã.>[_
0x2EA0  2B 37 CA 02 AB E2 B7 C6 FB 74 23 B9 A6 C2 C6 0B  +7Ê.«â·Æût#¹¦ÂÆ.
0x2EB0  70 6F 79 CB AE 80 D9 B9 62 1A D6 69 F6 47 FB F2  poyË®€Ù¹b.ÖiöGûò
<span style="background:#ff6666;">First hash</span> is blank, can't hash the table of hashes.<br />
The remaining hashes are for each of the remaining Index Table Entries.<br />
i.e the second entry in the Index Table should have a SHA-256 matching the following
<span style="background:#6666ff;">0xB434DD9CB3C79196ECF7D1F68F8A2D188D078F2D33E9096B1D22B54E7AF26D6A</span>


== PFS ==
== PFS ==
Main article: [[PFS]] ''(only explains un-encrypted, un-signed PFS)''


The main portion of a PKG file is its signed and encrypted PFS image. It is encrypted with XTS-AES with a block size of 0x1000. The key is derived from the HMAC-SHA256 of the concatenation of "0x01 0x00 0x00 0x00" in and the EKPFS. The HMAC key is the "crypt seed" which is at offset 0x370 in the PFS itself. The tweak key is the first 16 bytes of the HMAC result, while the data key is the second 16 bytes.
* Note that the main article [[PFS]] ''only explains un-encrypted, un-signed PFS''.


The PFS image in a PKG is different from the decrypted image exposed by the system in <code>/mnt/sandbox/pfsmnt/CUSA00001-app0-nest/pfs_image.dat</code>. This image uses <code>dinode_s32</code>, the signed inode variant, which includes a 32-byte signature for every direct and indirect block. Additionally, the indirect block blocks also have a 32-byte signature for each block within.
The main portion of a PKG file is its signed and encrypted [[PFS]] image. It is encrypted with XTS-AES with a block size of 0x1000. The key is derived from the HMAC-SHA256 of the concatenation of "0x01 0x00 0x00 0x00" in and the EKPFS. The HMAC key is the "crypt seed" which is at offset 0x370 in the PFS itself. The tweak key is the first 16 bytes of the HMAC result, while the data key is the second 16 bytes.


Inside the PFS image in a PKG is a single file: <code>pfs_image.dat</code> which is a special type of compressed PFS image with a <code>PFSC</code> header. Inside <code>pfs_image.dat</code> is where the actual game/theme/DLC data lives.
The PFS image in a PKG file is different from the decrypted image exposed by the system in <code>/mnt/sandbox/pfsmnt/CUSA00001-app0-nest/pfs_image.dat</code>. This nested image uses <code>dinode_s32</code>, the signed inode variant, which includes a 32-byte signature for every direct and indirect block. Additionally, the indirect blocks also have a 32-byte signature for each block within.
 
Inside the PFS image in a PKG is a single file: <code>pfs_image.dat</code> which is a special type of compressed PFS image with a <code>PFSC</code> header. Inside <code>pfs_image.dat</code> is where the actual game/theme/DLC data live.


== Delivery ==
== Delivery ==
=== Title XML ===
=== Title XML ===
The PS4 fetches information about pkg files (including where to download them) from an XML file.  
 
This XML file contains information such as if the latest patch is mandatory, the latest package version, the manifest, as well as param.sfo information. Below is an example of a typical title XML file:
The PS4 fetches information about package files (including where to download them) from an XML file. This XML file contains information such as if the latest patch is mandatory, the latest package version, the manifest, as well as information in a param.sfo file. Below is an example of a typical title XML file:


  <titlepatch titleid="CUSAXXXXX">
  <titlepatch titleid="CUSAXXXXX">
Line 119: Line 182:


=== Manifest ===
=== Manifest ===
It should be noted that PS4 package files have a maximum size of 4GB (or 4096MB), therefore large (most) games are split into chunks or pieces. This is kept track of in the manifest file, which contains json fields which document things such as the size of the final package after the chunks are spliced together, the digest of the final package, the number of chunks, as well as information for each chunk such as the pkg url, offset for splicing, size of the file, and the sha1 hash value of the individual chunk.
 
It should be noted that PS4 package files have a maximum size of 4GB (or 4096MB), therefore large (most) games are split into chunks or pieces. This is kept track of in the manifest file, which contains JSON fields which document things such as the size of the final package after the chunks are spliced together, the digest of the final package, the number of chunks, as well as information for each chunk such as the package file URL, offset for splicing, size of the file, and the sha1 hash value of the individual chunk.


  originalFileSize: [size]
  originalFileSize: [size]
Line 131: Line 195:
   fileSize: [size, often 4294967296 until last chunk]
   fileSize: [size, often 4294967296 until last chunk]
   hashValue: "[sha1 hash of chunk]"
   hashValue: "[sha1 hash of chunk]"
== Tools ==
=== UnPKG tool by flatz ===
UnPKG is an open source python2 library made by flatz to extract a PS4 ?release and/or debug? NPDRM PKG file. It maybe only extracts only partially the PKG or maybe requires a per-content key.
<source lang="python">
# UnPKG rev 0x00000008 (public edition), (c) flatz
import sys, os, hashlib, hmac, struct, math, traceback
from cStringIO import StringIO
# parse arguments
if len(sys.argv) < 3:
script_file_name = os.path.split(sys.argv[0])[1]
print 'usage: {0} <pkg file> <output dir>'.format(script_file_name)
sys.exit()
pkg_file_path = sys.argv[1]
if not os.path.isfile(pkg_file_path):
print 'error: invalid file specified'
sys.exit()
output_dir = sys.argv[2]
if os.path.exists(output_dir) and not os.path.isdir(output_dir):
print 'error: invalid directory specified'
sys.exit()
elif not os.path.exists(output_dir):
os.makedirs(output_dir)
# cryptography functions
def sha256(data):
return hashlib.sha256(data).digest()
# utility functions
uint64_fmt, uint32_fmt, uint16_fmt, uint8_fmt = '>Q', '>I', '>H', '>B'
int64_fmt, int32_fmt, int16_fmt, int8_fmt = '>q', '>i', '>h', '>b'
def read_string(f, length):
return f.read(length)
def read_cstring(f):
s = ''
while True:
c = f.read(1)
if not c:
return False
if ord(c) == 0:
break
s += c
return s
def read_uint8_le(f):
return struct.unpack('<B', f.read(struct.calcsize('<B')))[0]
def read_uint8_be(f):
return struct.unpack('>B', f.read(struct.calcsize('>B')))[0]
def read_uint16_le(f):
return struct.unpack('<H', f.read(struct.calcsize('<H')))[0]
def read_uint16_be(f):
return struct.unpack('>H', f.read(struct.calcsize('>H')))[0]
def read_uint32_le(f):
return struct.unpack('<I', f.read(struct.calcsize('<I')))[0]
def read_uint32_be(f):
return struct.unpack('>I', f.read(struct.calcsize('>I')))[0]
def read_uint64_le(f):
return struct.unpack('<Q', f.read(struct.calcsize('<Q')))[0]
def read_uint64_be(f):
return struct.unpack('>Q', f.read(struct.calcsize('>Q')))[0]
def read_int8_le(f):
return struct.unpack('<b', f.read(struct.calcsize('<b')))[0]
def read_int8_be(f):
return struct.unpack('>b', f.read(struct.calcsize('>b')))[0]
def read_int16_le(f):
return struct.unpack('<h', f.read(struct.calcsize('<h')))[0]
def read_int16_be(f):
return struct.unpack('>h', f.read(struct.calcsize('>h')))[0]
def read_int32_le(f):
return struct.unpack('<i', f.read(struct.calcsize('<i')))[0]
def read_int32_be(f):
return struct.unpack('>i', f.read(struct.calcsize('>i')))[0]
def read_int64_le(f):
return struct.unpack('<q', f.read(struct.calcsize('<q')))[0]
def read_int64_be(f):
return struct.unpack('>q', f.read(struct.calcsize('>q')))[0]
# main code
PKG_MAGIC = '\x7FCNT'
CONTENT_ID_SIZE = 0x24
SHA256_HASH_SIZE = 0x20
META_ENTRY_SIZE = 0x20
FILE_TYPE_FLAGS_RETAIL = 1 << 31
ENTRY_TYPE_DIGEST_TABLE = 0x0001
ENTRY_TYPE_0x800        = 0x0010
ENTRY_TYPE_0x200        = 0x0020
ENTRY_TYPE_0x180        = 0x0080
ENTRY_TYPE_META_TABLE  = 0x0100
ENTRY_TYPE_NAME_TABLE  = 0x0200
ENTRY_TYPE_LICENSE = 0x04
ENTRY_TYPE_FILE1  = 0x10
ENTRY_TYPE_FILE2  = 0x12
ENTRY_TABLE_MAP = {
ENTRY_TYPE_DIGEST_TABLE: '.digests',
ENTRY_TYPE_0x800: '.entry_0x800',
ENTRY_TYPE_0x200: '.entry_0x200',
ENTRY_TYPE_0x180: '.entry_0x180',
ENTRY_TYPE_META_TABLE: '.meta',
ENTRY_TYPE_NAME_TABLE: '.names',
0x0400: 'license.dat',
0x0401: 'license.info',
0x1000: 'param.sfo',
0x1001: 'playgo-chunk.dat',
0x1002: 'playgo-chunk.sha',
0x1003: 'playgo-manifest.xml',
0x1004: 'pronunciation.xml',
0x1005: 'pronunciation.sig',
0x1006: 'pic1.png',
0x1008: 'app/playgo-chunk.dat',
0x1200: 'icon0.png',
0x1220: 'pic0.png',
0x1240: 'snd0.at9',
0x1260: 'changeinfo/changeinfo.xml',
}
class MyError(Exception):
def __init__(self, message):
self.message = message
def __str__(self):
return repr(self.message)
class FileTableEntry:
entry_fmt = '>IIIIII8x'
def __init__(self):
pass
def read(self, f):
self.type, self.unk1, self.flags1, self.flags2, self.offset, self.size = struct.unpack(self.entry_fmt, f.read(struct.calcsize(self.entry_fmt)))
self.key_index = (self.flags2 & 0xF000) >> 12
self.name = None
try:
with open(pkg_file_path, 'rb') as pkg_file:
magic = read_string(pkg_file, 4)
if magic != PKG_MAGIC:
raise MyError('invalid file magic')
type = read_uint32_be(pkg_file)
is_retail = (type & FILE_TYPE_FLAGS_RETAIL) != 0
pkg_file.seek(0x10) # FIXME: or maybe uint16 at 0x16???
num_table_entries = read_uint32_be(pkg_file)
pkg_file.seek(0x14)
num_system_entries = read_uint16_be(pkg_file)
pkg_file.seek(0x18)
file_table_offset = read_uint32_be(pkg_file)
pkg_file.seek(0x1C)
main_entries_data_size = read_uint32_be(pkg_file)
pkg_file.seek(0x24)
body_offset = read_uint32_be(pkg_file)
pkg_file.seek(0x2C)
body_size = read_uint32_be(pkg_file)
pkg_file.seek(0x414)
content_offset = read_uint32_be(pkg_file)
pkg_file.seek(0x41C)
content_size = read_uint32_be(pkg_file)
pkg_file.seek(0x40)
content_id = read_cstring(pkg_file)
if len(content_id) != CONTENT_ID_SIZE:
raise MyError('invalid content id')
pkg_file.seek(0x100)
main_entries1_digest = pkg_file.read(SHA256_HASH_SIZE)
main_entries2_digest = pkg_file.read(SHA256_HASH_SIZE)
digest_table_digest = pkg_file.read(SHA256_HASH_SIZE)
body_digest = pkg_file.read(SHA256_HASH_SIZE)
pkg_file.seek(0x440)
content_digest = pkg_file.read(SHA256_HASH_SIZE)
content_one_block_digest = pkg_file.read(SHA256_HASH_SIZE)
table_entries = []
table_entries_map = {}
pkg_file.seek(file_table_offset)
for i in xrange(num_table_entries):
entry = FileTableEntry()
entry.read(pkg_file)
table_entries_map[entry.type] = len(table_entries)
table_entries.append(entry)
entry_names = None
entry_digests = None
for i in xrange(num_table_entries):
entry = table_entries[i]
if entry.type == ENTRY_TYPE_NAME_TABLE:
pkg_file.seek(entry.offset)
data = pkg_file.read(entry.size)
if data and len(data) > 0:
data = StringIO(data)
entry_names = []
c = data.read(1)
if ord(c) == 0:
while True:
name = read_cstring(data)
if not name:
break
entry_names.append(name)
else:
raise MyError('weird name table format')
break
entry_name_index = 0
for i in xrange(num_table_entries):
entry = table_entries[i]
type, index = (entry.type >> 8) & 0xFF, entry.type & 0xFF
if type == ENTRY_TYPE_FILE1 or type == ENTRY_TYPE_FILE2:
if entry_name_index < len(entry_names):
entry.name = entry_names[entry_name_index]
entry_name_index += 1
else:
raise MyError('entry name index out of bounds')
elif entry.type in ENTRY_TABLE_MAP:
entry.name = ENTRY_TABLE_MAP[entry.type]
if entry.type == ENTRY_TYPE_DIGEST_TABLE and entry_digests is None:
pkg_file.seek(entry.offset)
entry_digests = pkg_file.read(entry.size)
data = ''
for entry_type in [ENTRY_TYPE_0x800, ENTRY_TYPE_0x200, ENTRY_TYPE_0x180, ENTRY_TYPE_META_TABLE, ENTRY_TYPE_DIGEST_TABLE]:
entry = table_entries[table_entries_map[entry_type]]
pkg_file.seek(entry.offset)
data += pkg_file.read(entry.size)
computed_main_entries1_digest = sha256(data)
data = ''
for entry_type in [ENTRY_TYPE_0x800, ENTRY_TYPE_0x200, ENTRY_TYPE_0x180, ENTRY_TYPE_META_TABLE]:
entry = table_entries[table_entries_map[entry_type]]
pkg_file.seek(entry.offset)
size = entry.size if entry_type != ENTRY_TYPE_META_TABLE else num_system_entries * META_ENTRY_SIZE
data += pkg_file.read(size)
computed_main_entries2_digest = sha256(data)
entry = table_entries[table_entries_map[ENTRY_TYPE_DIGEST_TABLE]]
pkg_file.seek(entry.offset)
data = pkg_file.read(entry.size)
computed_digest_table_digest = sha256(data)
pkg_file.seek(body_offset)
body = pkg_file.read(body_size)
computed_body_digest = sha256(body)
computed_entry_digests = '\x00' * SHA256_HASH_SIZE
for i in xrange(num_table_entries):
entry = table_entries[i]
if entry.type == ENTRY_TYPE_DIGEST_TABLE:
continue
pkg_file.seek(entry.offset)
data = pkg_file.read(entry.size)
computed_entry_digests += sha256(data)
for i in xrange(num_table_entries):
entry = table_entries[i]
name = entry.name if entry.name is not None else 'entry_{0:03}.bin'.format(i)
file_path = os.path.join(output_dir, name)
file_dir = os.path.split(file_path)[0]
if not os.path.exists(file_dir):
os.makedirs(file_dir)
with open(file_path, 'wb') as entry_file:
pkg_file.seek(entry.offset)
data = pkg_file.read(entry.size)
entry_file.write(data)
block_size = 0x10000
num_blocks = 1 + int((content_size - 1) / block_size) if content_size > 0 else 0
pkg_file.seek(content_offset)
data = pkg_file.read(block_size)
computed_content_one_block_digest = sha256(data)
hash_context = hashlib.sha256()
pkg_file.seek(content_offset)
bytes_left = content_size
for i in xrange(num_blocks):
current_size = block_size if bytes_left > block_size else bytes_left
data = pkg_file.read(current_size)
hash_context.update(data)
bytes_left -= block_size
computed_content_digest = hash_context.digest()
is_digests_valid = computed_main_entries1_digest == main_entries1_digest
is_digests_valid = is_digests_valid and computed_main_entries2_digest == main_entries2_digest
is_digests_valid = is_digests_valid and computed_digest_table_digest == digest_table_digest
is_digests_valid = is_digests_valid and computed_body_digest == body_digest
is_digests_valid = is_digests_valid and computed_entry_digests == entry_digests
is_digests_valid = is_digests_valid and computed_content_digest == content_digest
is_digests_valid = is_digests_valid and computed_content_one_block_digest == content_one_block_digest
print 'File information:'
print '            Magic: 0x{0}'.format(magic.encode('hex').upper())
print '              Type: 0x{0:08X}'.format(type), '(retail)' if is_retail else ''
print '        Content ID: {0}'.format(content_id)
print ' Num table entries: {0}'.format(num_table_entries)
print 'Entry table offset: 0x{0:08X}'.format(file_table_offset)
print '    Digest status: {0}'.format('OK' if is_digests_valid else 'FAIL')
print
if num_table_entries > 0:
print 'Table entries:'
for i in xrange(num_table_entries):
entry = table_entries[i]
print '  Entry #{0:03}:'.format(i)
print '        Type: 0x{0:08X}'.format(entry.type)
print '        Unk1: 0x{0:08X}'.format(entry.unk1)
if entry.name is not None:
print '        Name: {0}'.format(entry.name)
print '      Offset: 0x{0:08X}'.format(entry.offset)
print '        Size: 0x{0:08X}'.format(entry.size)
print '      Flags 1: 0x{0:08X}'.format(entry.flags1)
print '      Flags 2: 0x{0:08X}'.format(entry.flags2)
print '    Key index: {0}'.format('N/A' if entry.key_index == 0 else entry.key_index)
print
except IOError:
print 'error: i/o error during processing'
except MyError as e:
print 'error: {0}', e.message
except:
print 'error: unexpected error:', sys.exc_info()[0]
traceback.print_exc(file=sys.stdout)
</source>


== Sample Packages ==
== Sample Packages ==
=== Apps/Games ===
=== Apps/Games ===
* Amazon/LOVEFiLM App
* Amazon/LOVEFiLM App
  http://gs2.ww.prod.dl.playstation.net/gs2/appkgo/prod/CUSA00126_00/1/f_012ccf9936265867696e3906c9bd9f0fd1869111fa372b2d1fad0ca4127ba67b/f/EP4183-CUSA00126_00-AIV00000000000EU.pkg
  http://gs2.ww.prod.dl.playstation.net/gs2/appkgo/prod/CUSA00126_00/1/f_012ccf9936265867696e3906c9bd9f0fd1869111fa372b2d1fad0ca4127ba67b/f/EP4183-CUSA00126_00-AIV00000000000EU.pkg
Line 152: Line 562:


=== Themes ===
=== Themes ===
* 20th Anniversary Dynamic Theme
* 20th Anniversary Dynamic Theme
  http://gs2.ww.prod.dl.playstation.net/gs2/acpkgo/prod/CUSA01501_00/5/f_3074fd8eb8322540c8742865a722c6773b031f68d517df3e18d7037fe58af7b0/f/EP9000-CUSA01501_00-20THANNITHEME001.pkg
  http://gs2.ww.prod.dl.playstation.net/gs2/acpkgo/prod/CUSA01501_00/5/f_3074fd8eb8322540c8742865a722c6773b031f68d517df3e18d7037fe58af7b0/f/EP9000-CUSA01501_00-20THANNITHEME001.pkg
Line 167: Line 578:
  http://gs2.ww.prod.dl.playstation.net/gs2/acpkgo/prod/CUSA01501_00/1/f_a6fb07c75a9776ca2f71e557a98620318bf11378c69d812f147c9f0fe12ee1c6/f/EP9000-CUSA01501_00-0000000000000003.pkg
  http://gs2.ww.prod.dl.playstation.net/gs2/acpkgo/prod/CUSA01501_00/1/f_a6fb07c75a9776ca2f71e557a98620318bf11378c69d812f147c9f0fe12ee1c6/f/EP9000-CUSA01501_00-0000000000000003.pkg


== See also ==


'''Source:''' https://boerse.to/thema/datenbank-fuer-ps4-psn-links.1979635/
* [[Package Files/Raw List 1]], [[Package Files/Raw List 2]].
 
* [https://gist.github.com/hosamn/255a5c29c5b3e47a50641a804813bf32 PS4 PKGs CUSA1-10000.csv (2022-11-28)]
See also: [[Package Files/Raw List 1]], [[Package Files/Raw List 2]]
* [https://gist.github.com/hosamn/8e22e39353bbbe3285f9bd603e3e0b70 PS4 PKGs CUSA1-10000.csv (2018-04-13)]
* [https://gist.github.com/hosamn/cadee497bacff249ccf95bad73b0f923 ps4_theme_links.csv]
* [https://gist.github.com/hosamn/2b7e30f095bd2c68908c18276ea19aed ps4_theme_links_private.csv]


{{File Formats}}
{{File Formats}}
<noinclude>[[Category:Main]]</noinclude>
<noinclude>[[Category:Main]]</noinclude>

Latest revision as of 23:32, 23 December 2024

See also PS5 PKG files, PSP, PS3 and PS Vita PKG files, PS Vita PKG files on henkaku wiki.

Structure[edit | edit source]

File Header[edit | edit source]

While most of the PS4 files are in little-endian, the package file header still uses big endianness as the headers are based on their PSP, PS3 and PS Vita predecessors.

 typedef struct {
   uint32_t pkg_magic;                      // 0x000 - 0x7F434E54
   uint32_t pkg_type;                       // 0x004
   uint32_t pkg_0x008;                      // 0x008 - unknown field
   uint32_t pkg_file_count;                 // 0x00C
   uint32_t pkg_entry_count;                // 0x010
   uint16_t pkg_sc_entry_count;             // 0x014
   uint16_t pkg_entry_count_2;              // 0x016 - same as pkg_entry_count
   uint32_t pkg_table_offset;               // 0x018 - file table offset
   uint32_t pkg_entry_data_size;            // 0x01C
   uint64_t pkg_body_offset;                // 0x020 - offset of PKG entries
   uint64_t pkg_body_size;                  // 0x028 - length of all PKG entries
   uint64_t pkg_content_offset;             // 0x030
   uint64_t pkg_content_size;               // 0x038
   unsigned char pkg_content_id[0x24];      // 0x040 - packages' content ID as a 36-byte string
   unsigned char pkg_padding[0xC];          // 0x064 - padding
   uint32_t pkg_drm_type;                   // 0x070 - DRM type
   uint32_t pkg_content_type;               // 0x074 - Content type
   uint32_t pkg_content_flags;              // 0x078 - Content flags
   uint32_t pkg_promote_size;               // 0x07C
   uint32_t pkg_version_date;               // 0x080
   uint32_t pkg_version_hash;               // 0x084
   uint32_t pkg_0x088;                      // 0x088
   uint32_t pkg_0x08C;                      // 0x08C
   uint32_t pkg_0x090;                      // 0x090
   uint32_t pkg_0x094;                      // 0x094
   uint32_t pkg_iro_tag;                    // 0x098
   uint32_t pkg_drm_type_version;           // 0x09C
   
/* Digest table */ unsigned char digest_entries1[0x20]; // 0x100 - sha256 digest for main entry 1 unsigned char digest_entries2[0x20]; // 0x120 - sha256 digest for main entry 2 unsigned char digest_table_digest[0x20]; // 0x140 - sha256 digest for digest table unsigned char digest_body_digest[0x20]; // 0x160 - sha256 digest for main table // ... uint32_t pfs_image_count; // 0x404 - count of PFS images uint64_t pfs_image_flags; // 0x408 - PFS flags uint64_t pfs_image_offset; // 0x410 - offset to start of external PFS image uint64_t pfs_image_size; // 0x418 - size of external PFS image uint64_t mount_image_offset; // 0x420 uint64_t mount_image_size; // 0x428 uint64_t pkg_size; // 0x430 uint32_t pfs_signed_size; // 0x438 uint32_t pfs_cache_size; // 0x43C unsigned char pfs_image_digest[0x20]; // 0x440 unsigned char pfs_signed_digest[0x20]; // 0x460 uint64_t pfs_split_size_nth_0; // 0x480 uint64_t pfs_split_size_nth_1; // 0x488 // ... unsigned char pkg_digest[0x20]; // 0xFE0 } pkg_header; // 0x1000

Files[edit | edit source]

The file table is a list of file entries:

 typedef struct {
   uint32_t id;               // File ID, useful for files without a filename entry
   uint32_t filename_offset;  // Offset into the filenames table (ID 0x200) where this file's name is located
   uint32_t flags1;           // Flags including encrypted flag, etc
   uint32_t flags2;           // Flags including encryption key index, etc
   uint32_t offset;           // Offset into PKG to find the file
   uint32_t size;             // Size of the file
   uint64_t padding;          // blank padding
 } pkg_table_entry;

Some of the files listed in the table with filenames include:

param.sfo - contains information critical to the app / game
playgo-chunk.dat - contains data regarding playgo (see Playgo)
playgo-chunk.sha - contains hash of playgo (see Playgo)
playgo-manifest.xml - contains manifest for playgo (see Playgo)
pronunciation.xml - contains word definitions for PS4's voice recognition software
pronunciation.sig - signature of definition file
pic0.png - small game preview icon
pic1.png - large game preview icon
icon0.png - small icon
icon1.png - large icon

There are also files without plaintext filenames. These are identified by their ID in the file entry table.

Table Entry Hashes (SHA-256)[edit | edit source]

The first entry in the index table points to a block of hashes.

Here is an example using an Amazon Instant Video package file (UP2064-CUSA00130_00-AIV00000000000US.pkg).

0x2A80  00 00 00 01 00 00 00 00 40 00 00 00 00 00 00 00  ........@....... First Entry in Index Table.
0x2A90  00 00 2C A0 00 00 02 20 00 00 00 00 00 00 00 00  .., ... ........
0x2AA0  00 00 00 10 00 00 00 00 60 00 00 00 00 00 00 00  ........`.......
0x2AB0  00 00 20 00 00 00 08 00 00 00 00 00 00 00 00 00  .. .............
0x2AC0  00 00 00 20 00 00 00 00 E0 00 00 00 00 00 30 00  ... ....à.....0.
0x2AD0  00 00 28 00 00 00 01 00 00 00 00 00 00 00 00 00  ..(.............
...

Offset: 0x00002CA0 Length: 0x00000220

0x2CA0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x2CB0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x2CC0  B4 34 DD 9C B3 C7 91 96 EC F7 D1 F6 8F 8A 2D 18  ´4Ýœ³Ç‘–ì÷Ñö.Š-.
0x2CD0  8D 07 8F 2D 33 E9 09 6B 1D 22 B5 4E 7A F2 6D 6A  ...-3é.k."µNzòmj
0x2CE0  F0 F5 9E 95 F4 74 13 FE 9F 35 DB 44 57 76 DE 49  ðõž•ôt.þŸ5ÛDWvÞI
0x2CF0  90 B1 68 20 97 8F 70 79 9D 62 95 CD 97 67 5D B0  .±h —.py.b•Í—g]°
0x2D00  1D 6E EE E7 67 3D 7E B4 2F 78 F1 26 2C EE EC 7A  .nîçg=~´/xñ&,îìz
0x2D10  10 40 90 BA FD 0F F9 AF BE ED F1 BC DE 84 30 55  .@.ºý.ù¯¾íñ¼Þ„0U
0x2D20  57 65 D8 7B DD 85 5E D0 73 1B 78 4D A6 EE 00 CF  WeØ{Ý…^Ðs.xM¦î.Ï
0x2D30  A1 0C 3F C4 03 E0 19 5A 0B 36 E1 64 33 7A D1 C6  ¡.?Ä.à.Z.6ád3zÑÆ
0x2D40  1A 4E E9 FA 4F DD AD F4 63 FF 73 8F 9F 24 6F 0E  .NéúOÝ.ôcÿs.Ÿ$o.
0x2D50  DF 22 EB 3D 43 F1 A3 7D C6 D0 BD 97 49 03 EC C2  ß"ë=Cñ£}Æн—I.ìÂ
0x2D60  DB 04 17 61 81 6A 14 9B 0F A3 B6 D7 6D AA 48 5A  Û..a.j.›.£¶×mªHZ
0x2D70  1F 3E 95 6B 63 BD AE B2 A2 E0 AE 44 8D D0 05 EA  .>•kc½®²¢à®D.Ð.ê
0x2D80  93 BB 8F 3E 60 72 F8 0C BD BA DB 0E 4D 01 AA AA  “».>`rø.½ºÛ.M.ªª
0x2D90  65 C0 97 E3 89 18 BB A2 17 6E 49 EE 3A 36 CA 91  eÀ—ã‰.»¢.nIî:6Ê‘
0x2DA0  5B EE 4F 1B 1B 7F 52 17 04 99 DD 8C 19 3A 31 BB  [îO...R..™ÝŒ.:1»
0x2DB0  79 9D F4 70 38 D5 F6 DD FF AA 76 5E 10 F2 CC 8F  y.ôp8ÕöÝÿªv^.òÌ.
0x2DC0  0A D9 DC 1C BA 98 EB B3 4A 74 02 E9 F1 0A 0A 90  .ÙÜ.º˜ë³Jt.éñ...
0x2DD0  69 AC D0 29 9F 93 DF 45 80 35 6E FB AF D6 B1 A5  i¬Ð)Ÿ“ßE€5nû¯Ö±¥
0x2DE0  C6 13 74 C9 51 F7 BA A5 CF 0D DE 13 E3 BB 02 0D  Æ.tÉQ÷º¥Ï.Þ.ã»..
0x2DF0  06 6E 44 64 FF 2A CA 37 B0 20 4C 03 44 CA 5E C9  .nDdÿ*Ê7° L.DÊ^É
0x2E00  B4 D0 03 6B 54 4A 66 ED C7 32 CB D2 E0 34 CF 5F  ´Ð.kTJfíÇ2ËÒà4Ï_
0x2E10  5B 1F 46 B5 81 72 09 D3 33 B3 3E 5E FC 01 6B 11  [.Fµ.r.Ó3³>^ü.k.
0x2E20  9A DF 99 EE A2 2B 5E E2 72 B9 32 02 6B B7 E8 D1  šß™î¢+^âr¹2.k·èÑ
0x2E30  5A 9D B8 A9 97 17 47 4F 11 75 FA 41 6E 79 7A 1B  Z.¸©—.GO.uúAnyz.
0x2E40  94 A5 62 30 EA E0 99 89 3D BB 34 5D 0B F5 E3 17  ”¥b0êà™‰=»4].õã.
0x2E50  BE 2C EE 7B D5 EA 8F 05 FB 0E 07 A2 40 FF 7A 59  ¾,î{Õê..û..¢@ÿzY
0x2E60  6B FE F8 0B 1E 61 85 83 18 9A 53 3A F0 91 46 B7  kþø..a…ƒ.šS:ð‘F·
0x2E70  86 83 38 B8 C1 3E E8 74 C5 4F 4E E6 B6 28 7F 52  †ƒ8¸Á>ètÅONæ¶(.R
0x2E80  55 FE CE EA F4 9E 98 2A BC A5 C4 21 D6 44 17 C9  UþÎêôž˜*¼¥Ä!ÖD.É
0x2E90  76 D9 1C 02 FD 75 BB 37 C3 96 1A C3 1C 3E 5B 5F  vÙ..ýu»7Ö.Ã.>[_
0x2EA0  2B 37 CA 02 AB E2 B7 C6 FB 74 23 B9 A6 C2 C6 0B  +7Ê.«â·Æût#¹¦ÂÆ.
0x2EB0  70 6F 79 CB AE 80 D9 B9 62 1A D6 69 F6 47 FB F2  poyË®€Ù¹b.ÖiöGûò

First hash is blank, can't hash the table of hashes.
The remaining hashes are for each of the remaining Index Table Entries.

i.e the second entry in the Index Table should have a SHA-256 matching the following

0xB434DD9CB3C79196ECF7D1F68F8A2D188D078F2D33E9096B1D22B54E7AF26D6A

PFS[edit | edit source]

  • Note that the main article PFS only explains un-encrypted, un-signed PFS.

The main portion of a PKG file is its signed and encrypted PFS image. It is encrypted with XTS-AES with a block size of 0x1000. The key is derived from the HMAC-SHA256 of the concatenation of "0x01 0x00 0x00 0x00" in and the EKPFS. The HMAC key is the "crypt seed" which is at offset 0x370 in the PFS itself. The tweak key is the first 16 bytes of the HMAC result, while the data key is the second 16 bytes.

The PFS image in a PKG file is different from the decrypted image exposed by the system in /mnt/sandbox/pfsmnt/CUSA00001-app0-nest/pfs_image.dat. This nested image uses dinode_s32, the signed inode variant, which includes a 32-byte signature for every direct and indirect block. Additionally, the indirect blocks also have a 32-byte signature for each block within.

Inside the PFS image in a PKG is a single file: pfs_image.dat which is a special type of compressed PFS image with a PFSC header. Inside pfs_image.dat is where the actual game/theme/DLC data live.

Delivery[edit | edit source]

Title XML[edit | edit source]

The PS4 fetches information about package files (including where to download them) from an XML file. This XML file contains information such as if the latest patch is mandatory, the latest package version, the manifest, as well as information in a param.sfo file. Below is an example of a typical title XML file:

<titlepatch titleid="CUSAXXXXX">
 <tag name="37" mandatory="true">
<package version="01.xx" size="" digest="" manifest_url="" content_id="" system_ver="" type="cumulative" remaster="false" patchgo="true"> <delta_info_set url="" />
<paramsfo> <title></title> ... </paramsfo> </package>
<latest_playgo_manifest url="" /> </tag> </titlepatch>

Notes:

  • The 'size' attribute of the 'package' node is in bytes
  • The 'system_ver' attribute of the 'package' node should be converted to hexadecimal for system firmware version
  • The 'delta_info_set' node may or may not be present depending on the package

Manifest[edit | edit source]

It should be noted that PS4 package files have a maximum size of 4GB (or 4096MB), therefore large (most) games are split into chunks or pieces. This is kept track of in the manifest file, which contains JSON fields which document things such as the size of the final package after the chunks are spliced together, the digest of the final package, the number of chunks, as well as information for each chunk such as the package file URL, offset for splicing, size of the file, and the sha1 hash value of the individual chunk.

originalFileSize: [size]
packageDigest: "[sha256 digest]"
numberOfSplitFiles: [num]
pieces:
 [n]:
  url: "[url of pkg chunk]"
  fileOffset: [offset]
  fileSize: [size, often 4294967296 until last chunk]
  hashValue: "[sha1 hash of chunk]"

Tools[edit | edit source]

UnPKG tool by flatz[edit | edit source]

UnPKG is an open source python2 library made by flatz to extract a PS4 ?release and/or debug? NPDRM PKG file. It maybe only extracts only partially the PKG or maybe requires a per-content key.

# UnPKG rev 0x00000008 (public edition), (c) flatz

import sys, os, hashlib, hmac, struct, math, traceback
from cStringIO import StringIO

# parse arguments

if len(sys.argv) < 3:
	script_file_name = os.path.split(sys.argv[0])[1]
	print 'usage: {0} <pkg file> <output dir>'.format(script_file_name)
	sys.exit()

pkg_file_path = sys.argv[1]
if not os.path.isfile(pkg_file_path):
	print 'error: invalid file specified'
	sys.exit()

output_dir = sys.argv[2]
if os.path.exists(output_dir) and not os.path.isdir(output_dir):
	print 'error: invalid directory specified'
	sys.exit()
elif not os.path.exists(output_dir):
	os.makedirs(output_dir)

# cryptography functions

def sha256(data):
	return hashlib.sha256(data).digest()

# utility functions

uint64_fmt, uint32_fmt, uint16_fmt, uint8_fmt = '>Q', '>I', '>H', '>B'
int64_fmt, int32_fmt, int16_fmt, int8_fmt = '>q', '>i', '>h', '>b'

def read_string(f, length):
	return f.read(length)
def read_cstring(f):
	s = ''
	while True:
		c = f.read(1)
		if not c:
			return False
		if ord(c) == 0:
			break
		s += c
	return s

def read_uint8_le(f):
	return struct.unpack('<B', f.read(struct.calcsize('<B')))[0]
def read_uint8_be(f):
	return struct.unpack('>B', f.read(struct.calcsize('>B')))[0]
def read_uint16_le(f):
	return struct.unpack('<H', f.read(struct.calcsize('<H')))[0]
def read_uint16_be(f):
	return struct.unpack('>H', f.read(struct.calcsize('>H')))[0]
def read_uint32_le(f):
	return struct.unpack('<I', f.read(struct.calcsize('<I')))[0]
def read_uint32_be(f):
	return struct.unpack('>I', f.read(struct.calcsize('>I')))[0]
def read_uint64_le(f):
	return struct.unpack('<Q', f.read(struct.calcsize('<Q')))[0]
def read_uint64_be(f):
	return struct.unpack('>Q', f.read(struct.calcsize('>Q')))[0]
def read_int8_le(f):
	return struct.unpack('<b', f.read(struct.calcsize('<b')))[0]
def read_int8_be(f):
	return struct.unpack('>b', f.read(struct.calcsize('>b')))[0]
def read_int16_le(f):
	return struct.unpack('<h', f.read(struct.calcsize('<h')))[0]
def read_int16_be(f):
	return struct.unpack('>h', f.read(struct.calcsize('>h')))[0]
def read_int32_le(f):
	return struct.unpack('<i', f.read(struct.calcsize('<i')))[0]
def read_int32_be(f):
	return struct.unpack('>i', f.read(struct.calcsize('>i')))[0]
def read_int64_le(f):
	return struct.unpack('<q', f.read(struct.calcsize('<q')))[0]
def read_int64_be(f):
	return struct.unpack('>q', f.read(struct.calcsize('>q')))[0]

# main code

PKG_MAGIC = '\x7FCNT'
CONTENT_ID_SIZE = 0x24
SHA256_HASH_SIZE = 0x20
META_ENTRY_SIZE = 0x20

FILE_TYPE_FLAGS_RETAIL = 1 << 31

ENTRY_TYPE_DIGEST_TABLE = 0x0001
ENTRY_TYPE_0x800        = 0x0010
ENTRY_TYPE_0x200        = 0x0020
ENTRY_TYPE_0x180        = 0x0080
ENTRY_TYPE_META_TABLE   = 0x0100
ENTRY_TYPE_NAME_TABLE   = 0x0200

ENTRY_TYPE_LICENSE = 0x04
ENTRY_TYPE_FILE1   = 0x10
ENTRY_TYPE_FILE2   = 0x12

ENTRY_TABLE_MAP = {
	ENTRY_TYPE_DIGEST_TABLE: '.digests',
	ENTRY_TYPE_0x800: '.entry_0x800',
	ENTRY_TYPE_0x200: '.entry_0x200',
	ENTRY_TYPE_0x180: '.entry_0x180',
	ENTRY_TYPE_META_TABLE: '.meta',
	ENTRY_TYPE_NAME_TABLE: '.names',

	0x0400: 'license.dat',
	0x0401: 'license.info',
	0x1000: 'param.sfo',
	0x1001: 'playgo-chunk.dat',
	0x1002: 'playgo-chunk.sha',
	0x1003: 'playgo-manifest.xml',
	0x1004: 'pronunciation.xml',
	0x1005: 'pronunciation.sig',
	0x1006: 'pic1.png',
	0x1008: 'app/playgo-chunk.dat',
	0x1200: 'icon0.png',
	0x1220: 'pic0.png',
	0x1240: 'snd0.at9',
	0x1260: 'changeinfo/changeinfo.xml',
}

class MyError(Exception):
	def __init__(self, message):
		self.message = message

	def __str__(self):
		return repr(self.message)

class FileTableEntry:
	entry_fmt = '>IIIIII8x'

	def __init__(self):
		pass

	def read(self, f):
		self.type, self.unk1, self.flags1, self.flags2, self.offset, self.size = struct.unpack(self.entry_fmt, f.read(struct.calcsize(self.entry_fmt)))
		self.key_index = (self.flags2 & 0xF000) >> 12
		self.name = None

try:
	with open(pkg_file_path, 'rb') as pkg_file:
		magic = read_string(pkg_file, 4)
		if magic != PKG_MAGIC:
			raise MyError('invalid file magic')

		type = read_uint32_be(pkg_file)
		is_retail = (type & FILE_TYPE_FLAGS_RETAIL) != 0

		pkg_file.seek(0x10) # FIXME: or maybe uint16 at 0x16???
		num_table_entries = read_uint32_be(pkg_file)

		pkg_file.seek(0x14)
		num_system_entries = read_uint16_be(pkg_file)

		pkg_file.seek(0x18)
		file_table_offset = read_uint32_be(pkg_file)

		pkg_file.seek(0x1C)
		main_entries_data_size = read_uint32_be(pkg_file)

		pkg_file.seek(0x24)
		body_offset = read_uint32_be(pkg_file)
		pkg_file.seek(0x2C)
		body_size = read_uint32_be(pkg_file)

		pkg_file.seek(0x414)
		content_offset = read_uint32_be(pkg_file)
		pkg_file.seek(0x41C)
		content_size = read_uint32_be(pkg_file)

		pkg_file.seek(0x40)
		content_id = read_cstring(pkg_file)
		if len(content_id) != CONTENT_ID_SIZE:
			raise MyError('invalid content id')

		pkg_file.seek(0x100)
		main_entries1_digest = pkg_file.read(SHA256_HASH_SIZE)
		main_entries2_digest = pkg_file.read(SHA256_HASH_SIZE)
		digest_table_digest = pkg_file.read(SHA256_HASH_SIZE)
		body_digest = pkg_file.read(SHA256_HASH_SIZE)

		pkg_file.seek(0x440)
		content_digest = pkg_file.read(SHA256_HASH_SIZE)
		content_one_block_digest = pkg_file.read(SHA256_HASH_SIZE)

		table_entries = []
		table_entries_map = {}
		pkg_file.seek(file_table_offset)
		for i in xrange(num_table_entries):
			entry = FileTableEntry()
			entry.read(pkg_file)
			table_entries_map[entry.type] = len(table_entries)
			table_entries.append(entry)

		entry_names = None
		entry_digests = None
		for i in xrange(num_table_entries):
			entry = table_entries[i]
			if entry.type == ENTRY_TYPE_NAME_TABLE:
				pkg_file.seek(entry.offset)
				data = pkg_file.read(entry.size)
				if data and len(data) > 0:
					data = StringIO(data)
					entry_names = []
					c = data.read(1)
					if ord(c) == 0:
						while True:
							name = read_cstring(data)
							if not name:
								break
							entry_names.append(name)
					else:
						raise MyError('weird name table format')
				break
		entry_name_index = 0
		for i in xrange(num_table_entries):
			entry = table_entries[i]
			type, index = (entry.type >> 8) & 0xFF, entry.type & 0xFF
			if type == ENTRY_TYPE_FILE1 or type == ENTRY_TYPE_FILE2:
				if entry_name_index < len(entry_names):
					entry.name = entry_names[entry_name_index]
					entry_name_index += 1
				else:
					raise MyError('entry name index out of bounds')
			elif entry.type in ENTRY_TABLE_MAP:
				entry.name = ENTRY_TABLE_MAP[entry.type]
			if entry.type == ENTRY_TYPE_DIGEST_TABLE and entry_digests is None:
				pkg_file.seek(entry.offset)
				entry_digests = pkg_file.read(entry.size)

		data = ''
		for entry_type in [ENTRY_TYPE_0x800, ENTRY_TYPE_0x200, ENTRY_TYPE_0x180, ENTRY_TYPE_META_TABLE, ENTRY_TYPE_DIGEST_TABLE]:
			entry = table_entries[table_entries_map[entry_type]]
			pkg_file.seek(entry.offset)
			data += pkg_file.read(entry.size)
		computed_main_entries1_digest = sha256(data)

		data = ''
		for entry_type in [ENTRY_TYPE_0x800, ENTRY_TYPE_0x200, ENTRY_TYPE_0x180, ENTRY_TYPE_META_TABLE]:
			entry = table_entries[table_entries_map[entry_type]]
			pkg_file.seek(entry.offset)
			size = entry.size if entry_type != ENTRY_TYPE_META_TABLE else num_system_entries * META_ENTRY_SIZE
			data += pkg_file.read(size)
		computed_main_entries2_digest = sha256(data)

		entry = table_entries[table_entries_map[ENTRY_TYPE_DIGEST_TABLE]]
		pkg_file.seek(entry.offset)
		data = pkg_file.read(entry.size)
		computed_digest_table_digest = sha256(data)

		pkg_file.seek(body_offset)
		body = pkg_file.read(body_size)
		computed_body_digest = sha256(body)

		computed_entry_digests = '\x00' * SHA256_HASH_SIZE
		for i in xrange(num_table_entries):
			entry = table_entries[i]
			if entry.type == ENTRY_TYPE_DIGEST_TABLE:
				continue
			pkg_file.seek(entry.offset)
			data = pkg_file.read(entry.size)
			computed_entry_digests += sha256(data)

		for i in xrange(num_table_entries):
			entry = table_entries[i]
			name = entry.name if entry.name is not None else 'entry_{0:03}.bin'.format(i)
			file_path = os.path.join(output_dir, name)
			file_dir = os.path.split(file_path)[0]
			if not os.path.exists(file_dir):
				os.makedirs(file_dir)
			with open(file_path, 'wb') as entry_file:
				pkg_file.seek(entry.offset)
				data = pkg_file.read(entry.size)
				entry_file.write(data)

		block_size = 0x10000
		num_blocks = 1 + int((content_size - 1) / block_size) if content_size > 0 else 0

		pkg_file.seek(content_offset)
		data = pkg_file.read(block_size)
		computed_content_one_block_digest = sha256(data)

		hash_context = hashlib.sha256()
		pkg_file.seek(content_offset)
		bytes_left = content_size
		for i in xrange(num_blocks):
			current_size = block_size if bytes_left > block_size else bytes_left
			data = pkg_file.read(current_size)
			hash_context.update(data)
			bytes_left -= block_size
		computed_content_digest = hash_context.digest()

		is_digests_valid = computed_main_entries1_digest == main_entries1_digest
		is_digests_valid = is_digests_valid and computed_main_entries2_digest == main_entries2_digest
		is_digests_valid = is_digests_valid and computed_digest_table_digest == digest_table_digest
		is_digests_valid = is_digests_valid and computed_body_digest == body_digest
		is_digests_valid = is_digests_valid and computed_entry_digests == entry_digests
		is_digests_valid = is_digests_valid and computed_content_digest == content_digest
		is_digests_valid = is_digests_valid and computed_content_one_block_digest == content_one_block_digest

		print 'File information:'
		print '             Magic: 0x{0}'.format(magic.encode('hex').upper())
		print '              Type: 0x{0:08X}'.format(type), '(retail)' if is_retail else ''
		print '        Content ID: {0}'.format(content_id)
		print ' Num table entries: {0}'.format(num_table_entries)
		print 'Entry table offset: 0x{0:08X}'.format(file_table_offset)
		print '     Digest status: {0}'.format('OK' if is_digests_valid else 'FAIL')
		print

		if num_table_entries > 0:
			print 'Table entries:'
			for i in xrange(num_table_entries):
				entry = table_entries[i]
				print '  Entry #{0:03}:'.format(i)
				print '         Type: 0x{0:08X}'.format(entry.type)
				print '         Unk1: 0x{0:08X}'.format(entry.unk1)
				if entry.name is not None:
					print '         Name: {0}'.format(entry.name)
				print '       Offset: 0x{0:08X}'.format(entry.offset)
				print '         Size: 0x{0:08X}'.format(entry.size)
				print '      Flags 1: 0x{0:08X}'.format(entry.flags1)
				print '      Flags 2: 0x{0:08X}'.format(entry.flags2)
				print '    Key index: {0}'.format('N/A' if entry.key_index == 0 else entry.key_index)
			print
		
except IOError:
	print 'error: i/o error during processing'
except MyError as e:
	print 'error: {0}', e.message
except:
	print 'error: unexpected error:', sys.exc_info()[0]
	traceback.print_exc(file=sys.stdout)

Sample Packages[edit | edit source]

Apps/Games[edit | edit source]

  • Amazon/LOVEFiLM App
http://gs2.ww.prod.dl.playstation.net/gs2/appkgo/prod/CUSA00126_00/1/f_012ccf9936265867696e3906c9bd9f0fd1869111fa372b2d1fad0ca4127ba67b/f/EP4183-CUSA00126_00-AIV00000000000EU.pkg
  • Headset Begleit-App
http://gs2.ww.prod.dl.playstation.net/gs2/appkgo/prod/CUSA00468_00/2/f_2e42a175df474235aa4e1fd5e5b6fe744433ce5165a109d9f25e8a56cc2dae02/f/EP9000-CUSA00468_00-HEADSETCOMPANION.pkg
  • IGN App
http://gs2.ww.prod.dl.playstation.net/gs2/appkgo/prod/CUSA00268_00/2/f_d505314f45cf63825aba7b0d12e8d4d11248d43cb9da25ade13319a4dfc0835d/f/EP4436-CUSA00268_00-WEBMAF0000000IGN.pkg
  • Maxdome App
http://gs2.ww.prod.dl.playstation.net/gs2/appkgo/prod/CUSA00115_00/5/f_ce26675e7d8ea8a746486ebe08772d680a597603293b866a1ac79efc519362eb/f/EP4374-CUSA00115_00-MAXDOMEFULLAPP00.pkg
  • Netflix App
http://gs2.ww.prod.dl.playstation.net/gs2/appkgo/prod/CUSA00127_00/1/f_7ebe28278e8c18913cb0118bdb960e852d44aec490c238e74e899749c793cbaf/f/EP4350-CUSA00127_00-NETFLIXPOLLUX001.pkg
  • SingStar App
http://gs2.ww.prod.dl.playstation.net/gs2/appkgo/prod/CUSA00033_00/3/f_3228ceaa0d67c882a7d21d67790e1216dd6dd1a5c69e93a3d6bf6edb46bcaca2/f/EP9000-CUSA00033_00-SINGSTARE3XX2013.pkg
  • VidZone App
http://gs2.ww.prod.dl.playstation.net/gs2/appkgo/prod/CUSA00235_00/5/f_6e74c11865320274ad56655f051502a3935bf33de4af448a268cac707e50025a/f/EP4071-CUSA00235_00-0000000000000000.pkg
  • YouTube App
http://gs2.ww.prod.dl.playstation.net/gs2/appkgo/prod/CUSA01116_00/3/f_46f1700767dd845e919dd18aeb8fc1e96c4ba8ac6053b75f5ff3a0b8745d524a/f/EP4381-CUSA01116_00-YOUTUBESCEE00000.pkg

Themes[edit | edit source]

  • 20th Anniversary Dynamic Theme
http://gs2.ww.prod.dl.playstation.net/gs2/acpkgo/prod/CUSA01501_00/5/f_3074fd8eb8322540c8742865a722c6773b031f68d517df3e18d7037fe58af7b0/f/EP9000-CUSA01501_00-20THANNITHEME001.pkg
  • AR-Roboter Dynamic Theme
http://gs2.ww.prod.dl.playstation.net/gs2/acpkgo/prod/CUSA00001_00/13/f_aef3a991112dfa798166c951f32b20870b1313f586bdc7788678ba6659671259/f/IP9100-CUSA00001_00-PLAYROOM0THEME01.pkg
  • Rechtecke Dynamic Theme
http://gs2.ww.prod.dl.playstation.net/gs2/acpkgo/prod/CUSA01501_00/3/f_65216ee84a210c1541f395558498b451952b1a81fd4a9a0ae77e32b97aeb6122/f/EP9000-CUSA01501_00-0000000000000002.pkg
  • Papierskulptur Dynamic Theme
http://gs2.ww.prod.dl.playstation.net/gs2/acpkgo/prod/CUSA01501_00/2/f_fc5c05478edf8847dc118d07225d6e58c630f088f5eae4d0559c88bdda674de6/f/EP9000-CUSA01501_00-0000000000000001.pkg
  • Spiralen Dynamic Theme
http://gs2.ww.prod.dl.playstation.net/gs2/acpkgo/prod/CUSA01501_00/1/f_a6fb07c75a9776ca2f71e557a98620318bf11378c69d812f147c9f0fe12ee1c6/f/EP9000-CUSA01501_00-0000000000000003.pkg

See also[edit | edit source]