crypto.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. #!/usr/bin/env python
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2014 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-tool' utility
  20. """
  21. import sys
  22. from binascii import hexlify
  23. from hashlib import sha256
  24. import mmgen.config as g
  25. from mmgen.util import *
  26. from mmgen.term import get_char
  27. crmsg = {
  28. 'incog_iv_id': """
  29. Check that the generated Incog ID above is correct.
  30. If it's not, then your incognito data is incorrect or corrupted.
  31. """,
  32. 'incog_iv_id_hidden': """
  33. Check that the generated Incog ID above is correct.
  34. If it's not, then your incognito data is incorrect or corrupted,
  35. or you've supplied an incorrect offset.
  36. """,
  37. 'usr_rand_notice': """
  38. You've chosen to not fully trust your OS's random number generator and provide
  39. some additional entropy of your own. Please type %s symbols on your keyboard.
  40. Type slowly and choose your symbols carefully for maximum randomness. Try to
  41. use both upper and lowercase as well as punctuation and numerals. What you
  42. type will not be displayed on the screen. Note that the timings between your
  43. keystrokes will also be used as a source of randomness.
  44. """,
  45. 'incorrect_incog_passphrase_try_again': """
  46. Incorrect passphrase, hash preset, or maybe old-format incog wallet.
  47. Try again? (Y)es, (n)o, (m)ore information:
  48. """.strip(),
  49. 'confirm_seed_id': """
  50. If the seed ID above is correct but you're seeing this message, then you need
  51. to exit and re-run the program with the '--old-incog-fmt' option.
  52. """.strip(),
  53. }
  54. def encrypt_seed(seed, key):
  55. return encrypt_data(seed, key, iv=1, what="seed")
  56. def decrypt_seed(enc_seed, key, seed_id, key_id):
  57. vmsg_r("Checking key...")
  58. chk1 = make_chksum_8(key)
  59. if key_id:
  60. if not compare_checksums(chk1, "of key", key_id, "in header"):
  61. msg("Incorrect passphrase")
  62. return False
  63. dec_seed = decrypt_data(enc_seed, key, iv=1, what="seed")
  64. chk2 = make_chksum_8(dec_seed)
  65. if seed_id:
  66. if compare_checksums(chk2,"of decrypted seed",seed_id,"in header"):
  67. qmsg("Passphrase is OK")
  68. else:
  69. if not g.debug:
  70. msg_r("Checking key ID...")
  71. if compare_checksums(chk1, "of key", key_id, "in header"):
  72. msg("Key ID is correct but decryption of seed failed")
  73. else:
  74. msg("Incorrect passphrase")
  75. vmsg("")
  76. return False
  77. # else:
  78. # qmsg("Generated IDs (Seed/Key): %s/%s" % (chk2,chk1))
  79. if g.debug: print "Decrypted seed: %s" % hexlify(dec_seed)
  80. vmsg("OK")
  81. return dec_seed
  82. def encrypt_data(data, key, iv=1, what="data", verify=True):
  83. """
  84. Encrypt arbitrary data using AES256 in counter mode
  85. """
  86. # 192-bit seed is 24 bytes -> not multiple of 16. Must use MODE_CTR
  87. from Crypto.Cipher import AES
  88. from Crypto.Util import Counter
  89. vmsg("Encrypting %s" % what)
  90. c = AES.new(key, AES.MODE_CTR,
  91. counter=Counter.new(g.aesctr_iv_len*8,initial_value=iv))
  92. enc_data = c.encrypt(data)
  93. if verify:
  94. vmsg_r("Performing a test decryption of the %s..." % what)
  95. c = AES.new(key, AES.MODE_CTR,
  96. counter=Counter.new(g.aesctr_iv_len*8,initial_value=iv))
  97. dec_data = c.decrypt(enc_data)
  98. if dec_data == data: vmsg("done\n")
  99. else:
  100. msg("ERROR.\nDecrypted %s doesn't match original %s" % (what,what))
  101. sys.exit(2)
  102. return enc_data
  103. def decrypt_data(enc_data, key, iv=1, what="data"):
  104. vmsg_r("Decrypting %s with key..." % what)
  105. from Crypto.Cipher import AES
  106. from Crypto.Util import Counter
  107. c = AES.new(key, AES.MODE_CTR,
  108. counter=Counter.new(g.aesctr_iv_len*8,initial_value=iv))
  109. return c.decrypt(enc_data)
  110. def scrypt_hash_passphrase(passwd, salt, hash_preset, buflen=32):
  111. # Buflen arg is for brainwallets only, which use this function to generate
  112. # the seed directly.
  113. N,r,p = get_hash_params(hash_preset)
  114. import scrypt
  115. return scrypt.hash(passwd, salt, 2**N, r, p, buflen=buflen)
  116. def make_key(passwd, salt, hash_preset, what="encryption key", verbose=False):
  117. if g.verbose or verbose:
  118. msg_r("Generating %s from passphrase.\nPlease wait..." % what)
  119. key = scrypt_hash_passphrase(passwd, salt, hash_preset)
  120. if g.verbose or verbose:
  121. msg("done")
  122. if g.debug: print "Key: %s" % hexlify(key)
  123. return key
  124. def get_random_data_from_user(uchars):
  125. if g.quiet: msg("Enter %s random symbols" % uchars)
  126. else: msg(crmsg['usr_rand_notice'] % uchars)
  127. prompt = "You may begin typing. %s symbols left: "
  128. msg_r(prompt % uchars)
  129. import time
  130. # time.clock() always returns zero, so we'll use time.time()
  131. saved_time = time.time()
  132. key_data,time_data,pp = "",[],True
  133. for i in range(uchars):
  134. key_data += get_char(immed_chars="ALL",prehold_protect=pp)
  135. pp = False
  136. msg_r("\r" + prompt % (uchars - i - 1))
  137. now = time.time()
  138. time_data.append(now - saved_time)
  139. saved_time = now
  140. if g.quiet: msg_r("\r")
  141. else: msg_r("\rThank you. That's enough.%s\n\n" % (" "*18))
  142. fmt_time_data = ["{:.22f}".format(i) for i in time_data]
  143. if g.debug:
  144. msg("\nUser input:\n%s\nKeystroke time intervals:\n%s\n" %
  145. (key_data,"\n".join(fmt_time_data)))
  146. prompt = "User random data successfully acquired. Press ENTER to continue"
  147. prompt_and_get_char(prompt,"",enter_ok=True)
  148. return key_data+"".join(fmt_time_data)
  149. def get_random(length,opts):
  150. from Crypto import Random
  151. os_rand = Random.new().read(length)
  152. if 'usr_randchars' in opts and opts['usr_randchars'] not in (0,-1):
  153. kwhat = "a key from OS random data plus "
  154. if not g.user_entropy:
  155. g.user_entropy = sha256(
  156. get_random_data_from_user(opts['usr_randchars'])).digest()
  157. kwhat += "user entropy"
  158. else:
  159. kwhat += "saved user entropy"
  160. key = make_key(g.user_entropy, "", '2', what=kwhat, verbose=True)
  161. return encrypt_data(os_rand,key,what="random data",verify=False)
  162. else:
  163. return os_rand
  164. def get_seed_from_wallet(
  165. infile,
  166. opts,
  167. prompt_info="{} wallet".format(g.proj_name),
  168. silent=False
  169. ):
  170. wdata = get_data_from_wallet(infile,silent=silent)
  171. label,metadata,hash_preset,salt,enc_seed = wdata
  172. if g.debug: display_control_data(*wdata)
  173. padd = " "+infile if g.quiet else ""
  174. passwd = get_mmgen_passphrase(prompt_info+padd,opts)
  175. key = make_key(passwd, salt, hash_preset)
  176. return decrypt_seed(enc_seed, key, metadata[0], metadata[1])
  177. def get_hidden_incog_data(opts):
  178. # Already sanity-checked:
  179. fname,offset,seed_len = opts['from_incog_hidden'].split(",")
  180. qmsg("Getting hidden incog data from file '%s'" % fname)
  181. z = 0 if 'old_incog_fmt' in opts else 8
  182. dlen = g.aesctr_iv_len + g.salt_len + (int(seed_len)/8) + z
  183. fsize = check_data_fits_file_at_offset(fname,int(offset),dlen,"read")
  184. import os
  185. f = os.open(fname,os.O_RDONLY)
  186. os.lseek(f, int(offset), os.SEEK_SET)
  187. data = os.read(f, dlen)
  188. os.close(f)
  189. qmsg("Data read from file '%s' at offset %s" % (fname,offset),
  190. "Data read from file")
  191. return data
  192. def confirm_old_format():
  193. while True:
  194. reply = get_char(
  195. crmsg['incorrect_incog_passphrase_try_again']+" ").strip("\n\r")
  196. if not reply: msg(""); return False
  197. elif reply in 'yY': msg(""); return False
  198. elif reply in 'nN': msg("\nExiting at user request"); sys.exit(1)
  199. elif reply in 'mM': msg(""); return True
  200. else:
  201. if g.verbose: msg("\nInvalid reply")
  202. else: msg_r("\r")
  203. def get_seed_from_incog_wallet(
  204. infile,
  205. opts,
  206. prompt_info="{} incognito wallet".format(g.proj_name),
  207. silent=False,
  208. hex_input=False
  209. ):
  210. what = "incognito wallet data"
  211. if "from_incog_hidden" in opts:
  212. d = get_hidden_incog_data(opts)
  213. else:
  214. d = get_data_from_file(infile,what)
  215. if hex_input:
  216. try:
  217. d = unhexlify("".join(d.split()).strip())
  218. except:
  219. msg("Data in file '%s' is not in hexadecimal format" % infile)
  220. sys.exit(2)
  221. # File could be of invalid length, so check:
  222. z = 0 if 'old_incog_fmt' in opts else 8
  223. valid_dlens = [i/8 + g.aesctr_iv_len + g.salt_len + z for i in g.seed_lens]
  224. # New fmt: [56, 64, 72]. Old fmt: [48, 56, 64].
  225. if len(d) not in valid_dlens:
  226. vn = [i/8 + g.aesctr_iv_len + g.salt_len + 8 for i in g.seed_lens]
  227. if len(d) in vn:
  228. msg("Re-run the program without the '--old-incog-fmt' option")
  229. sys.exit()
  230. else: qmsg(
  231. "Invalid incognito file size: %s. Valid sizes (in bytes): %s" %
  232. (len(d), " ".join([str(i) for i in valid_dlens])))
  233. return False
  234. iv, enc_incog_data = d[0:g.aesctr_iv_len], d[g.aesctr_iv_len:]
  235. incog_id = make_iv_chksum(iv)
  236. msg("Incog ID: %s (IV ID: %s)" % (incog_id,make_chksum_8(iv)))
  237. qmsg("Check the applicable value against your records.")
  238. vmsg(crmsg['incog_iv_id_hidden' if "from_incog_hidden" in opts
  239. else 'incog_iv_id'])
  240. while True:
  241. passwd = get_mmgen_passphrase(prompt_info+" "+incog_id,opts)
  242. qmsg("Configured hash presets: %s" % " ".join(sorted(g.hash_presets)))
  243. hp = get_hash_preset_from_user(what="incog wallet")
  244. # IV is used BOTH to initialize counter and to salt password!
  245. key = make_key(passwd, iv, hp, "wrapper key")
  246. d = decrypt_data(enc_incog_data, key, int(hexlify(iv),16), "incog data")
  247. salt,enc_seed = d[0:g.salt_len], d[g.salt_len:]
  248. key = make_key(passwd, salt, hp, "main key")
  249. vmsg("Key ID: %s" % make_chksum_8(key))
  250. seed = decrypt_seed(enc_seed, key, "", "")
  251. old_fmt_sid = make_chksum_8(seed)
  252. def confirm_correct_seed_id(sid):
  253. m = "Seed ID: %s. Is the Seed ID correct?" % sid
  254. return keypress_confirm(m, True)
  255. if 'old_incog_fmt' in opts:
  256. if confirm_correct_seed_id(old_fmt_sid):
  257. break
  258. else:
  259. chk,seed_maybe = seed[:8],seed[8:]
  260. if sha256(seed_maybe).digest()[:8] == chk:
  261. msg("Passphrase and hash preset are correct")
  262. seed = seed_maybe
  263. break
  264. elif confirm_old_format():
  265. if confirm_correct_seed_id(old_fmt_sid):
  266. break
  267. return seed
  268. def _get_seed(infile,opts,silent=False,seed_id=""):
  269. ext = get_extension(infile)
  270. if ext == g.mn_ext: source = "mnemonic"
  271. elif ext == g.brain_ext: source = "brainwallet"
  272. elif ext == g.seed_ext: source = "seed"
  273. elif ext == g.wallet_ext: source = "wallet"
  274. elif ext == g.incog_ext: source = "incognito wallet"
  275. elif ext == g.incog_hex_ext: source = "incognito wallet"
  276. elif 'from_mnemonic' in opts: source = "mnemonic"
  277. elif 'from_brain' in opts: source = "brainwallet"
  278. elif 'from_seed' in opts: source = "seed"
  279. elif 'from_incog' in opts: source = "incognito wallet"
  280. else:
  281. if infile: msg(
  282. "Invalid file extension for file: %s\nValid extensions: '.%s'" %
  283. (infile, "', '.".join(g.seedfile_exts)))
  284. else: msg("No seed source type specified and no file supplied")
  285. sys.exit(2)
  286. seed_id_str = " for seed ID "+seed_id if seed_id else ""
  287. if source == "mnemonic":
  288. prompt = "Enter mnemonic%s: " % seed_id_str
  289. words = get_words(infile,"mnemonic data",prompt,opts)
  290. wl = get_default_wordlist()
  291. from mmgen.mnemonic import get_seed_from_mnemonic
  292. seed = get_seed_from_mnemonic(words,wl)
  293. elif source == "brainwallet":
  294. if 'from_brain' not in opts:
  295. msg("'--from-brain' parameters must be specified for brainwallet file")
  296. sys.exit(2)
  297. prompt = "Enter brainwallet passphrase%s: " % seed_id_str
  298. words = get_words(infile,"brainwallet data",prompt,opts)
  299. seed = _get_seed_from_brain_passphrase(words,opts)
  300. elif source == "seed":
  301. prompt = "Enter seed%s in %s format: " % (seed_id_str,g.seed_ext)
  302. words = get_words(infile,"seed data",prompt,opts)
  303. seed = get_seed_from_seed_data(words)
  304. elif source == "wallet":
  305. seed = get_seed_from_wallet(infile, opts, silent=silent)
  306. elif source == "incognito wallet":
  307. h = ext == g.incog_hex_ext or 'from_incog_hex' in opts
  308. seed = get_seed_from_incog_wallet(infile, opts, silent=silent, hex_input=h)
  309. if infile and not seed and (
  310. source == "seed" or source == "mnemonic" or source == "incognito wallet"):
  311. msg("Invalid %s file '%s'" % (source,infile))
  312. sys.exit(2)
  313. if g.debug: print "Seed: %s" % hexlify(seed)
  314. return seed
  315. # Repeat if entered data is invalid
  316. def get_seed_retry(infile,opts,seed_id=""):
  317. silent = False
  318. while True:
  319. seed = _get_seed(infile,opts,silent=silent,seed_id=seed_id)
  320. silent = True
  321. if seed: return seed
  322. def _get_seed_from_brain_passphrase(words,opts):
  323. bp = " ".join(words)
  324. if g.debug: print "Sanitized brain passphrase: %s" % bp
  325. seed_len,hash_preset = get_from_brain_opt_params(opts)
  326. if g.debug: print "Brainwallet l = %s, p = %s" % (seed_len,hash_preset)
  327. vmsg_r("Hashing brainwallet data. Please wait...")
  328. # Use buflen arg of scrypt.hash() to get seed of desired length
  329. seed = scrypt_hash_passphrase(bp, "", hash_preset, buflen=seed_len/8)
  330. vmsg("Done")
  331. return seed
  332. # Vars for mmgen_*crypt functions only
  333. salt_len,sha256_len,nonce_len = 32,32,32
  334. def mmgen_encrypt(data,what="data",hash_preset='',opts={}):
  335. salt,iv,nonce = get_random(salt_len,opts),\
  336. get_random(g.aesctr_iv_len,opts), get_random(nonce_len,opts)
  337. hp = hash_preset or get_hash_preset_from_user('3',what)
  338. m = "default" if hp == '3' else "user-requested"
  339. vmsg("Encrypting %s" % what)
  340. qmsg("Using %s hash preset of '%s'" % (m,hp))
  341. passwd = get_new_passphrase(what, {})
  342. key = make_key(passwd, salt, hp)
  343. enc_d = encrypt_data(sha256(nonce+data).digest() + nonce + data, key,
  344. int(hexlify(iv),16), what=what)
  345. return salt+iv+enc_d
  346. def mmgen_decrypt(data,what="data",hash_preset=""):
  347. dstart = salt_len + g.aesctr_iv_len
  348. salt,iv,enc_d = data[:salt_len],data[salt_len:dstart],data[dstart:]
  349. vmsg("Preparing to decrypt %s" % what)
  350. hp = hash_preset or get_hash_preset_from_user('3',what)
  351. m = "default" if hp == '3' else "user-requested"
  352. qmsg("Using %s hash preset of '%s'" % (m,hp))
  353. passwd = get_mmgen_passphrase(what,{})
  354. key = make_key(passwd, salt, hp)
  355. dec_d = decrypt_data(enc_d, key, int(hexlify(iv),16), what)
  356. if dec_d[:sha256_len] == sha256(dec_d[sha256_len:]).digest():
  357. vmsg("OK")
  358. return dec_d[sha256_len+nonce_len:]
  359. else:
  360. msg("Incorrect passphrase or hash preset")
  361. return False
  362. def mmgen_decrypt_retry(d,what="data"):
  363. while True:
  364. d_dec = mmgen_decrypt(d,what)
  365. if d_dec: return d_dec
  366. msg("Trying again...")