openssl AES256 with a common standardized Password-Based Key Derivation Function
#6
Maybe this can help:

Code:
############### MultiBit ###############
# - MultiBit .key backup files
# - MultiDoge .key backup files
# - Bitcoin Wallet for Android/BlackBerry v3.47+ wallet backup files
# - Bitcoin Wallet for Android/BlackBerry v2.24 and older key backup files
# - Bitcoin Wallet for Android/BlackBerry v2.3 - v3.46 key backup files
# - KnC for Android key backup files (same as the above)

@register_wallet_class
class WalletMultiBit(object):

    class __metaclass__(type):
        @property
        def data_extract_id(cls): return b"mb"

    # MultiBit private key backup file (not the wallet file)
    @staticmethod
    def is_wallet_file(wallet_file):
        wallet_file.seek(0)
        try:  data = base64.b64decode(wallet_file.read(20).lstrip()[:12])
        except TypeError: return False
        return data.startswith(b"Salted__")

    def __init__(self, loading = False):
        assert loading, 'use load_from_* to create a ' + self.__class__.__name__
        aes_library_name = load_aes256_library().__name__
        self._passwords_per_second = 100000 if aes_library_name == "Crypto" else 5000

    def __setstate__(self, state):
        # (re-)load the required libraries after being unpickled
        load_aes256_library(warnings=False)
        self.__dict__ = state

    def passwords_per_seconds(self, seconds):
        return max(int(round(self._passwords_per_second * seconds)), 1)

    # Load a Multibit private key backup file (the part of it we need)
    @classmethod
    def load_from_filename(cls, privkey_filename):
        with open(privkey_filename) as privkey_file:
            # Multibit privkey files contain base64 text split into multiple lines;
            # we need the first 48 bytes after decoding, which translates to 64 before.
            data = b"".join(privkey_file.read(70).split())  # join multiple lines into one
        if len(data) < 64: raise EOFError("Expected at least 64 bytes of text in the MultiBit private key file")
        data = base64.b64decode(data[:64])
        assert data.startswith(b"Salted__"), "WalletBitcoinCore.load_from_filename: file starts with base64 'Salted__'"
        if len(data) < 48:  raise EOFError("Expected at least 48 bytes of decoded data in the MultiBit private key file")
        self = cls(loading=True)
        self._encrypted_block = data[16:48]  # the first two 16-byte AES blocks
        self._salt            = data[8:16]
        return self

    # Import a MultiBit private key that was extracted by extract-multibit-privkey.py
    @classmethod
    def load_from_data_extract(cls, privkey_data):
        assert len(privkey_data) == 24
        print(prog + ": WARNING: read the Usage for MultiBit Classic section of Extract_Scripts.md before proceeding", file=sys.stderr)
        self = cls(loading=True)
        self._encrypted_block = privkey_data[8:]  # a single 16-byte AES block
        self._salt            = privkey_data[:8]
        return self

    def difficulty_info(self):
        return "3 MD5 iterations"

    # This is the time-consuming function executed by worker thread(s). It returns a tuple: if a password
    # is correct return it, else return False for item 0; return a count of passwords checked for item 1
    assert b"1" < b"9" < b"A" < b"Z" < b"a" < b"z"  # the b58 check below assumes ASCII ordering in the interest of speed
    def return_verified_password_or_false(self, orig_passwords):
        # Copy a few globals into local for a small speed boost
        l_md5                = hashlib.md5
        l_aes256_cbc_decrypt  = aes256_cbc_decrypt
        encrypted_block      = self._encrypted_block
        salt                  = self._salt

        # Convert Unicode strings (lazily) to UTF-16 bytestrings, truncating each code unit to 8 bits
        if tstr == unicode:
            passwords = itertools.imap(lambda p: p.encode("utf_16_le", "ignore")[::2], orig_passwords)
        else:
            passwords = orig_passwords

        for count, password in enumerate(passwords, 1):
            salted = password + salt
            key1  = l_md5(salted).digest()
            key2  = l_md5(key1 + salted).digest()
            iv    = l_md5(key2 + salted).digest()
            b58_privkey = l_aes256_cbc_decrypt(key1 + key2, iv, encrypted_block[:16])

            # (all this may be fragile, e.g. what if comments or whitespace precede what's expected in future versions?)
            if b58_privkey[0] in b"LK5Q\x0a#":
                #
                # Does it look like a base58 private key (MultiBit, MultiDoge, or oldest-format Android key backup)?
                if b58_privkey[0] in b"LK5Q":  # private keys always start with L, K, or 5, or for MultiDoge Q
                    for c in b58_privkey[1:]:
                        # If it's outside of the base58 set [1-9A-HJ-NP-Za-km-z], break
                        if c > b"z" or c < b"1" or b"9" < c < b"A" or b"Z" < c < b"a" or c in b"IOl":
                            break
                    # If the loop above doesn't break, it's base58-looking so far
                    else:
                        # If another AES block is available, decrypt and check it as well to avoid false positives
                        if len(encrypted_block) >= 32:
                            b58_privkey = l_aes256_cbc_decrypt(key1 + key2, encrypted_block[:16], encrypted_block[16:32])
                            for c in b58_privkey:
                                if c > b"z" or c < b"1" or b"9" < c < b"A" or b"Z" < c < b"a" or c in b"IOl":
                                    break  # not base58
                            # If the loop above doesn't break, it's base58; we've found it
                            else:
                                return orig_passwords[count-1], count
                        else:
                            # (when no second block is available, there's a 1 in 300 billion false positive rate here)
                            return orig_passwords[count - 1], count
                #
                # Does it look like a bitcoinj protobuf (newest Bitcoin for Android backup)
                elif b58_privkey[2:6] == b"org." and b58_privkey[0] == b"\x0a" and ord(b58_privkey[1]) < 128:
                    for c in b58_privkey[6:14]:
                        # If it doesn't look like a lower alpha domain name of len >= 8 (e.g. 'bitcoin.'), break
                        if c > b"z" or (c < b"a" and c != b"."):
                            break
                    # If the loop above doesn't break, it looks like a domain name; we've found it
                    else:
                        return orig_passwords[count - 1], count
                #
                #  Does it look like a KnC for Android key backup?
                elif b58_privkey == b"# KEEP YOUR PRIV":
                    return orig_passwords[count-1], count

        return False, count


From: https://github.com/gurnec/btcrecover/blo...tcrpass.py
Reply


Messages In This Thread
RE: openssl AES256 with a common standardized Password-Based Key Derivation Function - by derlange2k - 01-28-2020, 03:47 PM