addr.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2019 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. addr.py: Address generation/display routines for the MMGen suite
  20. """
  21. from hashlib import sha256,sha512
  22. from mmgen.common import *
  23. from mmgen.obj import *
  24. pnm = g.proj_name
  25. def dmsg_sc(desc,data):
  26. if g.debug_addrlist: Msg('sc_debug_{}: {}'.format(desc,data))
  27. class AddrGenerator(MMGenObject):
  28. def __new__(cls,addr_type):
  29. if type(addr_type) == str: # allow override w/o check
  30. gen_method = addr_type
  31. elif type(addr_type) == MMGenAddrType:
  32. assert addr_type in g.proto.mmtypes,'{}: invalid address type for coin {}'.format(addr_type,g.coin)
  33. gen_method = addr_type.gen_method
  34. else:
  35. raise TypeError('{}: incorrect argument type for {}()'.format(type(addr_type),cls.__name__))
  36. gen_methods = {
  37. 'p2pkh': AddrGeneratorP2PKH,
  38. 'segwit': AddrGeneratorSegwit,
  39. 'bech32': AddrGeneratorBech32,
  40. 'ethereum': AddrGeneratorEthereum,
  41. 'zcash_z': AddrGeneratorZcashZ,
  42. 'monero': AddrGeneratorMonero}
  43. assert gen_method in gen_methods
  44. me = super(cls,cls).__new__(gen_methods[gen_method])
  45. me.desc = gen_methods
  46. return me
  47. class AddrGeneratorP2PKH(AddrGenerator):
  48. def to_addr(self,pubhex):
  49. from mmgen.protocol import hash160
  50. assert type(pubhex) == PubKey
  51. return CoinAddr(g.proto.pubhash2addr(hash160(pubhex),p2sh=False))
  52. def to_segwit_redeem_script(self,pubhex):
  53. raise NotImplementedError('Segwit redeem script not supported by this address type')
  54. class AddrGeneratorSegwit(AddrGenerator):
  55. def to_addr(self,pubhex):
  56. assert pubhex.compressed,'Uncompressed public keys incompatible with Segwit'
  57. return CoinAddr(g.proto.pubhex2segwitaddr(pubhex))
  58. def to_segwit_redeem_script(self,pubhex):
  59. assert pubhex.compressed,'Uncompressed public keys incompatible with Segwit'
  60. return HexStr(g.proto.pubhex2redeem_script(pubhex))
  61. class AddrGeneratorBech32(AddrGenerator):
  62. def to_addr(self,pubhex):
  63. assert pubhex.compressed,'Uncompressed public keys incompatible with Segwit'
  64. from mmgen.protocol import hash160
  65. return CoinAddr(g.proto.pubhash2bech32addr(hash160(pubhex)))
  66. def to_segwit_redeem_script(self,pubhex):
  67. raise NotImplementedError('Segwit redeem script not supported by this address type')
  68. class AddrGeneratorEthereum(AddrGenerator):
  69. def __init__(self,addr_type):
  70. try:
  71. assert not g.use_internal_keccak_module
  72. from sha3 import keccak_256
  73. except:
  74. from mmgen.keccak import keccak_256
  75. self.keccak_256 = keccak_256
  76. from mmgen.protocol import hash256
  77. self.hash256 = hash256
  78. return AddrGenerator.__init__(addr_type)
  79. def to_addr(self,pubhex):
  80. assert type(pubhex) == PubKey
  81. return CoinAddr(self.keccak_256(bytes.fromhex(pubhex[2:])).hexdigest()[24:])
  82. def to_wallet_passwd(self,sk_hex):
  83. return WalletPassword(self.hash256(sk_hex)[:32])
  84. def to_segwit_redeem_script(self,pubhex):
  85. raise NotImplementedError('Segwit redeem script not supported by this address type')
  86. # github.com/FiloSottile/zcash-mini/zcash/address.go
  87. class AddrGeneratorZcashZ(AddrGenerator):
  88. addr_width = 95
  89. vk_width = 97
  90. def zhash256(self,s,t):
  91. s = bytearray(s + bytes(32))
  92. s[0] |= 0xc0
  93. s[32] = t
  94. from mmgen.sha2 import Sha256
  95. return Sha256(s,preprocess=False).digest()
  96. def to_addr(self,pubhex): # pubhex is really privhex
  97. key = bytes.fromhex(pubhex)
  98. assert len(key) == 32,'{}: incorrect privkey length'.format(len(key))
  99. from nacl.bindings import crypto_scalarmult_base
  100. p2 = crypto_scalarmult_base(self.zhash256(key,1))
  101. from mmgen.protocol import _b58chk_encode
  102. ret = _b58chk_encode(g.proto.addr_ver_num['zcash_z'][0] + (self.zhash256(key,0)+p2).hex())
  103. assert len(ret) == self.addr_width,'Invalid Zcash z-address length'
  104. return CoinAddr(ret)
  105. def to_viewkey(self,pubhex): # pubhex is really privhex
  106. key = bytes.fromhex(pubhex)
  107. assert len(key) == 32,'{}: incorrect privkey length'.format(len(key))
  108. vk = bytearray(self.zhash256(key,0)+self.zhash256(key,1))
  109. vk[32] &= 0xf8
  110. vk[63] &= 0x7f
  111. vk[63] |= 0x40
  112. from mmgen.protocol import _b58chk_encode
  113. ret = _b58chk_encode(g.proto.addr_ver_num['viewkey'][0] + vk.hex())
  114. assert len(ret) == self.vk_width,'Invalid Zcash view key length'
  115. return ZcashViewKey(ret)
  116. def to_segwit_redeem_script(self,pubhex):
  117. raise NotImplementedError('Zcash z-addresses incompatible with Segwit')
  118. class AddrGeneratorMonero(AddrGenerator):
  119. def __init__(self,addr_type):
  120. try:
  121. assert not g.use_internal_keccak_module
  122. from sha3 import keccak_256
  123. except:
  124. from mmgen.keccak import keccak_256
  125. self.keccak_256 = keccak_256
  126. from mmgen.protocol import hash256
  127. self.hash256 = hash256
  128. if opt.use_old_ed25519:
  129. from mmgen.ed25519 import edwards,encodepoint,B,scalarmult
  130. else:
  131. from mmgen.ed25519ll_djbec import scalarmult
  132. from mmgen.ed25519 import edwards,encodepoint,B
  133. self.edwards = edwards
  134. self.encodepoint = encodepoint
  135. self.scalarmult = scalarmult
  136. self.B = B
  137. return AddrGenerator.__init__(addr_type)
  138. def b58enc(self,addr_bytes):
  139. enc = baseconv.fromhex
  140. l = len(addr_bytes)
  141. a = ''.join([enc((addr_bytes[i*8:i*8+8]).hex(),'b58',pad=11,tostr=True) for i in range(l//8)])
  142. b = enc((addr_bytes[l-l%8:]).hex(),'b58',pad=7,tostr=True)
  143. return a + b
  144. def to_addr(self,sk_hex): # sk_hex instead of pubhex
  145. # Source and license for scalarmultbase function:
  146. # https://github.com/bigreddmachine/MoneroPy/blob/master/moneropy/crypto/ed25519.py
  147. # Copyright (c) 2014-2016, The Monero Project
  148. # All rights reserved.
  149. def scalarmultbase(e):
  150. if e == 0: return [0, 1]
  151. Q = self.scalarmult(self.B, e//2)
  152. Q = self.edwards(Q, Q)
  153. if e & 1: Q = self.edwards(Q, self.B)
  154. return Q
  155. def hex2int_le(hexstr):
  156. return int((bytes.fromhex(hexstr)[::-1]).hex(),16)
  157. vk_hex = self.to_viewkey(sk_hex)
  158. pk_str = self.encodepoint(scalarmultbase(hex2int_le(sk_hex)))
  159. pvk_str = self.encodepoint(scalarmultbase(hex2int_le(vk_hex)))
  160. addr_p1 = bytes.fromhex(g.proto.addr_ver_num['monero'][0]) + pk_str + pvk_str
  161. return CoinAddr(self.b58enc(addr_p1 + self.keccak_256(addr_p1).digest()[:4]))
  162. def to_wallet_passwd(self,sk_hex):
  163. return WalletPassword(self.hash256(sk_hex)[:32])
  164. def to_viewkey(self,sk_hex):
  165. assert len(sk_hex) == 64,'{}: incorrect privkey length'.format(len(sk_hex))
  166. return MoneroViewKey(g.proto.preprocess_key(self.keccak_256(bytes.fromhex(sk_hex)).hexdigest(),None))
  167. def to_segwit_redeem_script(self,sk_hex):
  168. raise NotImplementedError('Monero addresses incompatible with Segwit')
  169. class KeyGenerator(MMGenObject):
  170. def __new__(cls,addr_type,generator=None,silent=False):
  171. if type(addr_type) == str: # allow override w/o check
  172. pubkey_type = addr_type
  173. elif type(addr_type) == MMGenAddrType:
  174. assert addr_type in g.proto.mmtypes,'{}: invalid address type for coin {}'.format(addr_type,g.coin)
  175. pubkey_type = addr_type.pubkey_type
  176. else:
  177. raise TypeError('{}: incorrect argument type for {}()'.format(type(addr_type),cls.__name__))
  178. if pubkey_type == 'std':
  179. if cls.test_for_secp256k1(silent=silent) and generator != 1:
  180. if not opt.key_generator or opt.key_generator == 2 or generator == 2:
  181. return super(cls,cls).__new__(KeyGeneratorSecp256k1)
  182. else:
  183. qmsg('Using (slow) native Python ECDSA library for address generation')
  184. return super(cls,cls).__new__(KeyGeneratorPython)
  185. elif pubkey_type in ('zcash_z','monero'):
  186. me = super(cls,cls).__new__(KeyGeneratorDummy)
  187. me.desc = 'mmgen-'+pubkey_type
  188. return me
  189. else:
  190. raise ValueError('{}: invalid pubkey_type argument'.format(pubkey_type))
  191. @classmethod
  192. def test_for_secp256k1(self,silent=False):
  193. try:
  194. from mmgen.secp256k1 import priv2pub
  195. m = 'Unable to execute priv2pub() from secp256k1 extension module'
  196. assert priv2pub(bytes.fromhex('deadbeef'*8),1),m
  197. return True
  198. except:
  199. return False
  200. import ecdsa
  201. class KeyGeneratorPython(KeyGenerator):
  202. desc = 'mmgen-python-ecdsa'
  203. def __init__(self,*args,**kwargs):
  204. # secp256k1: http://www.oid-info.com/get/1.3.132.0.10
  205. p = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f
  206. r = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
  207. b = 0x0000000000000000000000000000000000000000000000000000000000000007
  208. a = 0x0000000000000000000000000000000000000000000000000000000000000000
  209. Gx = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
  210. Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8
  211. curve_fp = ecdsa.ellipticcurve.CurveFp(p,a,b)
  212. G = ecdsa.ellipticcurve.Point(curve_fp,Gx,Gy,r)
  213. oid = (1,3,132,0,10)
  214. self.secp256k1 = ecdsa.curves.Curve('secp256k1',curve_fp,G,oid)
  215. # devdoc/guide_wallets.md:
  216. # Uncompressed public keys start with 0x04; compressed public keys begin with 0x03 or
  217. # 0x02 depending on whether they're greater or less than the midpoint of the curve.
  218. def privnum2pubhex(self,numpriv,compressed=False):
  219. pko = ecdsa.SigningKey.from_secret_exponent(numpriv,self.secp256k1)
  220. # pubkey = x (32 bytes) + y (32 bytes) (unsigned big-endian)
  221. pubkey = (pko.get_verifying_key().to_string()).hex()
  222. if compressed: # discard Y coord, replace with appropriate version byte
  223. # even y: <0, odd y: >0 -- https://bitcointalk.org/index.php?topic=129652.0
  224. return ('03','02')[pubkey[-1] in '02468ace'] + pubkey[:64]
  225. else:
  226. return '04' + pubkey
  227. def to_pubhex(self,privhex):
  228. assert type(privhex) == PrivKey
  229. return PubKey(self.privnum2pubhex(
  230. int(privhex,16),compressed=privhex.compressed),compressed=privhex.compressed)
  231. class KeyGeneratorSecp256k1(KeyGenerator):
  232. desc = 'mmgen-secp256k1'
  233. def to_pubhex(self,privhex):
  234. assert type(privhex) == PrivKey
  235. from mmgen.secp256k1 import priv2pub
  236. return PubKey(priv2pub(bytes.fromhex(privhex),int(privhex.compressed)).hex(),compressed=privhex.compressed)
  237. class KeyGeneratorDummy(KeyGenerator):
  238. desc = 'mmgen-dummy'
  239. def to_pubhex(self,privhex):
  240. assert type(privhex) == PrivKey
  241. return PubKey(privhex,compressed=privhex.compressed)
  242. class AddrListEntry(MMGenListItem):
  243. addr = MMGenListItemAttr('addr','CoinAddr')
  244. idx = MMGenListItemAttr('idx','AddrIdx') # not present in flat addrlists
  245. label = MMGenListItemAttr('label','TwComment',reassign_ok=True)
  246. sec = MMGenListItemAttr('sec',PrivKey,typeconv=False)
  247. viewkey = MMGenListItemAttr('viewkey','ViewKey')
  248. wallet_passwd = MMGenListItemAttr('wallet_passwd','WalletPassword')
  249. class PasswordListEntry(MMGenListItem):
  250. passwd = MMGenImmutableAttr('passwd',str,typeconv=False) # TODO: create Password type
  251. idx = MMGenImmutableAttr('idx','AddrIdx')
  252. label = MMGenListItemAttr('label','TwComment',reassign_ok=True)
  253. sec = MMGenListItemAttr('sec',PrivKey,typeconv=False)
  254. class AddrListChksum(str,Hilite):
  255. color = 'pink'
  256. trunc_ok = False
  257. def __new__(cls,addrlist):
  258. ea = addrlist.al_id.mmtype.extra_attrs # add viewkey and passwd to the mix, if present
  259. lines = [' '.join(
  260. addrlist.chksum_rec_f(e) +
  261. tuple(getattr(e,a) for a in ea if getattr(e,a))
  262. ) for e in addrlist.data]
  263. return str.__new__(cls,make_chksum_N(' '.join(lines), nchars=16, sep=True))
  264. class AddrListIDStr(str,Hilite):
  265. color = 'green'
  266. trunc_ok = False
  267. def __new__(cls,addrlist,fmt_str=None):
  268. idxs = [e.idx for e in addrlist.data]
  269. prev = idxs[0]
  270. ret = prev,
  271. for i in idxs[1:]:
  272. if i == prev + 1:
  273. if i == idxs[-1]: ret += '-', i
  274. else:
  275. if prev != ret[-1]: ret += '-', prev
  276. ret += ',', i
  277. prev = i
  278. s = ''.join(map(str,ret))
  279. if fmt_str:
  280. ret = fmt_str.format(s)
  281. else:
  282. bc = (g.proto.base_coin,g.coin)[g.proto.base_coin=='ETH']
  283. mt = addrlist.al_id.mmtype
  284. ret = '{}{}{}[{}]'.format(addrlist.al_id.sid,('-'+bc,'')[bc=='BTC'],('-'+mt,'')[mt in ('L','E')],s)
  285. dmsg_sc('id_str',ret[8:].split('[')[0])
  286. return str.__new__(cls,ret)
  287. class AddrList(MMGenObject): # Address info for a single seed ID
  288. msgs = {
  289. 'file_header': """
  290. # {pnm} address file
  291. #
  292. # This file is editable.
  293. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  294. # A text label of {n} characters or less may be added to the right of each
  295. # address, and it will be appended to the tracking wallet label upon import.
  296. # The label may contain any printable ASCII symbol.
  297. """.strip().format(n=TwComment.max_len,pnm=pnm),
  298. 'record_chksum': """
  299. Record this checksum: it will be used to verify the address file in the future
  300. """.strip(),
  301. 'check_chksum': 'Check this value against your records',
  302. 'removed_dup_keys': """
  303. Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file
  304. """.strip().format(pnm=pnm)
  305. }
  306. entry_type = AddrListEntry
  307. main_attr = 'addr'
  308. data_desc = 'address'
  309. file_desc = 'addresses'
  310. gen_desc = 'address'
  311. gen_desc_pl = 'es'
  312. gen_addrs = True
  313. gen_passwds = False
  314. gen_keys = False
  315. has_keys = False
  316. ext = 'addrs'
  317. chksum_rec_f = lambda foo,e: (str(e.idx), e.addr)
  318. def __init__(self,addrfile='',al_id='',adata=[],seed='',addr_idxs='',src='',
  319. addrlist='',keylist='',mmtype=None):
  320. do_chksum = True
  321. self.update_msgs()
  322. mmtype = mmtype or g.proto.dfl_mmtype
  323. assert mmtype in MMGenAddrType.mmtypes,'{}: mmtype not in {}'.format(mmtype,repr(MMGenAddrType.mmtypes))
  324. if seed and addr_idxs: # data from seed + idxs
  325. self.al_id,src = AddrListID(seed.sid,mmtype),'gen'
  326. adata = self.generate(seed,addr_idxs)
  327. elif addrfile: # data from MMGen address file
  328. adata = self.parse_file(addrfile) # sets self.al_id
  329. elif al_id and adata: # data from tracking wallet
  330. self.al_id = al_id
  331. do_chksum = False
  332. elif addrlist: # data from flat address list
  333. self.al_id = None
  334. adata = AddrListList([AddrListEntry(addr=a) for a in set(addrlist)])
  335. elif keylist: # data from flat key list
  336. self.al_id = None
  337. adata = AddrListList([AddrListEntry(sec=PrivKey(wif=k)) for k in set(keylist)])
  338. elif seed or addr_idxs:
  339. die(3,'Must specify both seed and addr indexes')
  340. elif al_id or adata:
  341. die(3,'Must specify both al_id and adata')
  342. else:
  343. die(3,'Incorrect arguments for {}'.format(type(self).__name__))
  344. # al_id,adata now set
  345. self.data = adata
  346. self.num_addrs = len(adata)
  347. self.fmt_data = ''
  348. self.chksum = None
  349. if self.al_id == None: return
  350. self.id_str = AddrListIDStr(self)
  351. if type(self) == KeyList: return
  352. if do_chksum:
  353. self.chksum = AddrListChksum(self)
  354. qmsg('Checksum for {} data {}: {}'.format(
  355. self.data_desc,self.id_str.hl(),self.chksum.hl()))
  356. qmsg(self.msgs[('check_chksum','record_chksum')[src=='gen']])
  357. def update_msgs(self):
  358. self.msgs = AddrList.msgs
  359. self.msgs.update(type(self).msgs)
  360. def generate(self,seed,addrnums):
  361. assert type(addrnums) is AddrIdxList
  362. seed = self.scramble_seed(seed.data)
  363. dmsg_sc('seed',seed[:8].hex())
  364. compressed = self.al_id.mmtype.compressed
  365. pubkey_type = self.al_id.mmtype.pubkey_type
  366. gen_wallet_passwd = type(self) == KeyAddrList and 'wallet_passwd' in self.al_id.mmtype.extra_attrs
  367. gen_viewkey = type(self) == KeyAddrList and 'viewkey' in self.al_id.mmtype.extra_attrs
  368. if self.gen_addrs:
  369. kg = KeyGenerator(self.al_id.mmtype)
  370. ag = AddrGenerator(self.al_id.mmtype)
  371. t_addrs,num,pos,out = len(addrnums),0,0,AddrListList()
  372. le = self.entry_type
  373. while pos != t_addrs:
  374. seed = sha512(seed).digest()
  375. num += 1 # round
  376. if num != addrnums[pos]: continue
  377. pos += 1
  378. if not g.debug:
  379. qmsg_r('\rGenerating {} #{} ({} of {})'.format(self.gen_desc,num,pos,t_addrs))
  380. e = le(idx=num)
  381. # Secret key is double sha256 of seed hash round /num/
  382. e.sec = PrivKey(sha256(sha256(seed).digest()).digest(),compressed=compressed,pubkey_type=pubkey_type)
  383. if self.gen_addrs:
  384. pubhex = kg.to_pubhex(e.sec)
  385. e.addr = ag.to_addr(pubhex)
  386. if gen_viewkey:
  387. e.viewkey = ag.to_viewkey(pubhex)
  388. if gen_wallet_passwd:
  389. e.wallet_passwd = ag.to_wallet_passwd(e.sec)
  390. if type(self) == PasswordList:
  391. e.passwd = str(self.make_passwd(e.sec)) # TODO - own type
  392. dmsg('Key {:>03}: {}'.format(pos,e.passwd))
  393. out.append(e)
  394. if g.debug_addrlist: Msg('generate():\n{}'.format(e.pformat()))
  395. qmsg('\r{}: {} {}{} generated{}'.format(
  396. self.al_id.hl(),t_addrs,self.gen_desc,suf(t_addrs,self.gen_desc_pl),' '*15))
  397. return out
  398. def check_format(self,addr): return True # format is checked when added to list entry object
  399. def scramble_seed(self,seed):
  400. is_btcfork = g.proto.base_coin == 'BTC'
  401. if is_btcfork and self.al_id.mmtype == 'L' and not g.proto.is_testnet():
  402. dmsg_sc('str','(none)')
  403. return seed
  404. if g.proto.base_coin == 'ETH':
  405. scramble_key = g.coin.lower()
  406. else:
  407. scramble_key = (g.coin.lower()+':','')[is_btcfork] + self.al_id.mmtype.name
  408. from mmgen.crypto import scramble_seed
  409. if g.proto.is_testnet():
  410. scramble_key += ':testnet'
  411. dmsg_sc('str',scramble_key)
  412. return scramble_seed(seed,scramble_key.encode(),g.scramble_hash_rounds)
  413. def encrypt(self,desc='new key list'):
  414. from mmgen.crypto import mmgen_encrypt
  415. self.fmt_data = mmgen_encrypt(self.fmt_data.encode(),desc,'')
  416. self.ext += '.'+g.mmenc_ext
  417. def write_to_file(self,ask_tty=True,ask_write_default_yes=False,binary=False,desc=None):
  418. tn = ('','.testnet')[g.proto.is_testnet()]
  419. fn = '{}{x}{}.{}'.format(self.id_str,tn,self.ext,x='-α' if g.debug_utf8 else '')
  420. ask_tty = self.has_keys and not opt.quiet
  421. write_data_to_file(fn,self.fmt_data,desc or self.file_desc,ask_tty=ask_tty,binary=binary)
  422. def idxs(self):
  423. return [e.idx for e in self.data]
  424. def addrs(self):
  425. return ['{}:{}'.format(self.al_id.sid,e.idx) for e in self.data]
  426. def addrpairs(self):
  427. return [(e.idx,e.addr) for e in self.data]
  428. def coinaddrs(self):
  429. return [e.addr for e in self.data]
  430. def comments(self):
  431. return [e.label for e in self.data]
  432. def entry(self,idx):
  433. for e in self.data:
  434. if idx == e.idx: return e
  435. def coinaddr(self,idx):
  436. for e in self.data:
  437. if idx == e.idx: return e.addr
  438. def comment(self,idx):
  439. for e in self.data:
  440. if idx == e.idx: return e.label
  441. def set_comment(self,idx,comment):
  442. for e in self.data:
  443. if idx == e.idx:
  444. e.label = comment
  445. def make_reverse_dict(self,coinaddrs):
  446. d,b = MMGenDict(),coinaddrs
  447. for e in self.data:
  448. try:
  449. d[b[b.index(e.addr)]] = MMGenID('{}:{}'.format(self.al_id,e.idx)),e.label
  450. except: pass
  451. return d
  452. def remove_dup_keys(self,cmplist):
  453. assert self.has_keys
  454. pop_list = []
  455. for n,d in enumerate(self.data):
  456. for e in cmplist.data:
  457. if e.sec.wif == d.sec.wif:
  458. pop_list.append(n)
  459. for n in reversed(pop_list): self.data.pop(n)
  460. if pop_list:
  461. vmsg(self.msgs['removed_dup_keys'].format(len(pop_list),suf(removed,'s')))
  462. def add_wifs(self,key_list):
  463. if not key_list: return
  464. for d in self.data:
  465. for e in key_list.data:
  466. if e.addr and e.sec and e.addr == d.addr:
  467. d.sec = e.sec
  468. def list_missing(self,key):
  469. return [d.addr for d in self.data if not getattr(d,key)]
  470. def generate_addrs_from_keys(self):
  471. # assume that the first listed mmtype is valid for flat key list
  472. t = MMGenAddrType(g.proto.mmtypes[0])
  473. kg = KeyGenerator(t.pubkey_type)
  474. ag = AddrGenerator(t.gen_method)
  475. d = self.data
  476. for n,e in enumerate(d,1):
  477. qmsg_r('\rGenerating addresses from keylist: {}/{}'.format(n,len(d)))
  478. e.addr = ag.to_addr(kg.to_pubhex(e.sec))
  479. if g.debug_addrlist: Msg('generate_addrs_from_keys():\n{}'.format(e.pformat()))
  480. qmsg('\rGenerated addresses from keylist: {}/{} '.format(n,len(d)))
  481. def format(self,enable_comments=False):
  482. out = [self.msgs['file_header']+'\n']
  483. if self.chksum:
  484. out.append('# {} data checksum for {}: {}'.format(
  485. capfirst(self.data_desc),self.id_str,self.chksum))
  486. out.append('# Record this value to a secure location.\n')
  487. if type(self) == PasswordList:
  488. lbl = '{} {} {}:{}'.format(self.al_id.sid,self.pw_id_str,self.pw_fmt,self.pw_len)
  489. else:
  490. bc,mt = g.proto.base_coin,self.al_id.mmtype
  491. l_coin = [] if bc == 'BTC' else [g.coin] if bc == 'ETH' else [bc]
  492. l_type = [] if mt == 'E' or (mt == 'L' and not g.proto.is_testnet()) else [mt.name.upper()]
  493. l_tn = [] if not g.proto.is_testnet() else ['TESTNET']
  494. lbl_p2 = ':'.join(l_coin+l_type+l_tn)
  495. lbl = self.al_id.sid + ('',' ')[bool(lbl_p2)] + lbl_p2
  496. dmsg_sc('lbl',lbl[9:])
  497. out.append('{} {{'.format(lbl))
  498. fs = ' {:<%s} {:<34}{}' % len(str(self.data[-1].idx))
  499. for e in self.data:
  500. c = ' '+e.label if enable_comments and e.label else ''
  501. if type(self) == KeyList:
  502. out.append(fs.format(e.idx,'{} {}'.format(self.al_id.mmtype.wif_label,e.sec.wif),c))
  503. elif type(self) == PasswordList:
  504. out.append(fs.format(e.idx,e.passwd,c))
  505. else: # First line with idx
  506. out.append(fs.format(e.idx,e.addr,c))
  507. if self.has_keys:
  508. if opt.b16: out.append(fs.format('', 'orig_hex: '+e.sec.orig_hex,c))
  509. out.append(fs.format('','{} {}'.format(self.al_id.mmtype.wif_label,e.sec.wif),c))
  510. for k in ('viewkey','wallet_passwd'):
  511. v = getattr(e,k)
  512. if v: out.append(fs.format('','{}: {}'.format(k,v),c))
  513. out.append('}')
  514. self.fmt_data = '\n'.join([l.rstrip() for l in out]) + '\n'
  515. def parse_file_body(self,lines):
  516. ret = AddrListList()
  517. le = self.entry_type
  518. def get_line():
  519. ret = lines.pop(0).split(None,2)
  520. if ret[0] == 'orig_hex:': # hacky
  521. return lines.pop(0).split(None,2)
  522. return ret
  523. while lines:
  524. d = get_line()
  525. assert is_mmgen_idx(d[0]),"'{}': invalid address num. in line: '{}'".format(d[0],' '.join(d))
  526. assert self.check_format(d[1]),"'{}': invalid {}".format(d[1],self.data_desc)
  527. if len(d) != 3: d.append('')
  528. a = le(**{'idx':int(d[0]),self.main_attr:d[1],'label':d[2]})
  529. if self.has_keys: # order: wif,(orig_hex),viewkey,wallet_passwd
  530. d = get_line()
  531. assert d[0] == self.al_id.mmtype.wif_label,"Invalid line in file: '{}'".format(' '.join(d))
  532. a.sec = PrivKey(wif=d[1])
  533. for k,dtype in (('viewkey',ViewKey),('wallet_passwd',WalletPassword)):
  534. if k in self.al_id.mmtype.extra_attrs:
  535. d = get_line()
  536. assert d[0] == k+':',"Invalid line in file: '{}'".format(' '.join(d))
  537. setattr(a,k,dtype(d[1]))
  538. ret.append(a)
  539. if self.has_keys:
  540. if (hasattr(opt,'yes') and opt.yes) or keypress_confirm('Check key-to-address validity?'):
  541. kg = KeyGenerator(self.al_id.mmtype)
  542. ag = AddrGenerator(self.al_id.mmtype)
  543. llen = len(ret)
  544. for n,e in enumerate(ret):
  545. qmsg_r('\rVerifying keys {}/{}'.format(n+1,llen))
  546. assert e.addr == ag.to_addr(kg.to_pubhex(e.sec)),(
  547. "Key doesn't match address!\n {}\n {}".format(e.sec.wif,e.addr))
  548. qmsg(' - done')
  549. return ret
  550. def parse_file(self,fn,buf=[],exit_on_error=True):
  551. def parse_addrfile_label(lbl): # we must maintain backwards compat, so parse is tricky
  552. al_coin,al_mmtype = None,None
  553. tn = lbl[-8:] == ':TESTNET'
  554. if tn:
  555. assert g.proto.is_testnet(),'{} file is testnet but protocol is mainnet!'.format(self.data_desc)
  556. lbl = lbl[:-8]
  557. else:
  558. assert not g.proto.is_testnet(),'{} file is mainnet but protocol is testnet!'.format(self.data_desc)
  559. lbl = lbl.split(':',1)
  560. if len(lbl) == 2:
  561. al_coin,al_mmtype = lbl[0],lbl[1].lower()
  562. else:
  563. if lbl[0].lower() in MMGenAddrType.get_names():
  564. al_mmtype = lbl[0].lower()
  565. else:
  566. al_coin = lbl[0]
  567. # this block fails if al_mmtype is invalid for g.coin
  568. if not al_mmtype:
  569. mmtype = MMGenAddrType('E' if al_coin in ('ETH','ETC') else 'L',on_fail='raise')
  570. else:
  571. mmtype = MMGenAddrType(al_mmtype,on_fail='raise')
  572. from mmgen.protocol import CoinProtocol
  573. base_coin = CoinProtocol(al_coin or 'BTC',testnet=False).base_coin
  574. return base_coin,mmtype
  575. def check_coin_mismatch(base_coin): # die if addrfile coin doesn't match g.coin
  576. m = '{} address file format, but base coin is {}!'
  577. assert base_coin == g.proto.base_coin, m.format(base_coin,g.proto.base_coin)
  578. lines = get_lines_from_file(fn,self.data_desc+' data',trim_comments=True)
  579. try:
  580. assert len(lines) >= 3, 'Too few lines in address file ({})'.format(len(lines))
  581. ls = lines[0].split()
  582. assert 1 < len(ls) < 5, "Invalid first line for {} file: '{}'".format(self.gen_desc,lines[0])
  583. assert ls.pop() == '{', "'{}': invalid first line".format(ls)
  584. assert lines[-1] == '}', "'{}': invalid last line".format(lines[-1])
  585. sid = ls.pop(0)
  586. assert is_mmgen_seed_id(sid),"'{}': invalid Seed ID".format(ls[0])
  587. if type(self) == PasswordList and len(ls) == 2:
  588. ss = ls.pop().split(':')
  589. assert len(ss) == 2,"'{}': invalid password length specifier (must contain colon)".format(ls[2])
  590. self.set_pw_fmt(ss[0])
  591. self.set_pw_len(ss[1])
  592. self.pw_id_str = MMGenPWIDString(ls.pop())
  593. mmtype = MMGenPasswordType('P')
  594. elif len(ls) == 1:
  595. base_coin,mmtype = parse_addrfile_label(ls[0])
  596. check_coin_mismatch(base_coin)
  597. elif len(ls) == 0:
  598. base_coin,mmtype = 'BTC',MMGenAddrType('L')
  599. check_coin_mismatch(base_coin)
  600. else:
  601. raise ValueError("'{}': Invalid first line for {} file '{}'".format(lines[0],self.gen_desc,fn))
  602. self.al_id = AddrListID(SeedID(sid=sid),mmtype)
  603. data = self.parse_file_body(lines[1:-1])
  604. assert issubclass(type(data),list),'Invalid file body data'
  605. except Exception as e:
  606. m = 'Invalid address list file ({})'.format(e.args[0])
  607. if exit_on_error: die(3,m)
  608. msg(msg)
  609. return False
  610. return data
  611. class KeyAddrList(AddrList):
  612. data_desc = 'key-address'
  613. file_desc = 'secret keys'
  614. gen_desc = 'key/address pair'
  615. gen_desc_pl = 's'
  616. gen_addrs = True
  617. gen_keys = True
  618. has_keys = True
  619. ext = 'akeys'
  620. chksum_rec_f = lambda foo,e: (str(e.idx), e.addr, e.sec.wif)
  621. class KeyList(AddrList):
  622. msgs = {
  623. 'file_header': """
  624. # {pnm} key file
  625. #
  626. # This file is editable.
  627. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  628. """.strip().format(pnm=pnm)
  629. }
  630. data_desc = 'key'
  631. file_desc = 'secret keys'
  632. gen_desc = 'key'
  633. gen_desc_pl = 's'
  634. gen_addrs = False
  635. gen_keys = True
  636. has_keys = True
  637. ext = 'keys'
  638. chksum_rec_f = lambda foo,e: (str(e.idx), e.addr, e.sec.wif)
  639. class PasswordList(AddrList):
  640. msgs = {
  641. 'file_header': """
  642. # {pnm} password file
  643. #
  644. # This file is editable.
  645. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  646. # A text label of {n} characters or less may be added to the right of each
  647. # password. The label may contain any printable ASCII symbol.
  648. #
  649. """.strip().format(n=TwComment.max_len,pnm=pnm),
  650. 'record_chksum': """
  651. Record this checksum: it will be used to verify the password file in the future
  652. """.strip()
  653. }
  654. entry_type = PasswordListEntry
  655. main_attr = 'passwd'
  656. data_desc = 'password'
  657. file_desc = 'passwords'
  658. gen_desc = 'password'
  659. gen_desc_pl = 's'
  660. gen_addrs = False
  661. gen_keys = False
  662. gen_passwds = True
  663. has_keys = False
  664. ext = 'pws'
  665. pw_len = None
  666. pw_fmt = None
  667. pw_info = {
  668. 'b58': { 'min_len': 8 , 'max_len': 36 ,'dfl_len': 20, 'desc': 'base-58 password' },
  669. 'b32': { 'min_len': 10 ,'max_len': 42 ,'dfl_len': 24, 'desc': 'base-32 password' },
  670. 'hex': { 'min_len': 64 ,'max_len': 64 ,'dfl_len': 64, 'desc': 'raw hex password' }
  671. }
  672. chksum_rec_f = lambda foo,e: (str(e.idx), e.passwd)
  673. def __init__( self,infile=None,seed=None,
  674. pw_idxs=None,pw_id_str=None,pw_len=None,pw_fmt=None,
  675. chk_params_only=False):
  676. self.update_msgs()
  677. if infile:
  678. self.data = self.parse_file(infile) # sets self.pw_id_str,self.pw_fmt,self.pw_len
  679. else:
  680. for k in seed,pw_idxs: assert chk_params_only or k
  681. for k in (pw_id_str,pw_fmt): assert k
  682. self.pw_id_str = MMGenPWIDString(pw_id_str)
  683. self.set_pw_fmt(pw_fmt)
  684. self.set_pw_len(pw_len)
  685. if chk_params_only: return
  686. self.al_id = AddrListID(seed.sid,MMGenPasswordType('P'))
  687. self.data = self.generate(seed,pw_idxs)
  688. self.num_addrs = len(self.data)
  689. self.fmt_data = ''
  690. self.chksum = AddrListChksum(self)
  691. fs = '{}-{}-{}-{}[{{}}]'.format(self.al_id.sid,self.pw_id_str,self.pw_fmt,self.pw_len)
  692. self.id_str = AddrListIDStr(self,fs)
  693. qmsg('Checksum for {} data {}: {}'.format(self.data_desc,self.id_str.hl(),self.chksum.hl()))
  694. qmsg(self.msgs[('record_chksum','check_chksum')[bool(infile)]])
  695. def set_pw_fmt(self,pw_fmt):
  696. assert pw_fmt in self.pw_info
  697. self.pw_fmt = pw_fmt
  698. def chk_pw_len(self,passwd=None):
  699. if passwd is None:
  700. assert self.pw_len
  701. pw_len = self.pw_len
  702. fs = '{l}: invalid user-requested length for {b} ({c}{m})'
  703. else:
  704. pw_len = len(passwd)
  705. fs = '{pw}: {b} has invalid length {l} ({c}{m} characters)'
  706. d = self.pw_info[self.pw_fmt]
  707. if pw_len > d['max_len']:
  708. die(2,fs.format(l=pw_len,b=d['desc'],c='>',m=d['max_len'],pw=passwd))
  709. elif pw_len < d['min_len']:
  710. die(2,fs.format(l=pw_len,b=d['desc'],c='<',m=d['min_len'],pw=passwd))
  711. def set_pw_len(self,pw_len):
  712. assert self.pw_fmt in self.pw_info
  713. d = self.pw_info[self.pw_fmt]
  714. if pw_len is None:
  715. self.pw_len = d['dfl_len']
  716. return
  717. if not is_int(pw_len):
  718. die(2,"'{}': invalid user-requested password length (not an integer)".format(pw_len,d['desc']))
  719. self.pw_len = int(pw_len)
  720. self.chk_pw_len()
  721. def make_passwd(self,hex_sec):
  722. assert self.pw_fmt in self.pw_info
  723. if self.pw_fmt == 'hex':
  724. return hex_sec
  725. else:
  726. # we take least significant part
  727. return baseconv.fromhex(hex_sec,self.pw_fmt,pad=self.pw_len,tostr=True)[-self.pw_len:]
  728. def check_format(self,pw):
  729. if not {'b58':is_b58_str,'b32':is_b32_str,'hex':is_hex_str}[self.pw_fmt](pw):
  730. msg('Password is not a valid {} string'.format(self.pw_fmt))
  731. return False
  732. if len(pw) != self.pw_len:
  733. msg('Password has incorrect length ({} != {})'.format(len(pw),self.pw_len))
  734. return False
  735. return True
  736. def scramble_seed(self,seed):
  737. # Changing either pw_fmt or pw_len will cause a different, unrelated
  738. # set of passwords to be generated: this is what we want.
  739. # NB: In original implementation, pw_id_str was 'baseN', not 'bN'
  740. scramble_key = '{}:{}:{}'.format(self.pw_fmt,self.pw_len,self.pw_id_str)
  741. from mmgen.crypto import scramble_seed
  742. return scramble_seed(seed,scramble_key.encode(),g.scramble_hash_rounds)
  743. class AddrData(MMGenObject):
  744. msgs = {
  745. 'too_many_acct_addresses': """
  746. ERROR: More than one address found for account: '{{}}'.
  747. Your 'wallet.dat' file appears to have been altered by a non-{pnm} program.
  748. Please restore your tracking wallet from a backup or create a new one and
  749. re-import your addresses.
  750. """.strip().format(pnm=pnm)
  751. }
  752. def __new__(cls,*args,**kwargs):
  753. return MMGenObject.__new__(altcoin_subclass(cls,'tw','AddrData'))
  754. def __init__(self,source=None):
  755. self.al_ids = {}
  756. if source == 'tw': self.add_tw_data()
  757. def seed_ids(self):
  758. return list(self.al_ids.keys())
  759. def addrlist(self,al_id):
  760. # TODO: Validate al_id
  761. if al_id in self.al_ids:
  762. return self.al_ids[al_id]
  763. def mmaddr2coinaddr(self,mmaddr):
  764. al_id,idx = MMGenID(mmaddr).rsplit(':',1)
  765. coinaddr = ''
  766. if al_id in self.al_ids:
  767. coinaddr = self.addrlist(al_id).coinaddr(int(idx))
  768. return coinaddr or None
  769. def coinaddr2mmaddr(self,coinaddr):
  770. d = self.make_reverse_dict([coinaddr])
  771. return (list(d.values())[0][0]) if d else None
  772. @classmethod
  773. def get_tw_data(cls):
  774. vmsg('Getting address data from tracking wallet')
  775. if 'label_api' in g.rpch.caps:
  776. accts = g.rpch.listlabels()
  777. alists = [list(a.keys()) for a in g.rpch.getaddressesbylabel([[k] for k in accts],batch=True)]
  778. else:
  779. accts = g.rpch.listaccounts(0,True)
  780. alists = g.rpch.getaddressesbyaccount([[k] for k in accts],batch=True)
  781. return list(zip(accts,alists))
  782. def add_tw_data(self):
  783. d,out,i = self.get_tw_data(),{},0
  784. for acct,addr_array in d:
  785. l = TwLabel(acct,on_fail='silent')
  786. if l and l.mmid.type == 'mmgen':
  787. obj = l.mmid.obj
  788. i += 1
  789. if len(addr_array) != 1:
  790. die(2,self.msgs['too_many_acct_addresses'].format(acct))
  791. al_id = AddrListID(SeedID(sid=obj.sid),MMGenAddrType(obj.mmtype))
  792. if al_id not in out:
  793. out[al_id] = []
  794. out[al_id].append(AddrListEntry(idx=obj.idx,addr=addr_array[0],label=l.comment))
  795. vmsg('{n} {pnm} addresses found, {m} accounts total'.format(n=i,pnm=pnm,m=len(d)))
  796. for al_id in out:
  797. self.add(AddrList(al_id=al_id,adata=AddrListList(sorted(out[al_id],key=lambda a: a.idx))))
  798. def add(self,addrlist):
  799. if type(addrlist) == AddrList:
  800. self.al_ids[addrlist.al_id] = addrlist
  801. return True
  802. else:
  803. raise TypeError('Error: object {!r} is not of type AddrList'.format(addrlist))
  804. def make_reverse_dict(self,coinaddrs):
  805. d = MMGenDict()
  806. for al_id in self.al_ids:
  807. d.update(self.al_ids[al_id].make_reverse_dict(coinaddrs))
  808. return d