msg.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
  4. # Copyright (C)2013-2023 The MMGen Project <mmgen@tuta.io>
  5. # Licensed under the GNU General Public License, Version 3:
  6. # https://www.gnu.org/licenses
  7. # Public project repositories:
  8. # https://github.com/mmgen/mmgen
  9. # https://gitlab.com/mmgen/mmgen
  10. """
  11. msg: base message signing classes
  12. """
  13. import os,importlib,json
  14. from .cfg import gc
  15. from .objmethods import MMGenObject,Hilite,InitErrors
  16. from .util import msg,die,suf,make_chksum_6,fmt_list,remove_dups
  17. from .color import red,orange,grnbg
  18. from .protocol import init_proto
  19. from .fileutil import get_data_from_file,write_data_to_file
  20. from .addr import MMGenID,CoinAddr
  21. class MMGenIDRange(str,Hilite,InitErrors,MMGenObject):
  22. """
  23. closely based on MMGenID
  24. """
  25. color = 'orange'
  26. width = 0
  27. trunc_ok = False
  28. def __new__(cls,proto,id_str):
  29. from .addrlist import AddrIdxList
  30. from .addr import AddrListID
  31. from .seed import SeedID
  32. try:
  33. ss = str(id_str).split(':')
  34. assert len(ss) in (2,3),'not 2 or 3 colon-separated items'
  35. t = proto.addr_type((ss[1],proto.dfl_mmtype)[len(ss)==2])
  36. me = str.__new__(cls,'{}:{}:{}'.format(ss[0],t,ss[-1]))
  37. me.sid = SeedID(sid=ss[0])
  38. me.idxlist = AddrIdxList(ss[-1])
  39. me.mmtype = t
  40. assert t in proto.mmtypes, f'{t}: invalid address type for {proto.cls_name}'
  41. me.al_id = str.__new__(AddrListID,me.sid+':'+me.mmtype) # checks already done
  42. me.proto = proto
  43. return me
  44. except Exception as e:
  45. return cls.init_fail(e,id_str)
  46. class coin_msg:
  47. class base(MMGenObject):
  48. def __init__(self,cfg):
  49. self.cfg = cfg
  50. ext = 'rawmsg.json'
  51. signed = False
  52. chksum_keys = ('addrlists','message','msghash_type','network')
  53. @property
  54. def desc(self):
  55. return ('signed' if self.signed else 'unsigned') + ' message data'
  56. @property
  57. def chksum(self):
  58. return make_chksum_6(
  59. json.dumps(
  60. {k:self.data[k] for k in self.chksum_keys},
  61. sort_keys = True,
  62. separators = (',', ':')
  63. ))
  64. @property
  65. def filename_stem(self):
  66. coin,network = self.data['network'].split('_')
  67. return '{}[{}]{}'.format(
  68. self.chksum.upper(),
  69. coin.upper(),
  70. ('' if network == 'mainnet' else '.'+network) )
  71. @property
  72. def filename(self):
  73. return f'{self.filename_stem}.{self.ext}'
  74. @property
  75. def signed_filename(self):
  76. return f'{self.filename_stem}.{coin_msg.signed.ext}'
  77. def get_proto_from_file(self,filename):
  78. data = json.loads(get_data_from_file( self.cfg, filename ))
  79. network_id = data['metadata']['network'] if 'metadata' in data else data['network'].lower()
  80. coin,network = network_id.split('_')
  81. return init_proto( cfg=self.cfg, coin=coin, network=network )
  82. def write_to_file(self,outdir=None,ask_overwrite=False):
  83. data = {
  84. 'id': f'{gc.proj_name} {self.desc}',
  85. 'metadata': self.data,
  86. 'signatures': self.sigs,
  87. }
  88. write_data_to_file(
  89. cfg = self.cfg,
  90. outfile = os.path.join(outdir or '',self.filename),
  91. data = json.dumps(data,sort_keys=True,indent=4),
  92. desc = self.desc,
  93. ask_overwrite = ask_overwrite )
  94. class new(base):
  95. def __init__(self,cfg,message,addrlists,msghash_type,*args,**kwargs):
  96. self.cfg = cfg
  97. msghash_type = msghash_type or self.msg_cls.msghash_types[0]
  98. if msghash_type not in self.msg_cls.msghash_types:
  99. die(2,f'msghash_type {msghash_type!r} not supported for {self.proto.base_proto} protocol')
  100. self.data = {
  101. 'network': '{}_{}'.format( self.proto.coin.lower(), self.proto.network ),
  102. 'addrlists': [MMGenIDRange(self.proto,i) for i in addrlists.split()],
  103. 'message': message,
  104. 'msghash_type': msghash_type,
  105. }
  106. self.sigs = {}
  107. class completed(base):
  108. def __init__(self,cfg,data,infile,*args,**kwargs):
  109. self.cfg = cfg
  110. if data:
  111. self.__dict__ = data
  112. return
  113. self.data = get_data_from_file(
  114. cfg = self.cfg,
  115. infile = infile,
  116. desc = self.desc )
  117. d = json.loads(self.data)
  118. self.data = d['metadata']
  119. self.sigs = d['signatures']
  120. self.addrlists = [MMGenIDRange(self.proto,i) for i in self.data['addrlists']]
  121. def format(self,req_addr=None):
  122. labels = {
  123. 'addr': 'address:',
  124. 'addr_p2pkh': 'addr_p2pkh:',
  125. 'pubhash': 'pubkey hash:',
  126. 'sig': 'signature:',
  127. }
  128. def gen_entry(e):
  129. for k in labels:
  130. if e.get(k):
  131. yield fs_sig.format( labels[k], e[k] )
  132. def gen_all():
  133. for k,v in hdr_data.items():
  134. yield fs_hdr.format( v[0], v[1](self.data[k]) )
  135. if self.sigs:
  136. yield ''
  137. yield 'Signatures:'
  138. for n,(k,v) in enumerate(self.sigs.items()):
  139. yield ''
  140. yield '{:>3}) {}'.format(n+1,k)
  141. for res in gen_entry(v):
  142. yield res
  143. def gen_single():
  144. for k,v in hdr_data.items():
  145. yield fs_hdr.format( v[0], v[1](self.data[k]) )
  146. if self.sigs:
  147. yield 'Signature data:'
  148. k = (
  149. CoinAddr(self.proto,req_addr) if type(self).__name__ == 'exported_sigs' else
  150. MMGenID(self.proto,req_addr) )
  151. if k not in self.sigs:
  152. die(1,f'{k}: address not found in signature data')
  153. for res in gen_entry(self.sigs[k]):
  154. yield res
  155. hdr_data = {
  156. 'message': ('Message:', lambda v: grnbg(v) ),
  157. 'network': ('Network:', lambda v: v.replace('_',' ').upper() ),
  158. 'msghash_type': ('Message Hash Type:', lambda v: v ),
  159. 'addrlists': ('Address Ranges:', lambda v: fmt_list(v,fmt='bare') ),
  160. 'failed_sids': ('Failed Seed IDs:', lambda v: red(fmt_list(v,fmt='bare')) ),
  161. }
  162. if len(self.msg_cls.msghash_types) == 1:
  163. del hdr_data['msghash_type']
  164. if req_addr or type(self).__name__ == 'exported_sigs':
  165. del hdr_data['addrlists']
  166. if req_addr or not self.data.get('failed_sids'):
  167. del hdr_data['failed_sids']
  168. fs_hdr = '{:%s} {}' % max(len(v[0]) for v in hdr_data.values())
  169. fs_sig = '%s{:%s} %s{}' % (
  170. ' ' * (2 if req_addr else 5),
  171. max(len(labels[k]) for v in self.sigs.values() for k in v.keys()),
  172. self.msg_cls.sigdata_pfx or ''
  173. ) if self.sigs else None
  174. if req_addr:
  175. return '\n'.join(gen_single())
  176. else:
  177. return (
  178. '{}SIGNED MESSAGE DATA:\n\n '.format('' if self.sigs else 'UN') +
  179. '\n '.join(gen_all()) )
  180. class unsigned(completed):
  181. async def sign(self,wallet_files):
  182. async def sign_list(al_in,seed):
  183. al = KeyAddrList(
  184. cfg = self.cfg,
  185. proto = self.proto,
  186. seed = seed,
  187. addr_idxs = al_in.idxlist,
  188. mmtype = al_in.mmtype,
  189. skip_chksum = True,
  190. add_p2pkh = al_in.mmtype in ('S','B') )
  191. for e in al.data:
  192. sig = await self.do_sign(
  193. wif = e.sec.wif,
  194. message = self.data['message'],
  195. msghash_type = self.data['msghash_type'] )
  196. mmid = '{}:{}:{}'.format( al_in.sid, al_in.mmtype, e.idx )
  197. data = {
  198. 'addr': e.addr,
  199. 'sig': sig,
  200. }
  201. if self.msg_cls.include_pubhash:
  202. data.update({ 'pubhash': self.proto.decode_addr(e.addr_p2pkh or e.addr).bytes.hex() })
  203. if e.addr_p2pkh:
  204. data.update({'addr_p2pkh': e.addr_p2pkh})
  205. self.sigs[mmid] = data
  206. if self.proto.sign_mode == 'daemon':
  207. from .rpc import rpc_init
  208. self.rpc = await rpc_init(self.cfg,self.proto)
  209. from .wallet import Wallet
  210. from .addrlist import KeyAddrList
  211. wallet_seeds = [Wallet(cfg=self.cfg,fn=fn).seed for fn in wallet_files]
  212. need_sids = remove_dups([al.sid for al in self.addrlists], quiet=True)
  213. saved_seeds = list()
  214. # First try wallet seeds:
  215. for sid in need_sids:
  216. for seed in wallet_seeds:
  217. if sid == seed.sid:
  218. saved_seeds.append(seed)
  219. need_sids.remove(sid)
  220. break
  221. # Then subseeds:
  222. for sid in need_sids:
  223. for seed in wallet_seeds:
  224. subseed = seed.subseeds.get_subseed_by_seed_id(sid,print_msg=True)
  225. if subseed:
  226. saved_seeds.append(subseed)
  227. need_sids.remove(sid)
  228. break
  229. for al in self.addrlists:
  230. for seed in saved_seeds:
  231. if al.sid == seed.sid:
  232. await sign_list(al,seed)
  233. break
  234. if need_sids:
  235. msg('Failed Seed IDs: {}'.format(orange(fmt_list(need_sids,fmt='bare'))))
  236. self.data['failed_sids'] = need_sids
  237. class signed(completed):
  238. ext = 'sigmsg.json'
  239. signed = True
  240. class signed_online(signed):
  241. def get_sigs(self,addr):
  242. if addr:
  243. req_addr = (
  244. CoinAddr(self.proto,addr) if type(self).__name__ == 'exported_sigs' else
  245. MMGenID(self.proto,addr) )
  246. sigs = {k:v for k,v in self.sigs.items() if k == req_addr}
  247. else:
  248. sigs = self.sigs
  249. if not sigs:
  250. die(1,'No signatures')
  251. return sigs
  252. async def verify(self,addr=None):
  253. sigs = self.get_sigs(addr)
  254. if self.proto.sign_mode == 'daemon':
  255. from .rpc import rpc_init
  256. self.rpc = await rpc_init(self.cfg,self.proto)
  257. for k,v in sigs.items():
  258. ret = await self.do_verify(
  259. addr = v.get('addr_p2pkh') or v['addr'],
  260. sig = v['sig'],
  261. message = self.data['message'],
  262. msghash_type = self.data['msghash_type'] )
  263. if not ret:
  264. die(3,f'Invalid signature for address {k} ({v["addr"]})')
  265. return len(sigs)
  266. def get_json_for_export(self,addr=None):
  267. sigs = list( self.get_sigs(addr).values() )
  268. pfx = self.msg_cls.sigdata_pfx
  269. if pfx:
  270. sigs = [{k:pfx+v for k,v in e.items()} for e in sigs]
  271. return json.dumps(
  272. {
  273. 'message': self.data['message'],
  274. 'msghash_type': self.data['msghash_type'],
  275. 'network': self.data['network'].upper(),
  276. 'signatures': sigs,
  277. },
  278. sort_keys = True,
  279. indent = 4
  280. )
  281. class exported_sigs(signed_online):
  282. def __init__(self,cfg,infile,*args,**kwargs):
  283. self.cfg = cfg
  284. self.data = json.loads(
  285. get_data_from_file(
  286. cfg = self.cfg,
  287. infile = infile,
  288. desc = self.desc )
  289. )
  290. pfx = self.msg_cls.sigdata_pfx
  291. self.sigs = {sig_data['addr']:sig_data for sig_data in (
  292. [{k:v[len(pfx):] for k,v in e.items()} for e in self.data['signatures']]
  293. if pfx else
  294. self.data['signatures']
  295. )}
  296. def _get_obj(clsname,cfg,coin=None,network='mainnet',infile=None,data=None,*args,**kwargs):
  297. assert not args, 'msg:_get_obj(): only keyword args allowed'
  298. if clsname == 'signed':
  299. assert data and not (coin or infile), 'msg:_get_obj(): chk2'
  300. else:
  301. assert not data and (coin or infile) and not (coin and infile), 'msg:_get_obj(): chk3'
  302. proto = (
  303. data['proto'] if data else
  304. init_proto( cfg=cfg, coin=coin, network=network ) if coin else
  305. coin_msg.base(cfg=cfg).get_proto_from_file(infile) )
  306. try:
  307. msg_cls = getattr(
  308. importlib.import_module(f'mmgen.proto.{proto.base_proto_coin.lower()}.msg'),
  309. 'coin_msg' )
  310. except:
  311. die(1,f'Message signing operations not supported for {proto.base_proto} protocol')
  312. me = MMGenObject.__new__(getattr( msg_cls, clsname, getattr(coin_msg,clsname) ))
  313. me.msg_cls = msg_cls
  314. me.proto = proto
  315. me.__init__(cfg,infile=infile,data=data,*args,**kwargs)
  316. return me
  317. def _get(clsname):
  318. return lambda *args,**kwargs: _get_obj(clsname,*args,**kwargs)
  319. NewMsg = _get('new')
  320. CompletedMsg = _get('completed')
  321. UnsignedMsg = _get('unsigned')
  322. SignedMsg = _get('signed')
  323. SignedOnlineMsg = _get('signed_online')
  324. ExportedMsgSigs = _get('exported_sigs')