add fused device support and warnings to precursorupdater

This commit is contained in:
bunnie 2022-12-10 17:44:54 +08:00
parent e42c4c7b33
commit 2b4e47384e
2 changed files with 174 additions and 1 deletions

View file

@ -12,6 +12,13 @@ try:
except ImportError:
from precursorusb import PrecursorUsb
from bip_utils import (
Bip39MnemonicValidator, Bip39MnemonicDecoder
)
from Crypto.Cipher import AES
from Crypto.Hash import SHA256
from Crypto.Random import get_random_bytes
QR_CODE = """
██████████████ ██ ████ ██████ ██ ██████████████
@ -46,6 +53,163 @@ QR_CODE = """
"""
def bitflip(data_block, bitwidth=32):
if bitwidth == 0:
return data_block
bytewidth = bitwidth // 8
bitswapped = bytearray()
i = 0
while i < len(data_block):
data = int.from_bytes(data_block[i:i+bytewidth], byteorder='big', signed=False)
b = '{:0{width}b}'.format(data, width=bitwidth)
bitswapped.extend(int(b[::-1], 2).to_bytes(bytewidth, byteorder='big'))
i = i + bytewidth
return bytes(bitswapped)
# assumes a, b are the same length eh?
def xor_bytes(a, b):
i = 0
y = bytearray()
while i < len(a):
y.extend((a[i] ^ b[i]).to_bytes(1, byteorder='little'))
i = i + 1
return bytes(y)
def try_key_to_bytes(input):
if len(input.split(' ')) == 24: # 24 words is BIP-39
# Get if a mnemonic is valid with automatic language detection, return bool
assert(Bip39MnemonicValidator().IsValid(input))
# Like before with automatic language detection
key_bytes = Bip39MnemonicDecoder().Decode(input)
else:
key_bytes = int(input, 16).to_bytes(32, byteorder='big')
return key_bytes
# binfile should be the input SoC file, already read in as bytes()
# returns the encrypted version of binfile
def encrypt_to_efuse(binfile, key):
print("Encrypting gateware to target-specific key...")
# extract the keys
key_bytes = bytes([0] * 32)
new_key = try_key_to_bytes(key)
new_hmac = get_random_bytes(32)
new_iv = get_random_bytes(16)
# search for structure
# 0x3001_6004 -> specifies the CBC key
# 4 words of CBC IV
# 0x3003_4001 -> ciphertext len
# 1 word of ciphertext len
# then ciphertext
position = 0
iv_pos = 0
while position < len(binfile):
cwd = int.from_bytes(binfile[position:position+4], 'big')
if cwd == 0x3001_6004:
iv_pos = position+4
if cwd == 0x3003_4001:
break
position = position + 1
position = position + 4
ciphertext_len = 4* int.from_bytes(binfile[position:position+4], 'big')
position = position + 4
active_area = binfile[position : position+ciphertext_len]
postamble = binfile[position+ciphertext_len:]
iv_bytes = bitflip(binfile[iv_pos : iv_pos+0x10]) # note that the IV is embedded in the file
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
plain_bitstream = cipher.decrypt(bitflip(active_area))
# now construct the output file and its hashes
plaintext = bytearray()
f = bytearray()
# fixed header that sets 66MHz config speed, x1, 1.8V, eFuse target
device_header = [
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xaa, 0x99, 0x55, 0x66, 0x20, 0x00, 0x00, 0x00, 0x30, 0x03, 0xe0, 0x01, 0x00, 0x00, 0x00, 0x0b,
0x30, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x12, 0x20, 0x00, 0x00, 0x00, 0x30, 0x00, 0xc0, 0x01,
0x80, 0x00, 0x00, 0x40, 0x30, 0x00, 0xa0, 0x01, 0x80, 0x00, 0x00, 0x40, 0x30, 0x01, 0xc0, 0x01,
0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00,
0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00,
0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00,
0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x30, 0x01, 0x60, 0x04,
]
for item in device_header: # add the cleartext header
f.extend(bytes([item]))
f.extend(bitflip(new_iv)) # insert the IV
ciphertext_header = [
0x30, 0x03, 0x40, 0x01, 0x00, 0x08, 0x5b, 0x98,
]
for item in ciphertext_header: # add the cleartext length-of-ciphertext field before the ciphertext
f.extend(bytes([item]))
# generate the header and footer hash keys.
header = int(0x6C6C6C6C6C6C6C6C6C6C6C6C6C6C6C6C6C6C6C6C6C6C6C6C6C6C6C6C6C6C6C6C).to_bytes(32, byteorder='big')
keyed_header = xor_bytes(header, new_hmac)
footer = int(0x3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A).to_bytes(32, byteorder='big')
keyed_footer = xor_bytes(footer, new_hmac)
# add the header
plaintext.extend(keyed_header)
plaintext.extend(header)
# insert the bitstream plaintext, skipping the header and the trailing HMAC.
plaintext.extend(plain_bitstream[64:-160])
# compute first HMAC of stream with new HMAC key
h1 = SHA256.new()
k = 0
while k < len(plaintext) - 320: # HMAC does /not/ cover the whole file, it stops 320 bytes short of the end
h1.update(bitflip(plaintext[k:k+16], 32))
k = k + 16
h1_digest = h1.digest()
# add the footer
plaintext.extend(keyed_footer)
plaintext.extend(footer)
plaintext.extend(bytes(32)) # empty spot where hash #1 would be stored
hash_pad = [ # sha-256 padding for the zero'd hash #1, which is in the bitstream and seems necessary for verification
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00,
]
plaintext.extend(hash_pad)
# compute the hash of the hash, presumably to prevent length extension attacks?
h2 = SHA256.new()
h2.update(bitflip(keyed_footer))
h2.update(bitflip(footer))
h2.update(h1_digest)
h2_digest = h2.digest()
# commit the final HMAC to the bitstream plaintext
plaintext.extend(bitflip(h2_digest))
# encrypt the bitstream
newcipher = AES.new(new_key, AES.MODE_CBC, new_iv)
# finally generate the ciphertext block, which encapsulates the HMACs
ciphertext = newcipher.encrypt(bytes(plaintext))
# add ciphertext to the bitstream
f.extend(bitflip(ciphertext))
# add the cleartext postamble to the bitstream. These are a series of NOP commands + all of the csr.csv data & signatures
f.extend(postamble)
print("Encryption success! {} bytes generated.".format(len(f)))
assert len(f) == 2621440, "Encryption length is incorrect; aborting!"
return f
def get_with_progress(url, name='Downloading'):
r = requests.get(url, stream=True)
total_length = int(r.headers.get('content-length'))
@ -148,6 +312,9 @@ def main():
parser.add_argument(
"--dry-run", required=False, help="Don't actually burn anything, but print what would happen if we did", action="store_true"
)
parser.add_argument(
"--key", help="Backup key in hex or BIP-39 format. Used to factory-reset eFused devices. Specify BIP-39 phrase within double-quotes.", type=str
)
args = parser.parse_args()
@ -210,6 +377,7 @@ def main():
exit(0)
if args.factory_reset:
print("\nWARNING: if a backup key is set, the correct key MUST be specified with `--key`, or else the device will be bricked.")
if False == single_yes_or_no_question("This will permanently erase user data on the device. Proceed? "):
print("Abort by user request.")
exit(0)
@ -271,6 +439,9 @@ def main():
pc_usb.erase_region(work[2], work[3])
break
else:
if args.factory_reset and args.key is not None and work[1] == "Overwriting boot gateware":
# re-encrypt the gateware if we're doing a factory reset and a key was specified
work[3] = encrypt_to_efuse(work[3], args.key)
if args.dry_run:
print("DRYRUN: Would write at 0x{:x}".format(work[2]))
else:

View file

@ -7,7 +7,7 @@ with open('README.md') as f:
setup(
name="precursorupdater",
version="0.0.9",
version="0.1.0",
author="bunnie",
description="Precursor USB Updater",
long_description=long_description,
@ -33,6 +33,8 @@ setup(
"requests >= 2",
"pyusb >= 1",
"progressbar2 >= 3",
"bip-utils >= 2.5",
"pycryptodome >= 3",
],
entry_points="""
[console_scripts]