mirror of
https://github.com/encounter/Decrypt9.git
synced 2026-03-30 11:06:30 -07:00
178 lines
5.2 KiB
Python
178 lines
5.2 KiB
Python
#!/usr/bin/env python2
|
|
# This script is old and shitty
|
|
|
|
import os
|
|
import errno
|
|
import sys
|
|
import urllib2
|
|
from struct import unpack
|
|
from binascii import hexlify
|
|
from hashlib import sha256
|
|
from Crypto.Cipher import AES
|
|
|
|
|
|
def mkdir_p(path):
|
|
try:
|
|
os.makedirs(path)
|
|
except OSError as exc: # Python >2.5
|
|
if exc.errno == errno.EEXIST and os.path.isdir(path):
|
|
pass
|
|
else:
|
|
raise
|
|
|
|
|
|
# ######## # From https://stackoverflow.com/questions/5783517/downloading-progress-bar-urllib2-python
|
|
def chunk_report(bytes_so_far, chunk_size, total_size):
|
|
percent = float(bytes_so_far) / total_size
|
|
percent = round(percent*100, 2)
|
|
sys.stdout.write("Downloaded %d of %d bytes (%0.2f%%)\r" % (bytes_so_far, total_size, percent))
|
|
|
|
if bytes_so_far >= total_size:
|
|
sys.stdout.write('\n')
|
|
|
|
|
|
def chunk_read(response, outfname, chunk_size=2*1024*1024, report_hook=None):
|
|
fh = open(outfname, 'wb')
|
|
total_size = response.info().getheader('Content-Length').strip()
|
|
total_size = int(total_size)
|
|
bytes_so_far = 0
|
|
|
|
while 1:
|
|
if report_hook:
|
|
report_hook(bytes_so_far, chunk_size, total_size)
|
|
|
|
chunk = response.read(chunk_size)
|
|
bytes_so_far += len(chunk)
|
|
if not chunk:
|
|
break
|
|
|
|
fh.write(chunk)
|
|
fh.close()
|
|
# ######## #
|
|
|
|
|
|
def display_usage_info():
|
|
print 'Usage: cdn_download.py TitleID TitleKey [-redown -redec]'
|
|
print '-redown : redownload content'
|
|
print '-redec : re-attempt content decryption'
|
|
raise SystemExit(0)
|
|
|
|
if len(sys.argv) < 3:
|
|
display_usage_info()
|
|
|
|
titleid = sys.argv[1]
|
|
titlekey = sys.argv[2]
|
|
forceDownload = 0
|
|
forceDecrypt = 0
|
|
|
|
for i in xrange(len(sys.argv)):
|
|
if sys.argv[i] == '-redown':
|
|
forceDownload = 1
|
|
elif sys.argv[i] == '-redec':
|
|
forceDecrypt = 1
|
|
|
|
if len(titleid) != 16 or len(titlekey) != 32:
|
|
print 'Invalid arguments'
|
|
raise SystemExit(0)
|
|
|
|
baseurl = 'http://nus.cdn.c.shop.nintendowifi.net/ccs/download/' + titleid
|
|
|
|
print 'Downloading TMD...'
|
|
|
|
try:
|
|
tmd = urllib2.urlopen(baseurl + '/tmd')
|
|
except urllib2.URLError, e:
|
|
print 'ERROR: Bad title ID?'
|
|
raise SystemExit(0)
|
|
|
|
tmd = tmd.read()
|
|
|
|
mkdir_p(titleid)
|
|
open(titleid + '/tmd', 'wb').write(tmd)
|
|
print 'Done\n'
|
|
|
|
if tmd[:4] != '\x00\x01\x00\x04':
|
|
print 'Unexpected signature type.'
|
|
raise SystemExit(0)
|
|
|
|
# Set Proper Version
|
|
version = unpack('>H', tmd[0x1dc:0x1de])[0]
|
|
# Set Save Size
|
|
saveSize = (unpack('<I', tmd[0x19a:0x19e])[0])/1024
|
|
|
|
contentCount = unpack('>H', tmd[0x206:0x208])[0]
|
|
|
|
print 'Content count: ' + str(contentCount) + '\n'
|
|
|
|
|
|
# Download Contents
|
|
fSize = 16*1024
|
|
for i in xrange(contentCount):
|
|
cOffs = 0xB04+(0x30*i)
|
|
cID = format(unpack('>I', tmd[cOffs:cOffs+4])[0], '08x')
|
|
cIDX = format(unpack('>H', tmd[cOffs+4:cOffs+6])[0], '04x')
|
|
cSIZE = format(unpack('>Q', tmd[cOffs+8:cOffs+16])[0], 'd')
|
|
cHASH = format(unpack('>32s', tmd[cOffs+16:cOffs+48])[0])
|
|
|
|
print 'Content ID: ' + cID
|
|
print 'Content Index: ' + cIDX
|
|
print 'Content Size: ' + cSIZE
|
|
print 'Content Hash: ' + hexlify(cHASH)
|
|
|
|
aes_obj = AES.new(titlekey.decode("hex"), AES.MODE_CBC, (cIDX + '0000000000000000000000000000').decode("hex"))
|
|
|
|
outfname = titleid + '/' + cID
|
|
if os.path.exists(outfname) == 0 or forceDownload == 1\
|
|
or os.path.getsize(outfname) != unpack('>Q', tmd[cOffs+8:cOffs+16])[0]:
|
|
response = urllib2.urlopen(baseurl + '/' + cID)
|
|
chunk_read(response, outfname, report_hook=chunk_report)
|
|
|
|
# If we redownloaded the content, then decrypting it is implied.
|
|
with open(outfname, 'rb') as fo:
|
|
ciphertext = fo.read()
|
|
dec = aes_obj.decrypt(ciphertext)
|
|
with open(outfname + '.dec', 'wb') as fo:
|
|
fo.write(dec)
|
|
|
|
elif os.path.exists(outfname + '.dec') == 0 or forceDecrypt == 1 \
|
|
or os.path.getsize(outfname + '.dec') != unpack('>Q', tmd[cOffs+8:cOffs+16])[0]:
|
|
with open(outfname, 'rb') as fo:
|
|
ciphertext = fo.read()
|
|
dec = aes_obj.decrypt(ciphertext)
|
|
with open(outfname + '.dec', 'wb') as fo:
|
|
fo.write(dec)
|
|
|
|
with open(outfname + '.dec', 'rb') as fh:
|
|
fh.seek(0, os.SEEK_END)
|
|
fhSize = fh.tell()
|
|
if fh.tell() != unpack('>Q', tmd[cOffs+8:cOffs+16])[0]:
|
|
print 'Title size mismatch. Download likely incomplete'
|
|
print 'Downloaded: ' + format(fh.tell(), 'd')
|
|
raise SystemExit(0)
|
|
fh.seek(0)
|
|
hash = sha256()
|
|
|
|
while fh.tell() != fhSize:
|
|
hash.update(fh.read(0x1000000))
|
|
print 'checking hash: ' + format(float(fh.tell()*100)/fhSize, '.1f') + '% done\r',
|
|
|
|
sha256file = hash.hexdigest()
|
|
if sha256file != hexlify(cHASH):
|
|
print 'hash mismatched, Decryption likely failed, wrong key?'
|
|
print 'got hash: ' + sha256file
|
|
raise SystemExit(0)
|
|
fh.seek(0x100)
|
|
if fh.read(4) != 'NCCH':
|
|
fh.seek(0x60)
|
|
if fh.read(4) != 'WfA\0':
|
|
print 'Not NCCH, nor DSiWare, file likely corrupted'
|
|
raise SystemExit(0)
|
|
else:
|
|
print 'Not an NCCH container, likely DSiWare'
|
|
fh.seek(0, os.SEEK_END)
|
|
fSize += fh.tell()
|
|
|
|
print '\n'
|
|
|
|
print 'Done!'
|