123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632 |
- #!/usr/bin/env python
- #
- # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
- # Copyright (C)2013-2015 Philemon <mmgen-py@yandex.com>
- #
- # This program is free software: you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
- """
- seed.py: Seed-related classes and methods for the MMGen suite
- """
- import sys
- from binascii import hexlify,unhexlify
- import mmgen.config as g
- from mmgen.obj import *
- from mmgen.filename import *
- from mmgen.util import *
- from mmgen.bitcoin import b58encode_pad,b58decode_pad
- from mmgen.crypto import *
- pnm = g.proj_name
- class Seed(MMGenObject):
- def __init__(self,seed_bin=None):
- if not seed_bin:
- from mmgen.crypto import get_random
- # Truncate random data for smaller seed lengths
- seed_bin = sha256(get_random(1033)).digest()[:opt.seed_len/8]
- elif len(seed_bin)*8 not in g.seed_lens:
- die(3,"%s: invalid seed length" % len(seed_bin))
- self.data = seed_bin
- self.hexdata = hexlify(seed_bin)
- self.sid = make_chksum_8(seed_bin)
- self.len_bytes = len(seed_bin)
- self.len_bits = len(seed_bin) * 8
- class SeedSource(MMGenObject):
- class SeedSourceData(MMGenObject): pass
- desc = "seed source"
- seed_opts = {
- "mnemonic": "Mnemonic",
- "brain": "Brainwallet",
- "seed": "SeedFile",
- "incog": "IncogWallet",
- "incog_hex": "IncogWalletHex",
- "incog_hidden": "IncogWalletHidden",
- }
- def __init__(self,fn=None,seed=None,passwd=None):
- self.ssdata = self.SeedSourceData()
- if seed:
- self.desc = "new " + self.desc
- self.seed = seed
- self.ssdata.passwd = passwd
- self._pre_encode()
- self._encode()
- else:
- self._get_formatted_data(fn)
- self._deformat()
- self._decode()
- def _get_formatted_data(self,fn):
- if fn:
- self.infile = fn
- self.fmt_data = get_data_from_file(fn.name,self.desc)
- else:
- self.infile = None
- self.fmt_data = get_data_from_user(self.desc)
- def _pre_encode(self): pass
- def init(cls,fn=None,seed=None,passwd=None):
- sstype = None
- sopts=["%s_%s" % (l,k) for k in cls.seed_opts for l in "from","export"]
- for o in sopts:
- if o in opt.__dict__ and opt.__dict__[o]:
- sstype = cls.seed_opts[o.split("_",1)[1]]
- break
- if seed:
- return globals()[sstype or "Wallet"](seed=seed)
- else:
- if fn:
- if opt.from_incog_hidden:
- fn = Filename(fn,ftype="hincog")
- else:
- fn = Filename(fn)
- sstype = fn.linked_obj
- return globals()[sstype](fn=fn)
- else:
- return globals()[sstype or "Wallet"]()
- init = classmethod(init)
- def write_to_file(self):
- self._format()
- write_to_file_or_stdout(self._filename(),self.fmt_data, self.desc)
- class SeedSourceUnenc(SeedSource): pass
- class SeedSourceEnc(SeedSource):
- _ss_enc_msg = {
- 'choose_passphrase': """
- You must choose a passphrase to encrypt your new %s with.
- A key will be generated from your passphrase using a hash preset of '%s'.
- Please note that no strength checking of passphrases is performed. For an
- empty passphrase, just hit ENTER twice.
- """.strip()
- }
- def _pre_encode(self):
- if not self.ssdata.passwd:
- self._get_hash_preset()
- self._get_first_passwd()
- self._encrypt_seed()
- def _get_first_passwd(self):
- qmsg(self._ss_enc_msg['choose_passphrase'] % (self.desc,opt.hash_preset))
- self.ssdata.passwd = get_new_passphrase(what=self.desc)
- def _get_hash_preset(self):
- self.ssdata.hash_preset = \
- opt.hash_preset or get_hash_preset_from_user(what=self.desc)
- def _encrypt_seed(self):
- d = self.ssdata
- d.salt = sha256(get_random(128)).digest()[:g.salt_len]
- key = make_key(d.passwd, d.salt, d.hash_preset)
- d.key_id = make_chksum_8(key)
- d.enc_seed = encrypt_seed(self.seed.data,key)
- class Mnemonic (SeedSourceUnenc):
- desc = "mnemonic data"
- wl_checksums = {
- "electrum": '5ca31424',
- "tirosh": '1a5faeff'
- }
- mn_base = 1626
- wordlists = sorted(wl_checksums)
- def _mn2hex_pad(self,mn): return len(mn) * 8 / 3
- def _hex2mn_pad(self,hexnum): return len(hexnum) * 3 / 8
- def _baseNtohex(self,base,words,wl,pad=0):
- deconv = [wl.index(words[::-1][i])*(base**i)
- for i in range(len(words))]
- ret = ("{:0%sx}" % pad).format(sum(deconv))
- return "%s%s" % (('0' if len(ret) % 2 else ''), ret)
- def _hextobaseN(self,base,hexnum,wl,pad=0):
- num,ret = int(hexnum,16),[]
- while num:
- ret.append(num % base)
- num /= base
- return [wl[n] for n in [0] * (pad-len(ret)) + ret[::-1]]
- def _get_wordlist(self,wordlist=g.default_wordlist):
- wordlist = wordlist.lower()
- if wordlist not in self.wordlists:
- die(1,'"%s": invalid wordlist. Valid choices: %s' %
- (wordlist,'"'+'" "'.join(self.wordlists)+'"'))
- if wordlist == "electrum":
- from mmgen.mn_electrum import words
- elif wordlist == "tirosh":
- from mmgen.mn_tirosh import words
- else:
- die(3,"Internal error: unknown wordlist")
- return words.strip().split("\n")
- def _encode(self):
- wl = self._get_wordlist()
- seed_hex = hexlify(self.seed.data)
- mn = self._hextobaseN(self.mn_base,seed_hex,wl,self._hex2mn_pad(seed_hex))
- rev = self._baseNtohex(self.mn_base,mn,wl,self._mn2hex_pad(mn))
- if rev != seed_hex:
- msg("ERROR: seed recomputed from wordlist doesn't match original seed!")
- msg("Original seed: %s" % seed_hex)
- msg("Recomputed seed: %s" % rev)
- sys.exit(3)
- self.ssdata.mnemonic = mn
- def _format(self):
- self.fmt_data = " ".join(self.ssdata.mnemonic) + "\n"
- def _deformat(self):
- mn = self.fmt_data.split()
- wl = self._get_wordlist()
- if len(mn) not in g.mn_lens:
- die(3,"Invalid mnemonic (%i words). Allowed numbers of words: %s" %
- (len(mn),", ".join([str(i) for i in g.mn_lens])))
- for n,w in enumerate(mn,1):
- if w not in wl:
- die(3,"Invalid mnemonic: word #%s is not in the wordlist" % n)
- self.ssdata.mnemonic = mn
- def _decode(self):
- mn = self.ssdata.mnemonic
- wl = self._get_wordlist()
- seed_hex = self._baseNtohex(self.mn_base,mn,wl,self._mn2hex_pad(mn))
- rev = self._hextobaseN(self.mn_base,seed_hex,wl,self._hex2mn_pad(seed_hex))
- if rev != mn:
- msg("ERROR: mnemonic recomputed from seed not the same as original")
- die(3,"Recomputed mnemonic:\n%s" % " ".join(rev))
- qmsg("Valid mnemonic for seed ID %s" % make_chksum_8(unhexlify(seed_hex)))
- self.seed = Seed(unhexlify(seed_hex))
- def _filename(self):
- return "%s.%s" % (self.seed.sid, g.mn_ext)
- class SeedFile (SeedSourceUnenc):
- desc = "seed data"
- def _encode(self):
- b58seed = b58encode_pad(self.seed.data)
- self.ssdata.chksum = make_chksum_6(b58seed)
- self.ssdata.b58seed = b58seed
- def _decode(self):
- seed = b58decode_pad(self.ssdata.b58seed)
- if seed == False:
- msg("Invalid base 58 string: %s" % val)
- return False
- msg("Valid seed data for seed ID %s" % make_chksum_8(seed))
- self.seed = Seed(seed)
- def _format(self):
- self.fmt_data = "%s %s\n" % (
- self.ssdata.chksum,
- split_into_columns(4,self.ssdata.b58seed)
- )
- def _deformat(self):
- what = self.desc
- ld = self.fmt_data.split()
- if not (7 <= len(ld) <= 12): # 6 <= padded b58 data (ld[1:]) <= 11
- msg("Invalid data length (%s) in %s" % (len(ld),what))
- return False
- a,b = ld[0],"".join(ld[1:])
- if not is_chksum_6(a):
- msg("'%s': invalid checksum format, in %s" % (a, what))
- return False
- if not is_b58string(b):
- msg("'%s': not a base 58 string, in %s" % (b, what))
- return False
- vmsg_r("Validating %s checksum..." % what)
- compare_chksums(a,"checksum",make_chksum_6(b),"base 58 data")
- self.ssdata.chksum = a
- self.ssdata.b58seed = b
- def _filename(self):
- return "%s.%s" % (self.seed.sid, g.seed_ext)
- class Wallet (SeedSourceEnc):
- desc = "{pnm} wallet".format(pnm=pnm)
- def _encode(self):
- d = self.ssdata
- d.label = opt.label or "No Label"
- d.pw_status = "NE" if len(d.passwd) else "E"
- d.timestamp = make_timestamp()
- def _format(self):
- d = self.ssdata
- s = self.seed
- s_fmt = b58encode_pad(d.salt)
- es_fmt = b58encode_pad(d.enc_seed)
- lines = (
- d.label,
- "{} {} {} {} {}".format(s.sid.lower(), d.key_id.lower(),
- s.len_bits, d.pw_status, d.timestamp),
- "{}: {} {} {}".format(d.hash_preset,*get_hash_params(d.hash_preset)),
- "{} {}".format(make_chksum_6(s_fmt), split_into_columns(4,s_fmt)),
- "{} {}".format(make_chksum_6(es_fmt), split_into_columns(4,es_fmt))
- )
- chksum = make_chksum_6(" ".join(lines))
- self.fmt_data = "%s\n" % "\n".join((chksum,)+lines)
- def _decode(self):
- d = self.ssdata
- # Needed for multiple transactions with {}-txsign
- prompt_add = " "+self.infile.name if opt.quiet else ""
- passwd = get_mmgen_passphrase(self.desc+prompt_add)
- key = make_key(passwd, d.salt, d.hash_preset)
- self.seed = Seed(decrypt_seed(d.enc_seed, key, d.seed_id, d.key_id))
- def _check_master_chksum(self,lines):
- if len(lines) != 6:
- vmsg("Invalid number of lines (%s) in %s data" % (len(lines),self.desc))
- elif not is_chksum_6(lines[0]):
- vmsg("Incorrect Master checksum (%s) in %s data" % (lines[0],self.desc))
- else:
- chk = make_chksum_6(" ".join(lines[1:]))
- if compare_chksums(lines[0],"master wallet",chk,"computed"):
- return True
- msg("Invalid %s data" % self.desc)
- sys.exit(2)
- def _deformat(self):
- qmsg("Getting {pnm} wallet data from file '{f}'".format(
- pnm=pnm,f=self.infile.name))
- lines = self.fmt_data.rstrip().split("\n")
- self._check_master_chksum(lines)
- d = self.ssdata
- d.label = lines[1]
- d1,d2,d3,d4,d5 = lines[2].split()
- d.seed_id = d1.upper()
- d.key_id = d2.upper()
- d.seed_len = int(d3)
- d.pw_status,d.timestamp = d4,d5
- hpdata = lines[3].split()
- d.hash_preset = hpdata[0][:-1] # a string!
- hash_params = [int(i) for i in hpdata[1:]]
- if hash_params != get_hash_params(d.hash_preset):
- msg("Hash parameters '%s' don't match hash preset '%s'" %
- (" ".join(hash_params), d.hash_preset))
- sys.exit(3)
- for i,key in (4,"salt"),(5,"enc_seed"):
- l = lines[i].split(" ",1)
- if len(l) != 2:
- msg("Invalid format for %s in %s: %s" % (key,self.desc,val))
- sys.exit(3)
- chk,val = l[0],l[1].replace(" ","")
- compare_chksums(chk,"wallet "+key,
- make_chksum_6(val),"computed checksum")
- val_bin = b58decode_pad(val)
- if val_bin == False:
- msg("Invalid base 58 number: %s" % val)
- sys.exit(3)
- setattr(d,key,val_bin)
- def _filename(self):
- return "{}-{}[{},{}].{}".format(
- self.seed.sid,
- self.ssdata.key_id,
- self.seed.len_bits,
- self.ssdata.hash_preset,
- g.wallet_ext
- )
- # def __str__(self):
- ## label,metadata,hash_preset,salt,enc_seed):
- # d = self.ssdata
- # s = self.seed
- # out = ["WALLET DATA"]
- # fs = " {:18} {}"
- # pw_empty = "Yes" if d.metadata[3] == "E" else "No"
- # for i in (
- # ("Label:", d.label),
- # ("Seed ID:", s.sid),
- # ("Key ID:", d.key_id),
- # ("Seed length:", "%s bits (%s bytes)" % (s.len_bits,s.len_bytes)),
- # ("Scrypt params:", "Preset '%s' (%s)" % (opt.hash_preset,
- # " ".join([str(i) for i in get_hash_params(opt.hash_preset)])
- # )
- # ),
- # ("Passphrase empty?", pw_empty),
- # ("Timestamp:", "%s UTC" % d.metadata[4]),
- # ): out.append(fs.format(*i))
- #
- # fs = " {:6} {}"
- # for i in (
- # ("Salt:", ""),
- # (" b58:", b58encode_pad(d.salt)),
- # (" hex:", hexlify(d.salt)),
- # ("Encrypted seed:", ""),
- # (" b58:", b58encode_pad(d.enc_seed)),
- # (" hex:", hexlify(d.enc_seed))
- # ): out.append(fs.format(*i))
- #
- # return "\n".join(out)
- class Brainwallet (SeedSourceEnc):
- desc = "brainwallet"
- def _deformat(self):
- self.brainpasswd = " ".join(self.fmt_data.split())
- def _decode(self):
- self._get_hash_preset()
- vmsg_r("Hashing brainwallet data. Please wait...")
- # Use buflen arg of scrypt.hash() to get seed of desired length
- seed = scrypt_hash_passphrase(self.brainpasswd, "",
- self.ssdata.hash_preset, buflen=opt.seed_len/8)
- vmsg("Done")
- self.seed = Seed(seed)
- class IncogWallet (SeedSourceEnc):
- desc = "incognito wallet"
- _icg_msg = {
- 'incog_iv_id': """
- Check that the generated Incog ID above is correct. If it's not, then your
- incognito data is incorrect or corrupted.
- """.strip(),
- 'incog_iv_id_hidden': """
- Check that the generated Incog ID above is correct. If it's not, then your
- incognito data is incorrect or corrupted, or you've supplied an incorrect
- offset.
- """.strip(),
- 'incorrect_incog_passphrase_try_again': """
- Incorrect passphrase, hash preset, or maybe old-format incog wallet.
- Try again? (Y)es, (n)o, (m)ore information:
- """.strip(),
- 'confirm_seed_id': """
- If the seed ID above is correct but you're seeing this message, then you need
- to exit and re-run the program with the '--old-incog-fmt' option.
- """.strip(),
- }
- def _make_iv_chksum(self,s): return sha256(s).hexdigest()[:8].upper()
- def _get_incog_data_len(self,seed_len):
- return g.aesctr_iv_len + g.salt_len + g.hincog_chk_len + seed_len/8
- def _encode (self):
- d = self.ssdata
- # IV is used BOTH to initialize counter and to salt password!
- d.iv = get_random(g.aesctr_iv_len)
- d.iv_id = self._make_iv_chksum(d.iv)
- msg("Incog ID: %s" % d.iv_id)
- d.salt = get_random(g.salt_len)
- key = make_key(d.passwd, d.salt, d.hash_preset, "incog wallet key")
- chk = sha256(self.seed.data).digest()[:8]
- d.enc_seed = encrypt_data(chk + self.seed.data, key, 1, "seed")
- d.wrapper_key = make_key(d.passwd, d.iv, d.hash_preset, "incog wrapper key")
- d.key_id = make_chksum_8(d.wrapper_key)
- d.data_len = self._get_incog_data_len(opt.seed_len)
- def _format(self):
- d = self.ssdata
- self.fmt_data = d.iv + encrypt_data(
- d.salt + d.enc_seed,
- d.wrapper_key,
- int(hexlify(d.iv),16),
- "incog data"
- )
- def _filename(self):
- return "{}-{}-{}[{},{}].{}".format(
- self.seed.sid,
- self.ssdata.key_id,
- self.ssdata.iv_id,
- self.seed.len_bits,
- self.ssdata.hash_preset,
- g.incog_ext
- )
- def _deformat(self):
- # Data could be of invalid length, so check:
- valid_dlens = map(self._get_incog_data_len, g.seed_lens)
- # => [56, 64, 72]
- raw_d = self.fmt_data
- if len(raw_d) not in valid_dlens:
- die(1,
- "Invalid incognito file size: %s. Valid sizes (in bytes): %s" %
- (len(raw_d), " ".join(map(str, valid_dlens))))
- d = self.ssdata
- d.iv = raw_d[0:g.aesctr_iv_len]
- d.incog_id = self._make_iv_chksum(d.iv)
- d.enc_incog_data = raw_d[g.aesctr_iv_len:]
- msg("Incog ID: %s" % d.incog_id)
- qmsg("Check the applicable value against your records")
- k = 'incog_iv_id_hidden' if opt.from_incog_hidden else 'incog_iv_id'
- vmsg("\n%s\n" % self._icg_msg[k])
- def _decode(self):
- d = self.ssdata
- prompt_info="{pnm} incognito wallet".format(pnm=pnm)
- while True:
- passwd = get_mmgen_passphrase(prompt_info+" "+d.incog_id)
- qmsg("Configured hash presets: %s" %
- " ".join(sorted(g.hash_presets)))
- d.hash_preset = get_hash_preset_from_user(what="incog wallet")
- # IV is used BOTH to initialize counter and to salt password!
- key = make_key(passwd, d.iv, d.hash_preset, "wrapper key")
- dd = decrypt_data(d.enc_incog_data, key,
- int(hexlify(d.iv),16), "incog data")
- d.salt = dd[0:g.salt_len]
- d.enc_seed = dd[g.salt_len:]
- key = make_key(passwd, d.salt, d.hash_preset, "main key")
- vmsg("Key ID: %s" % make_chksum_8(key))
- ret = decrypt_seed(d.enc_seed, key, "", "")
- chk,seed_maybe = ret[:8],ret[8:]
- if sha256(seed_maybe).digest()[:8] == chk:
- msg("Passphrase and hash preset are correct")
- seed = seed_maybe
- break
- else:
- msg("Incorrect passphrase or hash preset")
- self.seed = Seed(seed)
- class IncogWalletHex (IncogWallet):
- def _deformat(self):
- self.fmt_data = decode_pretty_hexdump(self.fmt_data)
- IncogWallet._deformat(self)
- class IncogWalletHidden (IncogWallet):
- def _parse_hincog_opt(self):
- class HincogParams(MMGenObject): pass
- o = opt.from_incog_hidden or opt.export_incog_hidden
- p = HincogParams()
- a,b = o.split(",")
- p.filename = a
- p.offset = int(b)
- return p
- def _check_valid_offset(self,fn,action):
- d = self.ssdata
- if fn.size < d.hincog_offset + d.data_len:
- die(1,
- "Destination file has length %s, too short to %s %s bytes of data at offset %s"
- % (f.size,action,d.data_len,d.hincog_offset))
- # overrides method in SeedSource
- def _get_formatted_data(self,fn):
- if fn: die(1,
- "Specify the filename as a parameter of the '--from-hidden-incog' option")
- d = self.ssdata
- p = self._parse_hincog_opt()
- d.hincog_offset = p.offset
- self.infile = Filename(p.filename,ftype="hincog")
- qmsg("Getting hidden incog data from file '%s'" % self.infile.name)
- # Already sanity-checked:
- d.data_len = self._get_incog_data_len(opt.seed_len)
- self._check_valid_offset(self.infile,"read")
- import os
- fh = os.open(self.infile.name,os.O_RDONLY)
- os.lseek(fh,int(p.offset),os.SEEK_SET)
- self.fmt_data = os.read(fh,d.data_len)
- os.close(fh)
- qmsg("Data read from file '%s' at offset %s" %
- (self.infile.name,p.offset), "Data read from file")
- # overrides method in SeedSource
- def write_to_file(self):
- d = self.ssdata
- self._format()
- compare_or_die(d.data_len, "target data length",
- len(self.fmt_data),"length of formatted " + self.desc)
- p = self._parse_hincog_opt()
- d.hincog_offset = p.offset
- self.outfile = f = Filename(p.filename,ftype="hincog")
- if opt.debug:
- Msg("Incog data len %s, offset %s" % (d.data_len,p.offset))
- self._check_valid_offset(f,"write")
- if not opt.quiet: confirm_or_exit("","alter file '%s'" % f.name)
- import os
- fh = os.open(f.name,os.O_RDWR)
- os.lseek(fh, int(p.offset), os.SEEK_SET)
- os.write(fh, self.fmt_data)
- os.close(fh)
- msg("Data written to file '%s' at offset %s" % (f.name,p.offset))
|