crypto.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2020 The MMGen Project <mmgen@tuta.io>
  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 cryptography.hazmat.primitives.ciphers import Cipher,algorithms,modes
  22. from cryptography.hazmat.backends import default_backend
  23. from hashlib import sha256
  24. from .common import *
  25. crmsg = {
  26. 'usr_rand_notice': """
  27. Now we're going to gather some additional input from the keyboard to
  28. further randomize the {d} we've already gathered.
  29. An encryption key will be created from this input, and the {d}
  30. will be encrypted using the key. The resulting data is guaranteed to be at
  31. least as random as the original {d}, so even if you type very
  32. predictably no harm will be done.
  33. However, to gain the maximum benefit, try making your input as random as
  34. possible. Type slowly and choose your symbols carefully. Try to use both
  35. upper and lowercase as well as punctuation and numerals. The timings between
  36. your keystrokes will also be used as a source of entropy, so be as
  37. unpredictable as possible in your timing as well.
  38. Please type {r} symbols on your keyboard. What you type will not be displayed
  39. on the screen.
  40. """
  41. }
  42. def sha256_rounds(s,n):
  43. for i in range(n):
  44. s = sha256(s).digest()
  45. return s
  46. def scramble_seed(seed,scramble_key):
  47. import hmac
  48. step1 = hmac.new(seed,scramble_key,sha256).digest()
  49. if g.debug:
  50. fs = 'Seed: {!r}\nScramble key: {}\nScrambled seed: {}\n'
  51. msg(fs.format(seed.hex(),scramble_key,step1.hex()))
  52. return sha256_rounds(step1,g.scramble_hash_rounds)
  53. def encrypt_seed(seed,key):
  54. return encrypt_data(seed,key,desc='seed')
  55. def decrypt_seed(enc_seed,key,seed_id,key_id):
  56. vmsg_r('Checking key...')
  57. chk1 = make_chksum_8(key)
  58. if key_id:
  59. if not compare_chksums(key_id,'key ID',chk1,'computed'):
  60. msg('Incorrect passphrase or hash preset')
  61. return False
  62. dec_seed = decrypt_data(enc_seed,key,desc='seed')
  63. chk2 = make_chksum_8(dec_seed)
  64. if seed_id:
  65. if compare_chksums(seed_id,'Seed ID',chk2,'decrypted seed'):
  66. qmsg('Passphrase is OK')
  67. else:
  68. if not opt.debug:
  69. msg_r('Checking key ID...')
  70. if compare_chksums(key_id,'key ID',chk1,'computed'):
  71. msg('Key ID is correct but decryption of seed failed')
  72. else:
  73. msg('Incorrect passphrase or hash preset')
  74. vmsg('')
  75. return False
  76. # else:
  77. # qmsg('Generated IDs (Seed/Key): {}/{}'.format(chk2,chk1))
  78. dmsg('Decrypted seed: {}'.format(dec_seed.hex()))
  79. return dec_seed
  80. def encrypt_data(data,key,iv=g.aesctr_dfl_iv,desc='data',verify=True):
  81. vmsg('Encrypting {}'.format(desc))
  82. c = Cipher(algorithms.AES(key),modes.CTR(iv),backend=default_backend())
  83. encryptor = c.encryptor()
  84. enc_data = encryptor.update(data) + encryptor.finalize()
  85. if verify:
  86. vmsg_r('Performing a test decryption of the {}...'.format(desc))
  87. c = Cipher(algorithms.AES(key),modes.CTR(iv),backend=default_backend())
  88. encryptor = c.encryptor()
  89. dec_data = encryptor.update(enc_data) + encryptor.finalize()
  90. if dec_data != data:
  91. die(2,"ERROR.\nDecrypted {s} doesn't match original {s}".format(s=desc))
  92. vmsg('done')
  93. return enc_data
  94. def decrypt_data(enc_data,key,iv=g.aesctr_dfl_iv,desc='data'):
  95. vmsg_r('Decrypting {} with key...'.format(desc))
  96. c = Cipher(algorithms.AES(key),modes.CTR(iv),backend=default_backend())
  97. encryptor = c.encryptor()
  98. return encryptor.update(enc_data) + encryptor.finalize()
  99. def scrypt_hash_passphrase(passwd,salt,hash_preset,buflen=32):
  100. # Buflen arg is for brainwallets only, which use this function to generate
  101. # the seed directly.
  102. N,r,p = get_hash_params(hash_preset)
  103. if isinstance(passwd,str): passwd = passwd.encode()
  104. def do_hashlib_scrypt():
  105. from hashlib import scrypt # Python >= v3.6
  106. return scrypt(passwd,salt=salt,n=2**N,r=r,p=p,maxmem=0,dklen=buflen)
  107. def do_standalone_scrypt():
  108. import scrypt
  109. return scrypt.hash(passwd,salt,2**N,r,p,buflen=buflen)
  110. if int(hash_preset) > 3:
  111. msg_r('Hashing passphrase, please wait...')
  112. # hashlib.scrypt doesn't support N > 14 (hash preset 3)
  113. if N > 14 or g.force_standalone_scrypt_module:
  114. ret = do_standalone_scrypt()
  115. else:
  116. try: ret = do_hashlib_scrypt()
  117. except: ret = do_standalone_scrypt()
  118. if int(hash_preset) > 3:
  119. msg_r('\b'*34 + ' '*34 + '\b'*34)
  120. return ret
  121. def make_key(passwd,salt,hash_preset,desc='encryption key',from_what='passphrase',verbose=False):
  122. if from_what: desc += ' from '
  123. if opt.verbose or verbose:
  124. msg_r('Generating {}{}...'.format(desc,from_what))
  125. key = scrypt_hash_passphrase(passwd,salt,hash_preset)
  126. if opt.verbose or verbose: msg('done')
  127. dmsg('Key: {}'.format(key.hex()))
  128. return key
  129. def _get_random_data_from_user(uchars,desc,test_suite=False):
  130. m = 'Enter {r} random symbols' if opt.quiet or test_suite else crmsg['usr_rand_notice']
  131. msg(m.format(r=uchars,d=desc))
  132. prompt = 'You may begin typing. {} symbols left: '
  133. import time
  134. from .term import get_char_raw
  135. key_data,time_data = '',[]
  136. for i in range(uchars):
  137. key_data += get_char_raw('\r'+prompt.format(uchars-i))
  138. time_data.append(time.time())
  139. msg_r('\r' if opt.quiet else "\rThank you. That's enough.{}\n\n".format(' '*18))
  140. time_data = ['{:.22f}'.format(t).rstrip('0') for t in time_data]
  141. avg_prec = sum(len(t.split('.')[1]) for t in time_data) // len(time_data)
  142. if avg_prec < g.min_time_precision:
  143. m = 'WARNING: Avg. time precision of only {} decimal points. User entropy quality is degraded!'
  144. ymsg(m.format(avg_prec))
  145. ret = key_data + '\n' + '\n'.join(time_data)
  146. if g.debug:
  147. msg('USER ENTROPY (user input + keystroke timings):\n{}'.format(ret))
  148. if not test_suite:
  149. my_raw_input('User random data successfully acquired. Press ENTER to continue: ')
  150. return ret.encode()
  151. def get_random(length):
  152. return add_user_random(os.urandom(length),'OS random data')
  153. def add_user_random(rand_bytes,desc):
  154. assert type(rand_bytes) == bytes, (
  155. "{!r}: invalid type for 'rand_bytes'".format(type(rand_bytes).__name__) )
  156. if opt.usr_randchars:
  157. if not g.user_entropy:
  158. g.user_entropy = \
  159. sha256(_get_random_data_from_user(opt.usr_randchars,desc)).digest()
  160. urand_desc = 'user-supplied entropy'
  161. else:
  162. urand_desc = 'saved user-supplied entropy'
  163. key = make_key(g.user_entropy,b'','2',from_what=urand_desc,verbose=True)
  164. msg('Encrypting {} with key'.format(desc))
  165. return encrypt_data(rand_bytes,key,desc=desc,verify=False)
  166. else:
  167. return rand_bytes
  168. def get_hash_preset_from_user(hp=g.dfl_hash_preset,desc='data'):
  169. prompt = f'Enter hash preset for {desc},\n or hit ENTER to accept the default value ({hp!r}): '
  170. while True:
  171. ret = my_raw_input(prompt)
  172. if ret:
  173. if ret in g.hash_presets:
  174. return ret
  175. else:
  176. msg('Invalid input. Valid choices are {}'.format(', '.join(g.hash_presets)))
  177. continue
  178. else:
  179. return hp
  180. def get_new_passphrase(desc,passchg=False):
  181. w = '{}passphrase for {}'.format(('','new ')[bool(passchg)], desc)
  182. if opt.passwd_file:
  183. pw = ' '.join(get_words_from_file(opt.passwd_file,w))
  184. elif opt.echo_passphrase:
  185. pw = ' '.join(get_words_from_user(f'Enter {w}: '))
  186. else:
  187. for i in range(g.passwd_max_tries):
  188. pw = ' '.join(get_words_from_user(f'Enter {w}: '))
  189. pw_chk = ' '.join(get_words_from_user('Repeat passphrase: '))
  190. dmsg(f'Passphrases: [{pw}] [{pw_chk}]')
  191. if pw == pw_chk:
  192. vmsg('Passphrases match'); break
  193. else: msg('Passphrases do not match. Try again.')
  194. else:
  195. die(2,f'User failed to duplicate passphrase in {g.passwd_max_tries} attempts')
  196. if pw == '':
  197. qmsg('WARNING: Empty passphrase')
  198. return pw
  199. def get_passphrase(desc,passchg=False):
  200. prompt ='Enter {}passphrase for {}: '.format(('','old ')[bool(passchg)],desc)
  201. if opt.passwd_file:
  202. pwfile_reuse_warning(opt.passwd_file)
  203. return ' '.join(get_words_from_file(opt.passwd_file,'passphrase'))
  204. else:
  205. return ' '.join(get_words_from_user(prompt))
  206. _salt_len,_sha256_len,_nonce_len = (32,32,32)
  207. def mmgen_encrypt(data,desc='data',hash_preset=''):
  208. salt = get_random(_salt_len)
  209. iv = get_random(g.aesctr_iv_len)
  210. nonce = get_random(_nonce_len)
  211. hp = hash_preset or opt.hash_preset or get_hash_preset_from_user('3',desc)
  212. m = ('user-requested','default')[hp=='3']
  213. vmsg(f'Encrypting {desc}')
  214. qmsg(f'Using {m} hash preset of {hp!r}')
  215. passwd = get_new_passphrase(desc,{})
  216. key = make_key(passwd,salt,hp)
  217. enc_d = encrypt_data(sha256(nonce+data).digest() + nonce + data, key, iv, desc=desc)
  218. return salt+iv+enc_d
  219. def mmgen_decrypt(data,desc='data',hash_preset=''):
  220. vmsg(f'Preparing to decrypt {desc}')
  221. dstart = _salt_len + g.aesctr_iv_len
  222. salt = data[:_salt_len]
  223. iv = data[_salt_len:dstart]
  224. enc_d = data[dstart:]
  225. hp = hash_preset or opt.hash_preset or get_hash_preset_from_user('3',desc)
  226. m = ('user-requested','default')[hp=='3']
  227. qmsg(f'Using {m} hash preset of {hp!r}')
  228. passwd = get_passphrase(desc)
  229. key = make_key(passwd,salt,hp)
  230. dec_d = decrypt_data(enc_d,key,iv,desc)
  231. if dec_d[:_sha256_len] == sha256(dec_d[_sha256_len:]).digest():
  232. vmsg('OK')
  233. return dec_d[_sha256_len+_nonce_len:]
  234. else:
  235. msg('Incorrect passphrase or hash preset')
  236. return False
  237. def mmgen_decrypt_retry(d,desc='data'):
  238. while True:
  239. d_dec = mmgen_decrypt(d,desc)
  240. if d_dec: return d_dec
  241. msg('Trying again...')