crypto.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2022 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: Random number, password hashing and symmetric encryption routines for the MMGen suite
  20. """
  21. import os
  22. from collections import namedtuple
  23. from .globalvars import g
  24. from .opts import opt
  25. from .util import (
  26. msg,
  27. msg_r,
  28. dmsg,
  29. vmsg,
  30. vmsg_r,
  31. qmsg,
  32. fmt,
  33. die,
  34. line_input,
  35. get_words_from_user,
  36. make_chksum_8,
  37. compare_chksums,
  38. pwfile_reuse_warning,
  39. )
  40. mmenc_ext = 'mmenc'
  41. scramble_hash_rounds = 10
  42. salt_len = 16
  43. aesctr_iv_len = 16
  44. aesctr_dfl_iv = int.to_bytes(1,aesctr_iv_len,'big')
  45. hincog_chk_len = 8
  46. # Scrypt params: 'id_num': [N, r, p] (N is an exponent of two)
  47. # NB: hashlib.scrypt in Python (>=v3.6) supports max N value of 14. This means that
  48. # for hash presets > 3 the standalone scrypt library must be used!
  49. _hp = namedtuple('scrypt_preset',['N','r','p'])
  50. hash_presets = {
  51. '1': _hp(12, 8, 1),
  52. '2': _hp(13, 8, 4),
  53. '3': _hp(14, 8, 8),
  54. '4': _hp(15, 8, 12),
  55. '5': _hp(16, 8, 16),
  56. '6': _hp(17, 8, 20),
  57. '7': _hp(18, 8, 24),
  58. }
  59. def get_hash_params(hash_preset):
  60. if hash_preset in hash_presets:
  61. return hash_presets[hash_preset] # N,r,p
  62. else: # Shouldn't be here
  63. die(3,f"{hash_preset}: invalid 'hash_preset' value")
  64. def sha256_rounds(s,n):
  65. from hashlib import sha256
  66. for i in range(n):
  67. s = sha256(s).digest()
  68. return s
  69. def scramble_seed(seed,scramble_key):
  70. import hmac
  71. step1 = hmac.digest(seed,scramble_key,'sha256')
  72. if g.debug:
  73. msg(f'Seed: {seed.hex()!r}\nScramble key: {scramble_key}\nScrambled seed: {step1.hex()}\n')
  74. return sha256_rounds( step1, scramble_hash_rounds )
  75. def encrypt_seed(data,key,desc='seed'):
  76. return encrypt_data(data,key,desc=desc)
  77. def decrypt_seed(enc_seed,key,seed_id,key_id):
  78. vmsg_r('Checking key...')
  79. chk1 = make_chksum_8(key)
  80. if key_id:
  81. if not compare_chksums(key_id,'key ID',chk1,'computed'):
  82. msg('Incorrect passphrase or hash preset')
  83. return False
  84. dec_seed = decrypt_data(enc_seed,key,desc='seed')
  85. chk2 = make_chksum_8(dec_seed)
  86. if seed_id:
  87. if compare_chksums(seed_id,'Seed ID',chk2,'decrypted seed'):
  88. qmsg('Passphrase is OK')
  89. else:
  90. if not opt.debug:
  91. msg_r('Checking key ID...')
  92. if compare_chksums(key_id,'key ID',chk1,'computed'):
  93. msg('Key ID is correct but decryption of seed failed')
  94. else:
  95. msg('Incorrect passphrase or hash preset')
  96. vmsg('')
  97. return False
  98. # else:
  99. # qmsg(f'Generated IDs (Seed/Key): {chk2}/{chk1}')
  100. dmsg(f'Decrypted seed: {dec_seed.hex()}')
  101. return dec_seed
  102. def encrypt_data(data,key,iv=aesctr_dfl_iv,desc='data',verify=True,silent=False):
  103. from cryptography.hazmat.primitives.ciphers import Cipher,algorithms,modes
  104. from cryptography.hazmat.backends import default_backend
  105. if not silent:
  106. vmsg(f'Encrypting {desc}')
  107. c = Cipher(algorithms.AES(key),modes.CTR(iv),backend=default_backend())
  108. encryptor = c.encryptor()
  109. enc_data = encryptor.update(data) + encryptor.finalize()
  110. if verify:
  111. vmsg_r(f'Performing a test decryption of the {desc}...')
  112. c = Cipher(algorithms.AES(key),modes.CTR(iv),backend=default_backend())
  113. encryptor = c.encryptor()
  114. dec_data = encryptor.update(enc_data) + encryptor.finalize()
  115. if dec_data != data:
  116. die(2,f'ERROR.\nDecrypted {desc} doesn’t match original {desc}')
  117. if not silent:
  118. vmsg('done')
  119. return enc_data
  120. def decrypt_data(enc_data,key,iv=aesctr_dfl_iv,desc='data'):
  121. from cryptography.hazmat.primitives.ciphers import Cipher,algorithms,modes
  122. from cryptography.hazmat.backends import default_backend
  123. vmsg_r(f'Decrypting {desc} with key...')
  124. c = Cipher(algorithms.AES(key),modes.CTR(iv),backend=default_backend())
  125. encryptor = c.encryptor()
  126. return encryptor.update(enc_data) + encryptor.finalize()
  127. def scrypt_hash_passphrase(passwd,salt,hash_preset,buflen=32):
  128. # Buflen arg is for brainwallets only, which use this function to generate
  129. # the seed directly.
  130. ps = get_hash_params(hash_preset)
  131. if isinstance(passwd,str):
  132. passwd = passwd.encode()
  133. def do_hashlib_scrypt():
  134. from hashlib import scrypt
  135. return scrypt(
  136. password = passwd,
  137. salt = salt,
  138. n = 2**ps.N,
  139. r = ps.r,
  140. p = ps.p,
  141. maxmem = 0,
  142. dklen = buflen )
  143. def do_standalone_scrypt():
  144. import scrypt
  145. return scrypt.hash(
  146. password = passwd,
  147. salt = salt,
  148. N = 2**ps.N,
  149. r = ps.r,
  150. p = ps.p,
  151. buflen = buflen )
  152. if int(hash_preset) > 3:
  153. msg_r('Hashing passphrase, please wait...')
  154. # hashlib.scrypt doesn't support N > 14 (hash preset > 3)
  155. ret = (
  156. do_standalone_scrypt() if ps.N > 14 or g.force_standalone_scrypt_module else
  157. do_hashlib_scrypt() )
  158. if int(hash_preset) > 3:
  159. msg_r('\b'*34 + ' '*34 + '\b'*34)
  160. return ret
  161. def make_key(passwd,salt,hash_preset,desc='encryption key',from_what='passphrase',verbose=False):
  162. if opt.verbose or verbose:
  163. msg_r(f"Generating {desc}{' from ' + from_what if from_what else ''}...")
  164. key = scrypt_hash_passphrase(passwd,salt,hash_preset)
  165. if opt.verbose or verbose: msg('done')
  166. dmsg(f'Key: {key.hex()}')
  167. return key
  168. def _get_random_data_from_user(uchars,desc):
  169. info1 = f"""
  170. Now we're going to gather some additional input from the keyboard to further
  171. randomize the random data {desc}.
  172. An encryption key will be created from this input, and the random data will
  173. be encrypted using the key. The resulting data is guaranteed to be at least
  174. as random as the original random data, so even if you type very predictably
  175. no harm will be done.
  176. However, to gain the maximum benefit, try making your input as random as
  177. possible. Type slowly and choose your symbols carefully. Try to use both
  178. upper and lowercase letters as well as punctuation and numerals. The timings
  179. between your keystrokes will also be used as a source of entropy, so be as
  180. random as possible in your timing as well.
  181. """
  182. info2 = f"""
  183. Please type {uchars} symbols on your keyboard. What you type will not be displayed
  184. on the screen.
  185. """
  186. msg(f'Enter {uchars} random symbols' if opt.quiet else
  187. '\n' + fmt(info1,indent=' ') +
  188. '\n' + fmt(info2) )
  189. import time
  190. from .term import get_char_raw
  191. key_data = ''
  192. time_data = []
  193. for i in range(uchars):
  194. key_data += get_char_raw(f'\rYou may begin typing. {uchars-i} symbols left: ')
  195. time_data.append(time.time())
  196. msg_r( '\r' if opt.quiet else f'\rThank you. That’s enough.{" "*18}\n\n' )
  197. time_data = [f'{t:.22f}'.rstrip('0') for t in time_data]
  198. avg_prec = sum(len(t.split('.')[1]) for t in time_data) // len(time_data)
  199. if avg_prec < g.min_time_precision:
  200. ymsg(f'WARNING: Avg. time precision of only {avg_prec} decimal points. User entropy quality is degraded!')
  201. ret = key_data + '\n' + '\n'.join(time_data)
  202. if g.debug:
  203. msg(f'USER ENTROPY (user input + keystroke timings):\n{ret}')
  204. line_input('User random data successfully acquired. Press ENTER to continue: ')
  205. return ret.encode()
  206. def get_random(length):
  207. os_rand = os.urandom(length)
  208. assert len(os_rand) == length, f'OS random number generator returned {len(os_rand)} (!= {length}) bytes!'
  209. return add_user_random(
  210. rand_bytes = os_rand,
  211. desc = 'from your operating system' )
  212. def add_user_random(
  213. rand_bytes,
  214. desc,
  215. urand = {'data':b'', 'counter':0} ):
  216. assert type(rand_bytes) == bytes, 'add_user_random_chk1'
  217. if opt.usr_randchars:
  218. if not urand['data']:
  219. from hashlib import sha256
  220. urand['data'] = sha256(_get_random_data_from_user(opt.usr_randchars,desc)).digest()
  221. # counter protects against very evil rng that might repeatedly output the same data
  222. urand['counter'] += 1
  223. os_rand = os.urandom(8)
  224. assert len(os_rand) == 8, f'OS random number generator returned {len(os_rand)} (!= 8) bytes!'
  225. import hmac
  226. key = hmac.digest(
  227. urand['data'],
  228. os_rand + int.to_bytes(urand['counter'],8,'big'),
  229. 'sha256' )
  230. msg('Encrypting random data {} with ephemeral key #{}'.format( desc, urand['counter'] ))
  231. return encrypt_data( data=rand_bytes, key=key, desc=desc, verify=False, silent=True )
  232. else:
  233. return rand_bytes
  234. def get_hash_preset_from_user(
  235. hash_preset = g.dfl_hash_preset,
  236. data_desc = 'data',
  237. prompt = None ):
  238. prompt = prompt or (
  239. f'Enter hash preset for {data_desc},\n' +
  240. f'or hit ENTER to accept the default value ({hash_preset!r}): ' )
  241. while True:
  242. ret = line_input(prompt)
  243. if ret:
  244. if ret in hash_presets:
  245. return ret
  246. else:
  247. msg('Invalid input. Valid choices are {}'.format(', '.join(hash_presets)))
  248. else:
  249. return hash_preset
  250. def get_new_passphrase(data_desc,hash_preset,passwd_file,pw_desc='passphrase'):
  251. message = f"""
  252. You must choose a passphrase to encrypt your {data_desc} with.
  253. A key will be generated from your passphrase using a hash preset of '{hash_preset}'.
  254. Please note that no strength checking of passphrases is performed.
  255. For an empty passphrase, just hit ENTER twice.
  256. """
  257. if passwd_file:
  258. from .fileutil import get_words_from_file
  259. pw = ' '.join(get_words_from_file(
  260. infile = passwd_file,
  261. desc = f'{pw_desc} for {data_desc}',
  262. quiet = pwfile_reuse_warning(passwd_file).warning_shown ))
  263. else:
  264. qmsg('\n'+fmt(message,indent=' '))
  265. if opt.echo_passphrase:
  266. pw = ' '.join(get_words_from_user(f'Enter {pw_desc} for {data_desc}: '))
  267. else:
  268. for i in range(g.passwd_max_tries):
  269. pw = ' '.join(get_words_from_user(f'Enter {pw_desc} for {data_desc}: '))
  270. pw_chk = ' '.join(get_words_from_user(f'Repeat {pw_desc}: '))
  271. dmsg(f'Passphrases: [{pw}] [{pw_chk}]')
  272. if pw == pw_chk:
  273. vmsg('Passphrases match')
  274. break
  275. else:
  276. msg('Passphrases do not match. Try again.')
  277. else:
  278. die(2,f'User failed to duplicate passphrase in {g.passwd_max_tries} attempts')
  279. if pw == '':
  280. qmsg('WARNING: Empty passphrase')
  281. return pw
  282. def get_passphrase(data_desc,passwd_file,pw_desc='passphrase'):
  283. if passwd_file:
  284. from .fileutil import get_words_from_file
  285. return ' '.join(get_words_from_file(
  286. infile = passwd_file,
  287. desc = f'{pw_desc} for {data_desc}',
  288. quiet = pwfile_reuse_warning(passwd_file).warning_shown ))
  289. else:
  290. return ' '.join(get_words_from_user(f'Enter {pw_desc} for {data_desc}: '))
  291. mmenc_salt_len = 32
  292. mmenc_nonce_len = 32
  293. def mmgen_encrypt(data,desc='data',hash_preset=None):
  294. salt = get_random(mmenc_salt_len)
  295. iv = get_random(aesctr_iv_len)
  296. nonce = get_random(mmenc_nonce_len)
  297. hp = hash_preset or opt.hash_preset or get_hash_preset_from_user(data_desc=desc)
  298. m = ('user-requested','default')[hp=='3']
  299. vmsg(f'Encrypting {desc}')
  300. qmsg(f'Using {m} hash preset of {hp!r}')
  301. passwd = get_new_passphrase(
  302. data_desc = desc,
  303. hash_preset = hp,
  304. passwd_file = opt.passwd_file )
  305. key = make_key(passwd,salt,hp)
  306. from hashlib import sha256
  307. enc_d = encrypt_data( sha256(nonce+data).digest() + nonce + data, key, iv, desc=desc )
  308. return salt+iv+enc_d
  309. def mmgen_decrypt(data,desc='data',hash_preset=None):
  310. vmsg(f'Preparing to decrypt {desc}')
  311. dstart = mmenc_salt_len + aesctr_iv_len
  312. salt = data[:mmenc_salt_len]
  313. iv = data[mmenc_salt_len:dstart]
  314. enc_d = data[dstart:]
  315. hp = hash_preset or opt.hash_preset or get_hash_preset_from_user(data_desc=desc)
  316. m = ('user-requested','default')[hp=='3']
  317. qmsg(f'Using {m} hash preset of {hp!r}')
  318. passwd = get_passphrase(
  319. data_desc = desc,
  320. passwd_file = opt.passwd_file )
  321. key = make_key(passwd,salt,hp)
  322. dec_d = decrypt_data( enc_d, key, iv, desc )
  323. sha256_len = 32
  324. from hashlib import sha256
  325. if dec_d[:sha256_len] == sha256(dec_d[sha256_len:]).digest():
  326. vmsg('OK')
  327. return dec_d[sha256_len+mmenc_nonce_len:]
  328. else:
  329. msg('Incorrect passphrase or hash preset')
  330. return False
  331. def mmgen_decrypt_retry(d,desc='data'):
  332. while True:
  333. d_dec = mmgen_decrypt(d,desc)
  334. if d_dec: return d_dec
  335. msg('Trying again...')