Last active
March 15, 2016 16:17
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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