|
@@ -0,0 +1,630 @@
|
|
|
+#!/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 *
|
|
|
+
|
|
|
+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 = "%s wallet" % g.proj_name
|
|
|
+
|
|
|
+ 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 {} wallet data from file '{}'".format(
|
|
|
+ g.proj_name,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="{} incognito wallet".format(g.proj_name)
|
|
|
+
|
|
|
+ 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))
|