95 lines
3.4 KiB
Python
Executable file
95 lines
3.4 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
# Based on https://problemkaputt.de/gbatek.htm#3dscartridgeregisters
|
|
import functools
|
|
import sys
|
|
|
|
import ctr_secrets
|
|
import cart_crypto
|
|
|
|
def rotl128(n, r):
|
|
return ((n << r) | (n >> (128 - r))) & ((2**128) - 1)
|
|
|
|
def derive_key(keyX, keyY):
|
|
x = rotl128(int.from_bytes(keyX, 'big'), 2)
|
|
x = (x ^ int.from_bytes(keyY, 'big')) & ((2**128) - 1)
|
|
x = (x + ctr_secrets.KEY_DERIVATION_CONSTANT) & ((2**128) - 1)
|
|
return rotl128(x, 87).to_bytes(16, 'big')
|
|
|
|
def card_seed(path):
|
|
with open(path, 'rb') as f:
|
|
cardinfo = f.read(0x1040)
|
|
|
|
keyX = ctr_secrets.BOOTROM_KEYS[0x3b]['keyX']
|
|
keyY = cardinfo[0x1000:0x1010]
|
|
enc = cardinfo[0x1010:0x1020]
|
|
tag = cardinfo[0x1020:0x1030]
|
|
nonce = cardinfo[0x1030:0x103c]
|
|
|
|
kek = derive_key(keyX, keyY)
|
|
c = cart_crypto.AES_128_CCM(kek)
|
|
try:
|
|
key = c.decrypt(tag, nonce, enc)
|
|
except ValueError:
|
|
print("Bad authentication tag on title key. Is ctr_secrets correct?")
|
|
sys.exit(1)
|
|
return int.from_bytes(key[0:8], 'big'), int.from_bytes(key[8:16], 'big')
|
|
|
|
# Use indexes 0-3 depending on ID2
|
|
cardrc4keyhalf = ctr_secrets.CART_KEYS[0]['rc4']
|
|
cardsnowiv = ctr_secrets.CART_KEYS[0]['snow']
|
|
|
|
def tolist(val, nbytes):
|
|
return [val >> i & 0xff for i in reversed(range(0, nbytes*8, 8))]
|
|
|
|
def initsnowstream(snowkey):
|
|
snow20stream = cart_crypto.SNOW20(snowkey, cardsnowiv)
|
|
for _ in range(32): # Discard the first 1024 bits of output
|
|
snow20stream.next()
|
|
return snow20stream
|
|
|
|
def initrc4stream(snowstream):
|
|
tmp = snowstream.next() << 96 | snowstream.next() << 64 | \
|
|
snowstream.next() << 32 | snowstream.next()
|
|
key = cardrc4keyhalf << 128 | tmp
|
|
rc4stream = cart_crypto.RC4(tolist(key, 32))
|
|
for _ in range(256): # Discard the first 256 bytes of output
|
|
rc4stream.next()
|
|
return rc4stream
|
|
|
|
# Sample data from https://www.3dbrew.org/wiki/Gamecards
|
|
titlekey1, titlekey2 = card_seed('0004000000038c00.3ds')
|
|
|
|
enccmds = [[0xf32c92d85c9d44ded3e0e41dbe7c90d9, 0x00],
|
|
[0x696b9d8582fb55d31b68cafe70c74a95, 0x04],
|
|
[0xbaa4812ca0ac9c5d19399530e3acccab, 0x04],
|
|
[0x178e427c22d87adb86387249a97d321a, 0x00],
|
|
[0xe06019b1bd5c9130ed6a4d9f4a9e7193, 0x04],
|
|
[0x4e0d224862523bbfe2e6255f80e15f37, 0x04],
|
|
[0x4cdf93d319fb62d0db632a45e3e8d84c, 0x04],
|
|
[0x9aa5d80551002f955546d296a57f0fef, 0x04],
|
|
[0xc12ba81aef30dddbd93fad5d544c6334, 0x04],
|
|
[0x62ec5fb7f420ae1dc6253ae18afa5bb3, 0x200+4], # + 4 byte CRC
|
|
[0xe3fa23aa016be0c93430d1f42ff41324, 0x200+4]] # + 4 byte CRC
|
|
# CRC polynomial is 0x82608edb, end result is inverted, CRC over decrypted data
|
|
|
|
snow20stream = initsnowstream(titlekey1 << 64 | titlekey2)
|
|
rc4stream = initrc4stream(snow20stream)
|
|
|
|
for cmd, datalen in enccmds:
|
|
print('encrypted command: %032x' % cmd)
|
|
dec = functools.reduce(lambda x, y: x<<8 | y, [
|
|
rc4stream.next() ^ x for x in tolist(cmd, 16)])
|
|
print('decrypted command: %032x' % dec)
|
|
|
|
if dec >> (15*8) == 0x83: #"set seed" command
|
|
randseed = dec & 0xFFFFFFFFFFFFFFFF
|
|
print('changing "seed" to %016x' % randseed)
|
|
snow20stream = initsnowstream(titlekey1 << 64 | randseed)
|
|
|
|
rc4stream = initrc4stream(snow20stream)
|
|
|
|
print('skipping %d bytes' % datalen)
|
|
for _ in range(datalen):
|
|
rc4stream.next() #these RC4 bytes would be used to decrypt the data
|
|
|