# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard # and is covered by GPLv3 license found in COPYING. # # compat7z.py # # Implement a bare-bones 7z encrypted file read/writer. Does not do compression, but # always does AES-256. Not really expecting to be able to read any 7z file, except # those we created ourselves. # import os, sys import ubinascii from ubinascii import hexlify as b2a_hex from ubinascii import unhexlify as a2b_hex from ubinascii import crc32 from ustruct import unpack, pack, calcsize from ucollections import namedtuple from trezorcrypto import sha256 # uhashlib also works import trezorcrypto from uio import BytesIO from common import noise def masked_crc(bits): return crc32(bits) & 0xffffffff def urandom(l): from noise_source import NoiseSource rv = bytearray(l) noise.random_bytes(rv, NoiseSource.ALL) return rv def encode_utf_16_le(s): # emulate: str.encode('utf-16-le') # by assuming ascii values if isinstance(s, str): s = s.encode() return bytes((s[i//2] if i%2==0 else 0) for i in range(len(s)*2)) def decode_utf_16_le(s): # emulate: bytes.dencode('utf-16-le') # by assuming simple ascii values if isinstance(s, str): s = s.encode() return bytes(s[i] for i in range(0, len(s), 2)).decode() ''' Size of encoding sequence depends from first byte: First_Byte Extra_Bytes Value (binary) 0xxxxxxx : ( xxxxxxx ) 10xxxxxx BYTE y[1] : ( xxxxxx << (8 * 1)) + y 110xxxxx BYTE y[2] : ( xxxxx << (8 * 2)) + y ... 1111110x BYTE y[6] : ( x << (8 * 6)) + y 11111110 BYTE y[7] : y 11111111 BYTE y[8] : y ''' def read_var64(f): ''' Decode their silly 64-bit encoding. ''' first = ord(f.read(1)) if first < 128: return first elif first == 0xfe or first == 0xff: return unpack("> pos) return (x << pos) + y def write_var64(n): # write their funky 64-bit variable-width unsigned number. # up to 64 bits of uint, but typically just single bytes # cheating a little here, these aren't optimal if n < 127: return chr(n) if n < 65536: return b'\xc0' + pack(' 10000: raise ValueError("Second header too big") # capture this spot data_start = f.tell() # expect 0x20 try: f.seek(sh.offset, 1) th = f.read(sh.size) if len(th) != sh.size: raise IndexError("Truncated file? %s" % e.message) # Look for properties about compression. this could be # faked-out but good enough for now if b'\x24\x06\xf1\x07\x01' not in th: raise RuntimeError("Not marked as AES+SHA encrypted?") except Exception as e: raise ValueError("Confused file? %s" % e.message) if masked_crc(th) != sh.crc: raise ValueError("Trailing header has wrong CRC") # Not clear if there can be more headers, but assume only one for now. # success; restore file pointer, just in case f.seek(0) return class FileHeader(object): def __init__(self): self.magic = b"7z\xbc\xaf'\x1c" self.major = 0 self.minor = 3 self.crc = 0 # actually the CRC of the next header def has_good_magic(self): if self.magic != b"7z\xbc\xaf'\x1c": return False if self.major != 0: return False if self.minor < 3: return False return True @classmethod def read(cls, f): fmt = '<6sBBL' bits = f.read(calcsize(fmt)) self = cls() self.bits = bits self.magic, self.major, self.minor, self.crc = unpack(fmt, bits) return self def write(self): self.bits = self.magic + pack(' %r" % (fname, unpacked_size, shdr)) files.append((fname, unpacked_size)) # should be at end of file now. assert not fd.read(10) return files def add_data(self, raw): if not self.aes: # do this late, so easier to test w/ known values. #self.aes = AES.AESCipher(self.key, mode=AES.MODE_CBC, IV=self.iv) # self.aes = tcc.AES(tcc.AES.CBC | tcc.AES.Encrypt, self.key, self.iv) self.aes = trezorcrypto.aes(trezorcrypto.aes.CBC, self.key, self.iv) here = len(raw) self.pt_crc = crc32(raw, self.pt_crc) padded_len = (here + 15) & ~15 if padded_len != here: if self.padding != None: raise ValueError("can't do less than a block except at end") self.padding = (padded_len - here) raw += '\x00' * self.padding self.unpacked_size += here assert len(raw) % 16 == 0, b2a_hex(raw) self.body += self.aes.encrypt(raw) def calculate_key(self, password, progress_fcn=None): # do the expected key-derivation # emulate CKeyInfo::CalculateDigest in p7zip_9.38.1/CPP/7zip/Crypto/7zAes.cpp rounds = 1 << self.rounds_pow password = encode_utf_16_le(password) result = sha256() for i in range(rounds): result.update(self.salt) result.update(password) temp = pack('> 4) & 0xf) + 1 iv_len = (second & 0xf) + 1 assert salt_len >= 16 assert iv_len >= 16 self.salt = rv.read(salt_len) self.iv = rv.read(iv_len) end_pos = rv.seek(0, 1) # .tell() is missing assert end_pos - start_pos == crypto_props_len, (end_pos, start_pos, crypto_props_len) rv = patmatch('01 00 0c', rv.getvalue()) unpacked_size = read_var64(rv) assert rv.read(1) == b'\0' rv = patmatch('08 0a 01', rv.getvalue()) expect_crc = unpack('