crypto.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. #!/usr/bin/env python
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2017 Philemon <mmgen-py@yandex.com>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. crypto.py: Cryptographic and related routines for the MMGen suite
  20. """
  21. from binascii import hexlify
  22. from hashlib import sha256
  23. from mmgen.common import *
  24. crmsg = {
  25. 'usr_rand_notice': """
  26. Since we don't fully trust our OS's random number generator, we'll provide
  27. some additional entropy of our own. Please type %s symbols on your keyboard.
  28. Type slowly and choose your symbols carefully for maximum randomness. Try to
  29. use both upper and lowercase as well as punctuation and numerals. What you
  30. type will not be displayed on the screen. Note that the timings between your
  31. keystrokes will also be used as a source of randomness.
  32. """,
  33. # 'incog_iv_id': """
  34. # Check that the generated Incog ID above is correct.
  35. # If it's not, then your incognito data is incorrect or corrupted.
  36. # """,
  37. # 'incog_iv_id_hidden': """
  38. # Check that the generated Incog ID above is correct.
  39. # If it's not, then your incognito data is incorrect or corrupted,
  40. # or you've supplied an incorrect offset.
  41. # """,
  42. # 'incorrect_incog_passphrase_try_again': """
  43. # Incorrect passphrase, hash preset, or maybe old-format incog wallet.
  44. # Try again? (Y)es, (n)o, (m)ore information:
  45. # """.strip(),
  46. # 'confirm_seed_id': """
  47. # If the Seed ID above is correct but you're seeing this message, then you need
  48. # to exit and re-run the program with the '--old-incog-fmt' option.
  49. # """.strip(),
  50. }
  51. def sha256_rounds(s,n):
  52. assert is_int(n) and n > 0
  53. for i in range(n):
  54. s = sha256(s).digest()
  55. return s
  56. def encrypt_seed(seed, key):
  57. return encrypt_data(seed, key, iv=1, desc='seed')
  58. def decrypt_seed(enc_seed, key, seed_id, key_id):
  59. vmsg_r('Checking key...')
  60. chk1 = make_chksum_8(key)
  61. if key_id:
  62. if not compare_chksums(key_id,'key ID',chk1,'computed'):
  63. msg('Incorrect passphrase or hash preset')
  64. return False
  65. dec_seed = decrypt_data(enc_seed, key, iv=1, desc='seed')
  66. chk2 = make_chksum_8(dec_seed)
  67. if seed_id:
  68. if compare_chksums(seed_id,'Seed ID',chk2,'decrypted seed'):
  69. qmsg('Passphrase is OK')
  70. else:
  71. if not opt.debug:
  72. msg_r('Checking key ID...')
  73. if compare_chksums(key_id,'key ID',chk1,'computed'):
  74. msg('Key ID is correct but decryption of seed failed')
  75. else:
  76. msg('Incorrect passphrase or hash preset')
  77. vmsg('')
  78. return False
  79. # else:
  80. # qmsg('Generated IDs (Seed/Key): %s/%s' % (chk2,chk1))
  81. dmsg('Decrypted seed: %s' % hexlify(dec_seed))
  82. return dec_seed
  83. def encrypt_data(data, key, iv=1, desc='data', verify=True):
  84. # 192-bit seed is 24 bytes -> not multiple of 16. Must use MODE_CTR
  85. from Crypto.Cipher import AES
  86. from Crypto.Util import Counter
  87. vmsg('Encrypting %s' % desc)
  88. c = AES.new(key, AES.MODE_CTR,
  89. counter=Counter.new(g.aesctr_iv_len*8,initial_value=iv))
  90. enc_data = c.encrypt(data)
  91. if verify:
  92. vmsg_r('Performing a test decryption of the %s...' % desc)
  93. c = AES.new(key, AES.MODE_CTR,
  94. counter=Counter.new(g.aesctr_iv_len*8,initial_value=iv))
  95. dec_data = c.decrypt(enc_data)
  96. if dec_data == data: vmsg('done')
  97. else:
  98. die(2,"ERROR.\nDecrypted %s doesn't match original %s" % (desc,desc))
  99. return enc_data
  100. def decrypt_data(enc_data, key, iv=1, desc='data'):
  101. vmsg_r('Decrypting %s with key...' % desc)
  102. from Crypto.Cipher import AES
  103. from Crypto.Util import Counter
  104. c = AES.new(key, AES.MODE_CTR,
  105. counter=Counter.new(g.aesctr_iv_len*8,initial_value=iv))
  106. return c.decrypt(enc_data)
  107. def scrypt_hash_passphrase(passwd, salt, hash_preset, buflen=32):
  108. # Buflen arg is for brainwallets only, which use this function to generate
  109. # the seed directly.
  110. N,r,p = get_hash_params(hash_preset)
  111. import scrypt
  112. return scrypt.hash(passwd, salt, 2**N, r, p, buflen=buflen)
  113. def make_key(passwd,salt,hash_preset,
  114. desc='encryption key',from_what='passphrase',verbose=False):
  115. if from_what: desc += ' from '
  116. if opt.verbose or verbose:
  117. msg_r('Generating %s%s...' % (desc,from_what))
  118. key = scrypt_hash_passphrase(passwd, salt, hash_preset)
  119. if opt.verbose or verbose: msg('done')
  120. dmsg('Key: %s' % hexlify(key))
  121. return key
  122. def _get_random_data_from_user(uchars):
  123. if opt.quiet: msg('Enter %s random symbols' % uchars)
  124. else: msg(crmsg['usr_rand_notice'] % uchars)
  125. prompt = 'You may begin typing. %s symbols left: '
  126. msg_r(prompt % uchars)
  127. import time
  128. from mmgen.term import get_char
  129. # time.clock() always returns zero, so we'll use time.time()
  130. saved_time = time.time()
  131. key_data,time_data,pp = '',[],True
  132. for i in range(uchars):
  133. key_data += get_char(immed_chars='ALL',prehold_protect=pp)
  134. pp = False
  135. msg_r('\r' + prompt % (uchars - i - 1))
  136. now = time.time()
  137. time_data.append(now - saved_time)
  138. saved_time = now
  139. if opt.quiet: msg_r('\r')
  140. else: msg_r("\rThank you. That's enough.%s\n\n" % (' '*18))
  141. fmt_time_data = ['{:.22f}'.format(i) for i in time_data]
  142. dmsg('\nUser input:\n%s\nKeystroke time intervals:\n%s\n' %
  143. (key_data,'\n'.join(fmt_time_data)))
  144. prompt = 'User random data successfully acquired. Press ENTER to continue'
  145. prompt_and_get_char(prompt,'',enter_ok=True)
  146. return key_data+''.join(fmt_time_data)
  147. def get_random(length):
  148. from Crypto import Random
  149. os_rand = Random.new().read(length)
  150. if opt.usr_randchars:
  151. from_what = 'OS random data'
  152. if not g.user_entropy:
  153. g.user_entropy = \
  154. sha256(_get_random_data_from_user(opt.usr_randchars)).digest()
  155. from_what += ' plus user-supplied entropy'
  156. else:
  157. from_what += ' plus saved user-supplied entropy'
  158. key = make_key(g.user_entropy, '', '2', from_what=from_what, verbose=True)
  159. return encrypt_data(os_rand,key,desc='random data',verify=False)
  160. else:
  161. return os_rand
  162. def get_hash_preset_from_user(hp=g.hash_preset,desc='data'):
  163. p = """Enter hash preset for %s,
  164. or hit ENTER to accept the default value ('%s'): """ % (desc,hp)
  165. while True:
  166. ret = my_raw_input(p)
  167. if ret:
  168. if ret in g.hash_presets.keys(): return ret
  169. else:
  170. msg('Invalid input. Valid choices are %s' %
  171. ', '.join(sorted(g.hash_presets.keys())))
  172. continue
  173. else: return hp
  174. # Vars for mmgen_*crypt functions only
  175. salt_len,sha256_len,nonce_len = 32,32,32
  176. def mmgen_encrypt(data,desc='data',hash_preset=''):
  177. salt,iv,nonce = get_random(salt_len),\
  178. get_random(g.aesctr_iv_len), \
  179. get_random(nonce_len)
  180. hp = hash_preset or get_hash_preset_from_user('3',desc)
  181. m = ('user-requested','default')[hp=='3']
  182. vmsg('Encrypting %s' % desc)
  183. qmsg("Using %s hash preset of '%s'" % (m,hp))
  184. passwd = get_new_passphrase(desc, {})
  185. key = make_key(passwd, salt, hp)
  186. enc_d = encrypt_data(sha256(nonce+data).digest() + nonce + data, key,
  187. int(hexlify(iv),16), desc=desc)
  188. return salt+iv+enc_d
  189. def mmgen_decrypt(data,desc='data',hash_preset=''):
  190. dstart = salt_len + g.aesctr_iv_len
  191. salt,iv,enc_d = data[:salt_len],data[salt_len:dstart],data[dstart:]
  192. vmsg('Preparing to decrypt %s' % desc)
  193. hp = hash_preset or get_hash_preset_from_user('3',desc)
  194. m = ('user-requested','default')[hp=='3']
  195. qmsg("Using %s hash preset of '%s'" % (m,hp))
  196. passwd = get_mmgen_passphrase(desc)
  197. key = make_key(passwd,salt,hp)
  198. dec_d = decrypt_data(enc_d, key, int(hexlify(iv),16), desc)
  199. if dec_d[:sha256_len] == sha256(dec_d[sha256_len:]).digest():
  200. vmsg('OK')
  201. return dec_d[sha256_len+nonce_len:]
  202. else:
  203. msg('Incorrect passphrase or hash preset')
  204. return False
  205. def mmgen_decrypt_retry(d,desc='data'):
  206. while True:
  207. d_dec = mmgen_decrypt(d,desc)
  208. if d_dec: return d_dec
  209. msg('Trying again...')