Skip to content

Instantly share code, notes, and snippets.

@markjenkins
Last active March 15, 2016 16:17
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save markjenkins/7036740 to your computer and use it in GitHub Desktop.
Save markjenkins/7036740 to your computer and use it in GitHub Desktop.
Custom program for cracking a multibit .key backup file (compatible with openssl enc/dec) with some existing knowledge of a passphrase I was told by someone who forgot some of it.
#!/usr/bin/env python
# Copying and distribution of this file, with or without modification,
# are permitted in any medium without royalty provided the copyright
# notice and this notice are preserved. This file is offered as-is,
# without any warranty.
# http://www.gnu.org/prep/maintain/html_node/License-Notices-for-Other-Files.html
# @author Mark Jenkins <mark@markjenkins.ca>
# usage
# python custom_crack_multibit_key_export.py sample.key
# builtin to python
from itertools import product
from string import punctuation
from base64 import b64decode
from hashlib import md5
# pycrypto
from Crypto.Cipher import AES
WORDS_ORIGINAL = WORDS = ('tiger', 'bear', 'salmon', 'elephant')
assert( word == word.lower() for word in WORDS )
# note my use of tuple arithmatic,
# e.g. (1, 2) + (3,4) + (5,) = (1, 2, 3, 4, 5)
# with last letter missing
WORDS = WORDS + tuple( word[:-1] for word in WORDS_ORIGINAL)
# with first letter missing
WORDS = WORDS + tuple( word[1:] for word in WORDS_ORIGINAL )
WORDS_LOWER = WORDS
# all of the above starting with a capital letter
WORDS = WORDS + tuple( word.capitalize() for word in WORDS_LOWER)
# all of the above in full upper case
WORDS = WORDS + tuple( word.upper() for word in WORDS_LOWER)
WORDS = WORDS + ('',) # word missing
PUNCS = tuple(punctuation) + ('',)
FOUR_LETTER_CONSTANTS = ('an7y', '')
SALT_LENGTH = 8 # 64 bits
SALTED_CONSTANT = 'Salted__'
assert( SALT_LENGTH == len(SALTED_CONSTANT) )
KEY_LENGTH = 32 # 256 bits
BLOCK_SIZE = AES.block_size # 16
MD5_SIZE = len(md5().digest())
def get_salt_and_ciphertext_from_base64_file(base64_file):
with open(base64_file) as f:
ascii_armoured_stuff = ''.join(f)
full_encrypt = b64decode(ascii_armoured_stuff)
if full_encrypt[:SALT_LENGTH] != SALTED_CONSTANT:
raise Exception("%s doesn't start with Salted__" % base64_file)
return ( full_encrypt[SALT_LENGTH:SALT_LENGTH*2],
full_encrypt[SALT_LENGTH*2:] )
def derive_key_and_iv(password, salt, key_length, iv_length):
"""Inspired by the function of the same name and signature at
http://stackoverflow.com/questions/16761458/how-to-aes-encrypt-decrypt-files-using-python-pycrypto-in-an-openssl-compatible
Really, just re-write using reduce(), as I'm just that kind of guy
"""
def round_of_md5( (so_far, last), i):
new_digest = md5( last + password + salt).digest()
return (so_far + new_digest, new_digest)
combined_key_and_iv_length = key_length + iv_length
# the number of bytes we need divided by the number of bytes we
# get per round
#
# With key_length being 32 bytes and iv_length being 16 bytes, and md5
# producing 16 bytes, we're only talking 3 rounds here!
number_of_rounds = combined_key_and_iv_length / MD5_SIZE
result, toss_away = reduce(round_of_md5, range(number_of_rounds),
('', '') )
return result[:key_length], result[key_length:combined_key_and_iv_length]
def decrypt(ciphertext, salt, password):
key, iv = derive_key_and_iv(password, salt, KEY_LENGTH, BLOCK_SIZE)
cipher = AES.new(key, AES.MODE_CBC, iv)
return cipher.decrypt(ciphertext)
def has_padding_return_real_stuff(plaintext_with_padding):
last_byte = plaintext_with_padding[-1]
last_byte_as_int = ord(last_byte)
if last_byte_as_int > BLOCK_SIZE:
return False, None
else:
# yeah for slicing, note the colon positions
padding = plaintext_with_padding[-last_byte_as_int:]
plaintext_without_padding = plaintext_with_padding[:-last_byte_as_int]
return ( all( given_byte == last_byte for given_byte in padding ),
plaintext_without_padding )
def is_all_ascii(plaintext):
# 2 ** 7 meaning, only first 7 bits used, e.g. < 128
return all( ord(char) < (2**7) for char in plaintext )
def try_decrypt(ciphertext, salt, password):
plaintext_w_padding = decrypt(ciphertext, salt, password)
has_pad, plaintext = has_padding_return_real_stuff(plaintext_w_padding)
return ( (False, None) if (not has_pad or not is_all_ascii(plaintext) )
else (True, plaintext) )
def main():
from sys import argv
print "%s words, %s punctuation choices, %s constants" % (
len(WORDS), len(PUNCS), len(FOUR_LETTER_CONSTANTS) )
print "up to %s passphrases will be tried" % (
(len(WORDS) ** 2) *
(len(PUNCS) ** 3) *
(len(FOUR_LETTER_CONSTANTS) ** 2 ) )
salt, ciphertext = get_salt_and_ciphertext_from_base64_file(argv[1])
for i, (punc_1, punc_2, punc_3,
word_1, word_2, four_let_1, four_let_2) in \
enumerate(product(PUNCS, PUNCS, PUNCS, WORDS, WORDS,
FOUR_LETTER_CONSTANTS, FOUR_LETTER_CONSTANTS),
1): # start enumeration at 1
passphrase = (
punc_1 +
word_1 +
four_let_1 +
punc_2 +
word_2 +
four_let_2 +
punc_3
)
result, plaintext = try_decrypt(ciphertext, salt, passphrase)
if result:
print 'found %s after %s tries' % (passphrase, i)
print plaintext, # should have a newline on the end already :)
exit(0)
print 'tried %s and failed' % i
if __name__ == "__main__":
main()
U2FsdGVkX181Ewhmj5SZjFjWrJqWgUlzj2GR2hgCH75V+SHQk9E97aFEVH3IS+U4vEhk/L42WaqC
7E1erg//b45GU467Q4eRl6oKtwKNVrKdcMbGg3Ifpab/nee5Ow0i
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment