Talk:Package Files: Difference between revisions
Jump to navigation
Jump to search
No edit summary |
(Added digest check) |
||
Line 4: | Line 4: | ||
<source lang="python"> | <source lang="python"> | ||
# UnPKG rev | # UnPKG rev 0x00000006 (public edition), (c) flatz | ||
import sys, os, hashlib, hmac, struct, traceback | import sys, os, hashlib, hmac, struct, traceback | ||
Line 28: | Line 28: | ||
os.makedirs(output_dir) | os.makedirs(output_dir) | ||
# utility | # cryptography functions | ||
def sha256(data): | |||
return hashlib.sha256(data).digest() | |||
# utility functions | |||
uint64_fmt, uint32_fmt, uint16_fmt, uint8_fmt = '>Q', '>I', '>H', '>B' | uint64_fmt, uint32_fmt, uint16_fmt, uint8_fmt = '>Q', '>I', '>H', '>B' | ||
Line 79: | Line 84: | ||
return struct.unpack('>q', f.read(struct.calcsize('>q')))[0] | return struct.unpack('>q', f.read(struct.calcsize('>q')))[0] | ||
# | # main code | ||
PKG_MAGIC = '\x7FCNT' | PKG_MAGIC = '\x7FCNT' | ||
Line 86: | Line 91: | ||
FILE_TYPE_FLAGS_RETAIL = 1 << 31 | FILE_TYPE_FLAGS_RETAIL = 1 << 31 | ||
ENTRY_TYPE_META_TABLE = | ENTRY_TYPE_DIGEST_TABLE = 0x0001 | ||
ENTRY_TYPE_NAME_TABLE = | ENTRY_TYPE_META_TABLE = 0x0100 | ||
ENTRY_TYPE_LICENSE | ENTRY_TYPE_NAME_TABLE = 0x0200 | ||
ENTRY_TYPE_FILE1 | |||
ENTRY_TYPE_FILE2 | ENTRY_TYPE_LICENSE = 0x04 | ||
ENTRY_TYPE_FILE1 = 0x10 | |||
ENTRY_TYPE_FILE2 = 0x12 | |||
ENTRY_TABLE_MAP = { | ENTRY_TABLE_MAP = { | ||
ENTRY_TYPE_DIGEST_TABLE: '.digests', | |||
ENTRY_TYPE_META_TABLE: '.meta', | |||
ENTRY_TYPE_NAME_TABLE: '.names', | |||
0x0400: 'license.dat', | 0x0400: 'license.dat', | ||
0x0401: 'license.info', | 0x0401: 'license.info', | ||
Line 139: | Line 147: | ||
pkg_file.seek(0x10) # FIXME: or maybe uint16 at 0x16??? | pkg_file.seek(0x10) # FIXME: or maybe uint16 at 0x16??? | ||
num_table_entries = read_uint32_be(pkg_file) | |||
pkg_file.seek(0x18) | pkg_file.seek(0x18) | ||
Line 149: | Line 157: | ||
raise MyError('invalid content id') | raise MyError('invalid content id') | ||
table_entries = [] | |||
pkg_file.seek(file_table_offset) | pkg_file.seek(file_table_offset) | ||
for i in xrange( | for i in xrange(num_table_entries): | ||
entry = FileTableEntry() | entry = FileTableEntry() | ||
entry.read(pkg_file) | entry.read(pkg_file) | ||
table_entries.append(entry) | |||
entry_names = None | entry_names = None | ||
for i in xrange( | entry_digests = None | ||
entry = | for i in xrange(num_table_entries): | ||
entry = table_entries[i] | |||
if entry.type == ENTRY_TYPE_NAME_TABLE: | |||
pkg_file.seek(entry.offset) | pkg_file.seek(entry.offset) | ||
data = pkg_file.read(entry.size) | data = pkg_file.read(entry.size) | ||
Line 177: | Line 185: | ||
break | break | ||
entry_name_index = 0 | entry_name_index = 0 | ||
for i in xrange( | for i in xrange(num_table_entries): | ||
entry = | entry = table_entries[i] | ||
type, index = (entry.type >> 8) & 0xFF, entry.type & 0xFF | type, index = (entry.type >> 8) & 0xFF, entry.type & 0xFF | ||
if type == ENTRY_TYPE_FILE1 or type == ENTRY_TYPE_FILE2: | if type == ENTRY_TYPE_FILE1 or type == ENTRY_TYPE_FILE2: | ||
Line 188: | Line 196: | ||
elif entry.type in ENTRY_TABLE_MAP: | elif entry.type in ENTRY_TABLE_MAP: | ||
entry.name = ENTRY_TABLE_MAP[entry.type] | 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) | |||
for i in xrange( | computed_entry_digests = '\x00' * 32 | ||
entry = | 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) | 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_path = os.path.join(output_dir, name) | ||
Line 200: | Line 212: | ||
data = pkg_file.read(entry.size) | data = pkg_file.read(entry.size) | ||
entry_file.write(data) | entry_file.write(data) | ||
if entry.type != ENTRY_TYPE_DIGEST_TABLE: | |||
computed_entry_digests += sha256(data) | |||
is_digests_valid = computed_entry_digests == entry_digests | |||
print 'File information:' | print 'File information:' | ||
Line 205: | Line 220: | ||
print ' Type: 0x{0:08X}'.format(type), '(retail)' if is_retail else '' | print ' Type: 0x{0:08X}'.format(type), '(retail)' if is_retail else '' | ||
print ' Content ID: {0}'.format(content_id) | print ' Content ID: {0}'.format(content_id) | ||
print ' Num table entries: {0}'.format( | print ' Num table entries: {0}'.format(num_table_entries) | ||
print 'Entry table offset: 0x{0:08X}'.format(file_table_offset) | print 'Entry table offset: 0x{0:08X}'.format(file_table_offset) | ||
print ' Digest status: {0}'.format('OK' if is_digests_valid else 'FAIL') | |||
print | print | ||
if | if num_table_entries > 0: | ||
print 'Table entries:' | print 'Table entries:' | ||
for i in xrange( | for i in xrange(num_table_entries): | ||
entry = | entry = table_entries[i] | ||
print ' Entry #{0:03}:'.format(i) | print ' Entry #{0:03}:'.format(i) | ||
print ' Type: 0x{0:08X}'.format(entry.type) | print ' Type: 0x{0:08X}'.format(entry.type) | ||
Line 232: | Line 248: | ||
print 'error: unexpected error:', sys.exc_info()[0] | print 'error: unexpected error:', sys.exc_info()[0] | ||
traceback.print_exc(file=sys.stdout) | traceback.print_exc(file=sys.stdout) | ||
</source> | </source> |
Revision as of 02:52, 20 November 2013
PKG file
UnPKG tool
# UnPKG rev 0x00000006 (public edition), (c) flatz
import sys, os, hashlib, hmac, struct, 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
FILE_TYPE_FLAGS_RETAIL = 1 << 31
ENTRY_TYPE_DIGEST_TABLE = 0x0001
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_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',
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(0x18)
file_table_offset = 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')
table_entries = []
pkg_file.seek(file_table_offset)
for i in xrange(num_table_entries):
entry = FileTableEntry()
entry.read(pkg_file)
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)
computed_entry_digests = '\x00' * 32
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)
if entry.type != ENTRY_TYPE_DIGEST_TABLE:
computed_entry_digests += sha256(data)
is_digests_valid = computed_entry_digests == entry_digests
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)