As promised, more details.
Wrote a custom python program that got it done in 5 seconds.
Found after 179,763 attempts out of the search space of 21,130,956 I was
looking through. This was done on an old 32 bit Pentium M laptop that
Skullspace gave me. Would have completed that entire search in 10 minutes.
Used my
brain and came up with some fairly reasonable and variations on the info
Ian provided.
I can now avoid disclosing exactly what Ian told me.
Attached is my code, but you'll notice my main program has been renamed
crack-censored.py and contains a fake word and constant list for Ian's
protection. (I asked him before even posting this much)
Structurally this tells more or less what I was told, so you can see the
kinds of possibilities I was looking through and the general information
I got out of him.
I've had some communication with the lead MultiBit developer. Will
probably get this code up on git hub and posted on the MultiBit section
of bitcointalk.org at some point.
Funds are now being transfered to safety. Details to come. I have to run
off now to the casino.. I mean thanksgiving dinner.
Annnnd they're gone.
Final graph of the address we thought we'd lost:
https://blockchain.info/charts/balance?showDataPoints=true×pan=&daysAverageString=1&scale=0&address=1Mz6YwFap2FEpPrSq3EEpqW4Endo7gA1wr
New address that Ian and I have joint control over (sorry, no fancy N of
M stuff) is:
https://blockchain.info/address/14eS3vDCn66Dy7N1PJXA7KTULPirfCJJeW
The big withdraw is me getting paid the coins I was owed from Sept 14.
(sold "Skullspace" coins by using mine, didn't know we were going to
lose control over them...)
Total might be smaller then you'd expect as well as we've had some
mining downtime this month. And mining profitability is falling fast as
expected -- calculator sites say we should get about 0.0149 a day for now.
Mark
#!/usr/bin/env python
from itertools import product
from string import punctuation
from decrypt import read_ciphertext_and_salt_files, try_decrypt
WORDS = ('tiger', 'bear', 'salmon', 'elephant') # not the actual Ian word list
assert( word == word.lower() for word in WORDS )
# with last letter missing
WORDS = WORDS + tuple( word[:-1] for word in WORDS)
# with first letter missing
WORDS = WORDS + tuple( word[1:] for word in WORDS )
WORDS = WORDS + ('',) # word missing
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)
PUNCS = tuple(punctuation) + ('',)
FOUR_LETTER_CONSTANT = 'an7y' # not the actual four letter constant from Ian
FOUR_LETTER_CONSTANTS = (FOUR_LETTER_CONSTANT, '')
print "%s words, %s puncs, %s constants" % (
len(WORDS), len(PUNCS), len(FOUR_LETTER_CONSTANT) )
print "should be %s trys" % (
(len(WORDS) ** 2) *
(len(PUNCS) ** 3) *
(len(FOUR_LETTER_CONSTANT) ** 2 ) )
ciphertext, salt = read_ciphertext_and_salt_files(
'multibit-20130724092711.key.ciphertext',
'multibit-20130724092711.key.salt', )
i = 0
for (punc_1, punc_2, punc_3,
word_1, word_2, four_let_1, four_let_2) in \
product(PUNCS, PUNCS, PUNCS, WORDS, WORDS,
FOUR_LETTER_CONSTANTS, FOUR_LETTER_CONSTANTS
):
passphrase = (
punc_1 +
word_1 +
four_let_1 +
punc_2 +
word_2 +
four_let_2 +
punc_3
)
result, plaintext = try_decrypt(ciphertext, salt, passphrase)
i+=1
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
#!/usr/bin/env python
# simplified and stolen from
# http://stackoverflow.com/questions/16761458/how-to-aes-encrypt-decrypt-files-using-python-pycrypto-in-an-openssl-compatible
from hashlib import md5
from Crypto.Cipher import AES
from Crypto import Random
KEY_LENGTH = 32 # 256 bits
BLOCK_SIZE = AES.block_size # 16
def derive_key_and_iv(password, salt, key_length, iv_length):
d = d_i = ''
while len(d) < key_length + iv_length:
d_i = md5(d_i + password + salt).digest()
d += d_i
return d[:key_length], d[key_length:key_length+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(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(plaintext_w_padding)
if not has_pad:
return False, None
elif not is_all_ascii(plaintext):
return False, None
else:
return True, plaintext
def read_ciphertext_and_salt_files(ciphertext_file, salt_file):
with open(ciphertext_file) as f:
ciphertext = ''.join(f)
with open(salt_file) as f:
salt = ''.join(f)
return ciphertext, salt
#!/usr/bin/env python
from sys import argv
from base64 import b64decode
from os.path import exists
with open(argv[1]) as f:
ascii_armoured_stuff = ''.join(f)
full_encrypt = b64decode(ascii_armoured_stuff)
assert( full_encrypt[:8] == 'Salted__')
salt_out = argv[2]
ciphertext_out = argv[3]
if exists(salt_out) or exists(ciphertext_out):
raise Exception("salt or ciphertext output file already exists")
with open(salt_out, 'w') as salt_out_file:
salt_out_file.write( full_encrypt[8:16] )
with open(ciphertext_out, 'w') as cipher_out_file:
cipher_out_file.write( full_encrypt[16:] )
_______________________________________________
SkullSpace Discuss Mailing List
Help: http://www.skullspace.ca/wiki/index.php/Mailing_List#Discuss
Archive: https://groups.google.com/group/skullspace-discuss-archive/