wallet.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2020 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. wallet.py: Wallet classes and methods for the MMGen suite
  20. """
  21. import os
  22. from .common import *
  23. from .obj import *
  24. from .crypto import *
  25. from .baseconv import *
  26. from .seed import Seed
  27. def check_usr_seed_len(seed_len):
  28. if opt.seed_len and opt.seed_len != seed_len:
  29. die(1,f"ERROR: requested seed length ({opt.seed_len}) doesn't match seed length of source ({seed_len})")
  30. def _is_mnemonic(s,fmt):
  31. oq_save = bool(opt.quiet)
  32. opt.quiet = True
  33. try:
  34. Wallet(in_data=s,in_fmt=fmt)
  35. ret = True
  36. except:
  37. ret = False
  38. finally:
  39. opt.quiet = oq_save
  40. return ret
  41. def is_bip39_mnemonic(s): return _is_mnemonic(s,fmt='bip39')
  42. def is_mmgen_mnemonic(s): return _is_mnemonic(s,fmt='words')
  43. class WalletMeta(type):
  44. wallet_classes = set() # one-instance class, so store data in class attr
  45. def __init__(cls,name,bases,namespace):
  46. cls.wallet_classes.add(cls)
  47. cls.wallet_classes -= set(bases)
  48. class Wallet(MMGenObject,metaclass=WalletMeta):
  49. desc = g.proj_name + ' seed source'
  50. file_mode = 'text'
  51. stdin_ok = False
  52. ask_tty = True
  53. no_tty = False
  54. op = None
  55. _msg = {}
  56. class WalletData(MMGenObject): pass
  57. def __new__(cls,
  58. fn = None,
  59. ss = None,
  60. seed_bin = None,
  61. seed = None,
  62. passchg = False,
  63. in_data = None,
  64. ignore_in_fmt = False,
  65. in_fmt = None ):
  66. in_fmt = in_fmt or opt.in_fmt
  67. if hasattr(opt,'out_fmt') and opt.out_fmt:
  68. out_cls = cls.fmt_code_to_type(opt.out_fmt)
  69. if not out_cls:
  70. die(1,'{!r}: unrecognized output format'.format(opt.out_fmt))
  71. else:
  72. out_cls = None
  73. def die_on_opt_mismatch(opt,sstype):
  74. compare_or_die(
  75. cls.fmt_code_to_type(opt).__name__, 'input format requested on command line',
  76. sstype.__name__, 'input file format' )
  77. if seed or seed_bin:
  78. me = super(cls,cls).__new__(out_cls or MMGenWallet) # default to MMGenWallet
  79. me.seed = seed or Seed(seed_bin=seed_bin)
  80. me.op = 'new'
  81. elif ss:
  82. me = super(cls,cls).__new__((ss.__class__ if passchg else out_cls) or MMGenWallet)
  83. me.seed = ss.seed
  84. me.ss_in = ss
  85. me.op = ('conv','pwchg_new')[bool(passchg)]
  86. elif fn or opt.hidden_incog_input_params:
  87. from .filename import Filename
  88. if fn:
  89. f = Filename(fn)
  90. else:
  91. # permit comma in filename
  92. fn = ','.join(opt.hidden_incog_input_params.split(',')[:-1])
  93. f = Filename(fn,ftype=IncogWalletHidden)
  94. if in_fmt and not ignore_in_fmt:
  95. die_on_opt_mismatch(in_fmt,f.ftype)
  96. me = super(cls,cls).__new__(f.ftype)
  97. me.infile = f
  98. me.op = ('old','pwchg_old')[bool(passchg)]
  99. elif in_fmt:
  100. me = super(cls,cls).__new__(cls.fmt_code_to_type(in_fmt))
  101. me.op = ('old','pwchg_old')[bool(passchg)]
  102. else: # called with no arguments: initialize with random seed
  103. me = super(cls,cls).__new__(out_cls or MMGenWallet)
  104. me.seed = Seed(None)
  105. me.op = 'new'
  106. return me
  107. def __init__(self,
  108. fn = None,
  109. ss = None,
  110. seed_bin = None,
  111. seed = None,
  112. passchg = False,
  113. in_data = None,
  114. ignore_in_fmt = False,
  115. in_fmt = None ):
  116. self.passwd_file = opt.passwd_file
  117. self.ssdata = self.WalletData()
  118. self.msg = {}
  119. self.in_data = in_data
  120. for c in reversed(self.__class__.__mro__):
  121. if hasattr(c,'_msg'):
  122. self.msg.update(c._msg)
  123. if hasattr(self,'seed'):
  124. self._encrypt()
  125. return
  126. elif hasattr(self,'infile') or self.in_data or not g.stdin_tty:
  127. self._deformat_once()
  128. self._decrypt_retry()
  129. else:
  130. if not self.stdin_ok:
  131. die(1,'Reading from standard input not supported for {} format'.format(self.desc))
  132. self._deformat_retry()
  133. self._decrypt_retry()
  134. m = ('',', seed length {}'.format(self.seed.bitlen))[self.seed.bitlen!=256]
  135. qmsg('Valid {} for Seed ID {}{}'.format(self.desc,self.seed.sid.hl(),m))
  136. def _get_data(self):
  137. if hasattr(self,'infile'):
  138. self.fmt_data = get_data_from_file(self.infile.name,self.desc,binary=self.file_mode=='binary')
  139. elif self.in_data:
  140. self.fmt_data = self.in_data
  141. else:
  142. self.fmt_data = self._get_data_from_user(self.desc)
  143. def _get_data_from_user(self,desc):
  144. return get_data_from_user(desc)
  145. def _deformat_once(self):
  146. self._get_data()
  147. if not self._deformat():
  148. die(2,'Invalid format for input data')
  149. def _deformat_retry(self):
  150. while True:
  151. self._get_data()
  152. if self._deformat():
  153. break
  154. msg('Trying again...')
  155. def _decrypt_retry(self):
  156. while True:
  157. if self._decrypt():
  158. break
  159. if self.passwd_file:
  160. die(2,'Passphrase from password file, so exiting')
  161. msg('Trying again...')
  162. @classmethod
  163. def get_extensions(cls):
  164. return [c.ext for c in cls.wallet_classes if hasattr(c,'ext')]
  165. @classmethod
  166. def fmt_code_to_type(cls,fmt_code):
  167. if fmt_code:
  168. for c in cls.wallet_classes:
  169. if fmt_code in getattr(c,'fmt_codes',[]):
  170. return c
  171. return None
  172. @classmethod
  173. def ext_to_type(cls,ext):
  174. if ext:
  175. for c in cls.wallet_classes:
  176. if ext == getattr(c,'ext',None):
  177. return c
  178. return None
  179. @classmethod
  180. def format_fmt_codes(cls):
  181. d = [(c.__name__,('.'+c.ext if c.ext else str(c.ext)),','.join(c.fmt_codes))
  182. for c in cls.wallet_classes
  183. if hasattr(c,'fmt_codes')]
  184. w = max(len(i[0]) for i in d)
  185. ret = ['{:<{w}} {:<9} {}'.format(a,b,c,w=w) for a,b,c in [
  186. ('Format','FileExt','Valid codes'),
  187. ('------','-------','-----------')
  188. ] + sorted(d)]
  189. return '\n'.join(ret) + ('','-α')[g.debug_utf8] + '\n'
  190. def get_fmt_data(self):
  191. self._format()
  192. return self.fmt_data
  193. def write_to_file(self,outdir='',desc=''):
  194. self._format()
  195. kwargs = {
  196. 'desc': desc or self.desc,
  197. 'ask_tty': self.ask_tty,
  198. 'no_tty': self.no_tty,
  199. 'binary': self.file_mode == 'binary'
  200. }
  201. # write_data_to_file(): outfile with absolute path overrides opt.outdir
  202. if outdir:
  203. of = os.path.abspath(os.path.join(outdir,self._filename()))
  204. write_data_to_file(of if outdir else self._filename(),self.fmt_data,**kwargs)
  205. class WalletUnenc(Wallet):
  206. def _decrypt_retry(self): pass
  207. def _encrypt(self): pass
  208. def _filename(self):
  209. s = self.seed
  210. return '{}[{}]{x}.{}'.format(
  211. s.fn_stem,
  212. s.bitlen,
  213. self.ext,
  214. x='-α' if g.debug_utf8 else '')
  215. def _choose_seedlen(self,desc,ok_lens,subtype):
  216. from .term import get_char
  217. def choose_len():
  218. prompt = self.choose_seedlen_prompt
  219. while True:
  220. r = get_char('\r'+prompt)
  221. if is_int(r) and 1 <= int(r) <= len(ok_lens):
  222. break
  223. msg_r(('\r','\n')[g.test_suite] + ' '*len(prompt) + '\r')
  224. return ok_lens[int(r)-1]
  225. m1 = blue('{} type:'.format(capfirst(desc)))
  226. m2 = yellow(subtype)
  227. msg('{} {}'.format(m1,m2))
  228. while True:
  229. usr_len = choose_len()
  230. prompt = self.choose_seedlen_confirm.format(usr_len)
  231. if keypress_confirm(prompt,default_yes=True,no_nl=not g.test_suite):
  232. return usr_len
  233. class WalletEnc(Wallet):
  234. _msg = {
  235. 'choose_passphrase': """
  236. You must choose a passphrase to encrypt your new {} with.
  237. A key will be generated from your passphrase using a hash preset of '{}'.
  238. Please note that no strength checking of passphrases is performed.
  239. For an empty passphrase, just hit ENTER twice.
  240. """
  241. }
  242. def _get_hash_preset_from_user(self,hp,add_desc=''):
  243. prompt = 'Enter {}hash preset for {}{}{},\nor hit ENTER to {} value ({!r}): '.format(
  244. ('old ' if self.op=='pwchg_old' else 'new ' if self.op=='pwchg_new' else ''),
  245. ('','new ')[self.op=='new'],
  246. self.desc,
  247. ('',' '+add_desc)[bool(add_desc)],
  248. ('accept the default','reuse the old')[self.op=='pwchg_new'],
  249. hp )
  250. while True:
  251. ret = my_raw_input(prompt)
  252. if ret:
  253. if ret in g.hash_presets:
  254. return ret
  255. else:
  256. msg('Invalid input. Valid choices are {}'.format(', '.join(g.hash_presets)))
  257. else:
  258. return hp
  259. def _get_hash_preset(self,add_desc=''):
  260. if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'hash_preset'):
  261. old_hp = self.ss_in.ssdata.hash_preset
  262. if opt.keep_hash_preset:
  263. hp = old_hp
  264. qmsg(f'Reusing hash preset {hp!r} at user request')
  265. elif opt.hash_preset:
  266. hp = opt.hash_preset
  267. qmsg(f'Using hash preset {hp!r} requested on command line')
  268. else: # Prompt, using old value as default
  269. hp = self._get_hash_preset_from_user(old_hp,add_desc)
  270. if (not opt.keep_hash_preset) and self.op == 'pwchg_new':
  271. qmsg('Hash preset {}'.format('unchanged' if hp==old_hp else f'changed to {hp!r}'))
  272. elif opt.hash_preset:
  273. hp = opt.hash_preset
  274. qmsg(f'Using hash preset {hp!r} requested on command line')
  275. else:
  276. hp = self._get_hash_preset_from_user(g.dfl_hash_preset,add_desc)
  277. self.ssdata.hash_preset = hp
  278. def _get_new_passphrase(self):
  279. desc = '{}passphrase for {}{}'.format(
  280. ('','new ')[self.op=='pwchg_new'],
  281. ('','new ')[self.op in ('new','conv')],
  282. self.desc
  283. )
  284. if self.passwd_file:
  285. pw = ' '.join(get_words_from_file(
  286. self.passwd_file,
  287. desc,
  288. quiet = pwfile_reuse_warning(self.passwd_file) ))
  289. else:
  290. qmsg('\n'+fmt(self.msg['choose_passphrase'].format(self.desc,self.ssdata.hash_preset),indent=' '))
  291. if opt.echo_passphrase:
  292. pw = ' '.join(get_words_from_user(f'Enter {desc}: '))
  293. else:
  294. for i in range(g.passwd_max_tries):
  295. pw = ' '.join(get_words_from_user(f'Enter {desc}: '))
  296. pw_chk = ' '.join(get_words_from_user('Repeat passphrase: '))
  297. dmsg(f'Passphrases: [{pw}] [{pw_chk}]')
  298. if pw == pw_chk:
  299. vmsg('Passphrases match')
  300. break
  301. else:
  302. msg('Passphrases do not match. Try again.')
  303. else:
  304. die(2,f'User failed to duplicate passphrase in {g.passwd_max_tries} attempts')
  305. if pw == '':
  306. qmsg('WARNING: Empty passphrase')
  307. self.ssdata.passwd = pw
  308. return pw
  309. def _get_passphrase(self,add_desc=''):
  310. desc = '{}passphrase for {}{}'.format(
  311. ('','old ')[self.op=='pwchg_old'],
  312. self.desc,
  313. ('',' '+add_desc)[bool(add_desc)]
  314. )
  315. if self.passwd_file:
  316. ret = ' '.join(get_words_from_file(
  317. self.passwd_file,
  318. desc,
  319. quiet = pwfile_reuse_warning(self.passwd_file) ))
  320. else:
  321. ret = ' '.join(get_words_from_user(f'Enter {desc}: '))
  322. self.ssdata.passwd = ret
  323. def _get_first_pw_and_hp_and_encrypt_seed(self):
  324. d = self.ssdata
  325. self._get_hash_preset()
  326. if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'passwd'):
  327. old_pw = self.ss_in.ssdata.passwd
  328. if opt.keep_passphrase:
  329. d.passwd = old_pw
  330. qmsg('Reusing passphrase at user request')
  331. else:
  332. pw = self._get_new_passphrase()
  333. if self.op == 'pwchg_new':
  334. qmsg('Passphrase {}'.format('unchanged' if pw==old_pw else 'changed'))
  335. else:
  336. self._get_new_passphrase()
  337. d.salt = sha256(get_random(128)).digest()[:g.salt_len]
  338. key = make_key(d.passwd, d.salt, d.hash_preset)
  339. d.key_id = make_chksum_8(key)
  340. d.enc_seed = encrypt_seed(self.seed.data,key)
  341. class Mnemonic(WalletUnenc):
  342. stdin_ok = True
  343. wclass = 'mnemonic'
  344. conv_cls = baseconv
  345. choose_seedlen_prompt = 'Choose a mnemonic length: 1) 12 words, 2) 18 words, 3) 24 words: '
  346. choose_seedlen_confirm = 'Mnemonic length of {} words chosen. OK?'
  347. @property
  348. def mn_lens(self):
  349. return sorted(self.conv_cls.seedlen_map_rev[self.wl_id])
  350. def _get_data_from_user(self,desc):
  351. if not g.stdin_tty:
  352. return get_data_from_user(desc)
  353. from .mn_entry import mn_entry # import here to catch cfg var errors
  354. mn_len = self._choose_seedlen(self.wclass,self.mn_lens,self.mn_type)
  355. return mn_entry(self.wl_id).get_mnemonic_from_user(mn_len)
  356. @staticmethod
  357. def _mn2hex_pad(mn): return len(mn) * 8 // 3
  358. @staticmethod
  359. def _hex2mn_pad(hexnum): return len(hexnum) * 3 // 8
  360. def _format(self):
  361. hexseed = self.seed.hexdata
  362. mn = self.conv_cls.fromhex(hexseed,self.wl_id,self._hex2mn_pad(hexseed))
  363. ret = self.conv_cls.tohex(mn,self.wl_id,self._mn2hex_pad(mn))
  364. # Internal error, so just die on fail
  365. compare_or_die(ret,'recomputed seed',hexseed,'original',e='Internal error')
  366. self.ssdata.mnemonic = mn
  367. self.fmt_data = ' '.join(mn) + '\n'
  368. def _deformat(self):
  369. self.conv_cls.init_mn(self.wl_id)
  370. mn = self.fmt_data.split()
  371. if len(mn) not in self.mn_lens:
  372. m = 'Invalid mnemonic ({} words). Valid numbers of words: {}'
  373. msg(m.format(len(mn),', '.join(map(str,self.mn_lens))))
  374. return False
  375. for n,w in enumerate(mn,1):
  376. if w not in self.conv_cls.digits[self.wl_id]:
  377. msg('Invalid mnemonic: word #{} is not in the {} wordlist'.format(n,self.wl_id.upper()))
  378. return False
  379. hexseed = self.conv_cls.tohex(mn,self.wl_id,self._mn2hex_pad(mn))
  380. ret = self.conv_cls.fromhex(hexseed,self.wl_id,self._hex2mn_pad(hexseed))
  381. if len(hexseed) * 4 not in g.seed_lens:
  382. msg('Invalid mnemonic (produces too large a number)')
  383. return False
  384. # Internal error, so just die
  385. compare_or_die(' '.join(ret),'recomputed mnemonic',' '.join(mn),'original',e='Internal error')
  386. self.seed = Seed(bytes.fromhex(hexseed))
  387. self.ssdata.mnemonic = mn
  388. check_usr_seed_len(self.seed.bitlen)
  389. return True
  390. class MMGenMnemonic(Mnemonic):
  391. fmt_codes = ('mmwords','words','mnemonic','mnem','mn','m')
  392. desc = 'MMGen native mnemonic data'
  393. mn_type = 'MMGen native'
  394. ext = 'mmwords'
  395. wl_id = 'mmgen'
  396. class BIP39Mnemonic(Mnemonic):
  397. fmt_codes = ('bip39',)
  398. desc = 'BIP39 mnemonic data'
  399. mn_type = 'BIP39'
  400. ext = 'bip39'
  401. wl_id = 'bip39'
  402. def __init__(self,*args,**kwargs):
  403. from .bip39 import bip39
  404. self.conv_cls = bip39
  405. super().__init__(*args,**kwargs)
  406. class MMGenSeedFile(WalletUnenc):
  407. stdin_ok = True
  408. fmt_codes = ('mmseed','seed','s')
  409. desc = 'seed data'
  410. ext = 'mmseed'
  411. def _format(self):
  412. b58seed = baseconv.frombytes(self.seed.data,'b58',pad='seed',tostr=True)
  413. self.ssdata.chksum = make_chksum_6(b58seed)
  414. self.ssdata.b58seed = b58seed
  415. self.fmt_data = '{} {}\n'.format(self.ssdata.chksum,split_into_cols(4,b58seed))
  416. def _deformat(self):
  417. desc = self.desc
  418. ld = self.fmt_data.split()
  419. if not (7 <= len(ld) <= 12): # 6 <= padded b58 data (ld[1:]) <= 11
  420. msg('Invalid data length ({}) in {}'.format(len(ld),desc))
  421. return False
  422. a,b = ld[0],''.join(ld[1:])
  423. if not is_chksum_6(a):
  424. msg("'{}': invalid checksum format in {}".format(a, desc))
  425. return False
  426. if not is_b58_str(b):
  427. msg("'{}': not a base 58 string, in {}".format(b, desc))
  428. return False
  429. vmsg_r('Validating {} checksum...'.format(desc))
  430. if not compare_chksums(a,'file',make_chksum_6(b),'computed',verbose=True):
  431. return False
  432. ret = baseconv.tobytes(b,'b58',pad='seed')
  433. if ret == False:
  434. msg('Invalid base-58 encoded seed: {}'.format(val))
  435. return False
  436. self.seed = Seed(ret)
  437. self.ssdata.chksum = a
  438. self.ssdata.b58seed = b
  439. check_usr_seed_len(self.seed.bitlen)
  440. return True
  441. class DieRollSeedFile(WalletUnenc):
  442. stdin_ok = True
  443. fmt_codes = ('b6d','die','dieroll')
  444. desc = 'base6d die roll seed data'
  445. ext = 'b6d'
  446. conv_cls = baseconv
  447. wclass = 'dieroll'
  448. wl_id = 'b6d'
  449. mn_type = 'base6d'
  450. choose_seedlen_prompt = 'Choose a seed length: 1) 128 bits, 2) 192 bits, 3) 256 bits: '
  451. choose_seedlen_confirm = 'Seed length of {} bits chosen. OK?'
  452. user_entropy_prompt = 'Would you like to provide some additional entropy from the keyboard?'
  453. interactive_input = False
  454. def _format(self):
  455. d = baseconv.frombytes(self.seed.data,'b6d',pad='seed',tostr=True) + '\n'
  456. self.fmt_data = block_format(d,gw=5,cols=5)
  457. def _deformat(self):
  458. d = remove_whitespace(self.fmt_data)
  459. rmap = self.conv_cls.seedlen_map_rev['b6d']
  460. if not len(d) in rmap:
  461. m = '{!r}: invalid length for {} (must be one of {})'
  462. raise SeedLengthError(m.format(len(d),self.desc,list(rmap)))
  463. # truncate seed to correct length, discarding high bits
  464. seed_len = rmap[len(d)]
  465. seed_bytes = baseconv.tobytes(d,'b6d',pad='seed')[-seed_len:]
  466. if self.interactive_input and opt.usr_randchars:
  467. if keypress_confirm(self.user_entropy_prompt):
  468. seed_bytes = add_user_random(seed_bytes,'die roll data')
  469. self.desc += ' plus user-supplied entropy'
  470. self.seed = Seed(seed_bytes)
  471. self.ssdata.hexseed = seed_bytes.hex()
  472. check_usr_seed_len(self.seed.bitlen)
  473. return True
  474. def _get_data_from_user(self,desc):
  475. if not g.stdin_tty:
  476. return get_data_from_user(desc)
  477. seed_bitlens = [n*8 for n in sorted(self.conv_cls.seedlen_map['b6d'])]
  478. seed_bitlen = self._choose_seedlen(self.wclass,seed_bitlens,self.mn_type)
  479. nDierolls = self.conv_cls.seedlen_map['b6d'][seed_bitlen // 8]
  480. m = """
  481. For a {sb}-bit seed you must roll the die {nd} times. After each die roll,
  482. enter the result on the keyboard as a digit. If you make an invalid entry,
  483. you'll be prompted to re-enter it.
  484. """
  485. msg('\n'+fmt(m.strip()).format(sb=seed_bitlen,nd=nDierolls)+'\n')
  486. b6d_digits = self.conv_cls.digits['b6d']
  487. cr = '\n' if g.test_suite else '\r'
  488. prompt_fs = '\b\b\b {}Enter die roll #{{}}: {}'.format(cr,CUR_SHOW)
  489. clear_line = '' if g.test_suite else '\r' + ' ' * 25
  490. invalid_msg = CUR_HIDE + cr + 'Invalid entry' + ' ' * 11
  491. from .term import get_char
  492. def get_digit(n):
  493. p = prompt_fs
  494. sleep = g.short_disp_timeout
  495. while True:
  496. ch = get_char(p.format(n),num_chars=1,sleep=sleep)
  497. if ch in b6d_digits:
  498. msg_r(CUR_HIDE + ' OK')
  499. return ch
  500. else:
  501. msg_r(invalid_msg)
  502. sleep = g.err_disp_timeout
  503. p = clear_line + prompt_fs
  504. dierolls,n = [],1
  505. while len(dierolls) < nDierolls:
  506. dierolls.append(get_digit(n))
  507. n += 1
  508. msg('Die rolls successfully entered' + CUR_SHOW)
  509. self.interactive_input = True
  510. return ''.join(dierolls)
  511. class PlainHexSeedFile(WalletUnenc):
  512. stdin_ok = True
  513. fmt_codes = ('hex','rawhex','plainhex')
  514. desc = 'plain hexadecimal seed data'
  515. ext = 'hex'
  516. def _format(self):
  517. self.fmt_data = self.seed.hexdata + '\n'
  518. def _deformat(self):
  519. desc = self.desc
  520. d = self.fmt_data.strip()
  521. if not is_hex_str_lc(d):
  522. msg("'{}': not a lowercase hexadecimal string, in {}".format(d,desc))
  523. return False
  524. if not len(d)*4 in g.seed_lens:
  525. msg('Invalid data length ({}) in {}'.format(len(d),desc))
  526. return False
  527. self.seed = Seed(bytes.fromhex(d))
  528. self.ssdata.hexseed = d
  529. check_usr_seed_len(self.seed.bitlen)
  530. return True
  531. class MMGenHexSeedFile(WalletUnenc):
  532. stdin_ok = True
  533. fmt_codes = ('seedhex','hexseed','mmhex')
  534. desc = 'hexadecimal seed data with checksum'
  535. ext = 'mmhex'
  536. def _format(self):
  537. h = self.seed.hexdata
  538. self.ssdata.chksum = make_chksum_6(h)
  539. self.ssdata.hexseed = h
  540. self.fmt_data = '{} {}\n'.format(self.ssdata.chksum, split_into_cols(4,h))
  541. def _deformat(self):
  542. desc = self.desc
  543. d = self.fmt_data.split()
  544. try:
  545. d[1]
  546. chk,hstr = d[0],''.join(d[1:])
  547. except:
  548. msg("'{}': invalid {}".format(self.fmt_data.strip(),desc))
  549. return False
  550. if not len(hstr)*4 in g.seed_lens:
  551. msg('Invalid data length ({}) in {}'.format(len(hstr),desc))
  552. return False
  553. if not is_chksum_6(chk):
  554. msg("'{}': invalid checksum format in {}".format(chk, desc))
  555. return False
  556. if not is_hex_str(hstr):
  557. msg("'{}': not a hexadecimal string, in {}".format(hstr, desc))
  558. return False
  559. vmsg_r('Validating {} checksum...'.format(desc))
  560. if not compare_chksums(chk,'file',make_chksum_6(hstr),'computed',verbose=True):
  561. return False
  562. self.seed = Seed(bytes.fromhex(hstr))
  563. self.ssdata.chksum = chk
  564. self.ssdata.hexseed = hstr
  565. check_usr_seed_len(self.seed.bitlen)
  566. return True
  567. class MMGenWallet(WalletEnc):
  568. fmt_codes = ('wallet','w')
  569. desc = g.proj_name + ' wallet'
  570. ext = 'mmdat'
  571. def __init__(self,*args,**kwargs):
  572. if opt.label:
  573. self.label = MMGenWalletLabel(
  574. opt.label,
  575. msg = "Error in option '--label'" )
  576. else:
  577. self.label = None
  578. super().__init__(*args,**kwargs)
  579. # logic identical to _get_hash_preset_from_user()
  580. def _get_label_from_user(self,old_lbl=''):
  581. prompt = 'Enter a wallet label, or hit ENTER {}: '.format(
  582. 'to reuse the label {}'.format(old_lbl.hl(encl="''")) if old_lbl else
  583. 'for no label' )
  584. while True:
  585. msg_r(prompt)
  586. ret = my_raw_input('')
  587. if ret:
  588. lbl = MMGenWalletLabel(ret,on_fail='return')
  589. if lbl:
  590. return lbl
  591. else:
  592. msg('Invalid label. Trying again...')
  593. else:
  594. return old_lbl or MMGenWalletLabel('No Label')
  595. # logic identical to _get_hash_preset()
  596. def _get_label(self):
  597. if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'label'):
  598. old_lbl = self.ss_in.ssdata.label
  599. if opt.keep_label:
  600. lbl = old_lbl
  601. qmsg('Reusing label {} at user request'.format(lbl.hl(encl="''")))
  602. elif self.label:
  603. lbl = self.label
  604. qmsg('Using label {} requested on command line'.format(lbl.hl(encl="''")))
  605. else: # Prompt, using old value as default
  606. lbl = self._get_label_from_user(old_lbl)
  607. if (not opt.keep_label) and self.op == 'pwchg_new':
  608. qmsg('Label {}'.format('unchanged' if lbl==old_lbl else f'changed to {lbl!r}'))
  609. elif self.label:
  610. lbl = self.label
  611. qmsg('Using label {} requested on command line'.format(lbl.hl(encl="''")))
  612. else:
  613. lbl = self._get_label_from_user()
  614. self.ssdata.label = lbl
  615. def _encrypt(self):
  616. self._get_first_pw_and_hp_and_encrypt_seed()
  617. self._get_label()
  618. d = self.ssdata
  619. d.pw_status = ('NE','E')[len(d.passwd)==0]
  620. d.timestamp = make_timestamp()
  621. def _format(self):
  622. d = self.ssdata
  623. s = self.seed
  624. slt_fmt = baseconv.frombytes(d.salt,'b58',pad='seed',tostr=True)
  625. es_fmt = baseconv.frombytes(d.enc_seed,'b58',pad='seed',tostr=True)
  626. lines = (
  627. d.label,
  628. '{} {} {} {} {}'.format(s.sid.lower(), d.key_id.lower(),
  629. s.bitlen, d.pw_status, d.timestamp),
  630. '{}: {} {} {}'.format(d.hash_preset,*get_hash_params(d.hash_preset)),
  631. '{} {}'.format(make_chksum_6(slt_fmt),split_into_cols(4,slt_fmt)),
  632. '{} {}'.format(make_chksum_6(es_fmt), split_into_cols(4,es_fmt))
  633. )
  634. chksum = make_chksum_6(' '.join(lines).encode())
  635. self.fmt_data = '\n'.join((chksum,)+lines) + '\n'
  636. def _deformat(self):
  637. def check_master_chksum(lines,desc):
  638. if len(lines) != 6:
  639. msg('Invalid number of lines ({}) in {} data'.format(len(lines),desc))
  640. return False
  641. if not is_chksum_6(lines[0]):
  642. msg('Incorrect master checksum ({}) in {} data'.format(lines[0],desc))
  643. return False
  644. chk = make_chksum_6(' '.join(lines[1:]))
  645. if not compare_chksums(lines[0],'master',chk,'computed',
  646. hdr='For wallet master checksum',verbose=True):
  647. return False
  648. return True
  649. lines = self.fmt_data.splitlines()
  650. if not check_master_chksum(lines,self.desc):
  651. return False
  652. d = self.ssdata
  653. d.label = MMGenWalletLabel(lines[1])
  654. d1,d2,d3,d4,d5 = lines[2].split()
  655. d.seed_id = d1.upper()
  656. d.key_id = d2.upper()
  657. check_usr_seed_len(int(d3))
  658. d.pw_status,d.timestamp = d4,d5
  659. hpdata = lines[3].split()
  660. d.hash_preset = hp = hpdata[0][:-1] # a string!
  661. qmsg("Hash preset of wallet: '{}'".format(hp))
  662. if opt.hash_preset and opt.hash_preset != hp:
  663. qmsg('Warning: ignoring user-requested hash preset {opt.hash_preset}')
  664. hash_params = list(map(int,hpdata[1:]))
  665. if hash_params != get_hash_params(d.hash_preset):
  666. msg(f"Hash parameters {' '.join(hash_params)!r} don't match hash preset {d.hash_preset!r}")
  667. return False
  668. lmin,foo,lmax = sorted(baseconv.seedlen_map_rev['b58']) # 22,33,44
  669. for i,key in (4,'salt'),(5,'enc_seed'):
  670. l = lines[i].split(' ')
  671. chk = l.pop(0)
  672. b58_val = ''.join(l)
  673. if len(b58_val) < lmin or len(b58_val) > lmax:
  674. msg('Invalid format for {} in {}: {}'.format(key,self.desc,l))
  675. return False
  676. if not compare_chksums(chk,key,
  677. make_chksum_6(b58_val),'computed checksum',verbose=True):
  678. return False
  679. val = baseconv.tobytes(b58_val,'b58',pad='seed')
  680. if val == False:
  681. msg('Invalid base 58 number: {}'.format(b58_val))
  682. return False
  683. setattr(d,key,val)
  684. return True
  685. def _decrypt(self):
  686. d = self.ssdata
  687. # Needed for multiple transactions with {}-txsign
  688. self._get_passphrase(
  689. add_desc = os.path.basename(self.infile.name) if opt.quiet else '' )
  690. key = make_key(d.passwd, d.salt, d.hash_preset)
  691. ret = decrypt_seed(d.enc_seed, key, d.seed_id, d.key_id)
  692. if ret:
  693. self.seed = Seed(ret)
  694. return True
  695. else:
  696. return False
  697. def _filename(self):
  698. s = self.seed
  699. d = self.ssdata
  700. return '{}-{}[{},{}]{x}.{}'.format(
  701. s.fn_stem,
  702. d.key_id,
  703. s.bitlen,
  704. d.hash_preset,
  705. self.ext,
  706. x='-α' if g.debug_utf8 else '')
  707. class Brainwallet(WalletEnc):
  708. stdin_ok = True
  709. fmt_codes = ('mmbrain','brainwallet','brain','bw','b')
  710. desc = 'brainwallet'
  711. ext = 'mmbrain'
  712. # brainwallet warning message? TODO
  713. def get_bw_params(self):
  714. # already checked
  715. a = opt.brain_params.split(',')
  716. return int(a[0]),a[1]
  717. def _deformat(self):
  718. self.brainpasswd = ' '.join(self.fmt_data.split())
  719. return True
  720. def _decrypt(self):
  721. d = self.ssdata
  722. if opt.brain_params:
  723. """
  724. Don't set opt.seed_len! When using multiple wallets, BW seed len might differ from others
  725. """
  726. bw_seed_len,d.hash_preset = self.get_bw_params()
  727. else:
  728. if not opt.seed_len:
  729. qmsg(f'Using default seed length of {yellow(str(g.dfl_seed_len))} bits\n'
  730. + 'If this is not what you want, use the --seed-len option' )
  731. self._get_hash_preset()
  732. bw_seed_len = opt.seed_len or g.dfl_seed_len
  733. qmsg_r('Hashing brainwallet data. Please wait...')
  734. # Use buflen arg of scrypt.hash() to get seed of desired length
  735. seed = scrypt_hash_passphrase(
  736. self.brainpasswd.encode(),
  737. b'',
  738. d.hash_preset,
  739. buflen = bw_seed_len // 8 )
  740. qmsg('Done')
  741. self.seed = Seed(seed)
  742. msg('Seed ID: {}'.format(self.seed.sid))
  743. qmsg('Check this value against your records')
  744. return True
  745. def _format(self):
  746. raise NotImplementedError('Brainwallet not supported as an output format')
  747. def _encrypt(self):
  748. raise NotImplementedError('Brainwallet not supported as an output format')
  749. class IncogWalletBase(WalletEnc):
  750. _msg = {
  751. 'check_incog_id': """
  752. Check the generated Incog ID above against your records. If it doesn't
  753. match, then your incognito data is incorrect or corrupted.
  754. """,
  755. 'record_incog_id': """
  756. Make a record of the Incog ID but keep it secret. You will use it to
  757. identify your incog wallet data in the future.
  758. """,
  759. 'incorrect_incog_passphrase_try_again': """
  760. Incorrect passphrase, hash preset, or maybe old-format incog wallet.
  761. Try again? (Y)es, (n)o, (m)ore information:
  762. """.strip(),
  763. 'confirm_seed_id': """
  764. If the Seed ID above is correct but you're seeing this message, then you need
  765. to exit and re-run the program with the '--old-incog-fmt' option.
  766. """.strip(),
  767. 'dec_chk': " {} hash preset"
  768. }
  769. def _make_iv_chksum(self,s): return sha256(s).hexdigest()[:8].upper()
  770. def _get_incog_data_len(self,seed_len):
  771. e = (g.hincog_chk_len,0)[bool(opt.old_incog_fmt)]
  772. return g.aesctr_iv_len + g.salt_len + e + seed_len//8
  773. def _incog_data_size_chk(self):
  774. # valid sizes: 56, 64, 72
  775. dlen = len(self.fmt_data)
  776. seed_len = opt.seed_len or g.dfl_seed_len
  777. valid_dlen = self._get_incog_data_len(seed_len)
  778. if dlen == valid_dlen:
  779. return True
  780. else:
  781. if opt.old_incog_fmt:
  782. msg('WARNING: old-style incognito format requested. Are you sure this is correct?')
  783. msg(f'Invalid incognito data size ({dlen} bytes) for this seed length ({seed_len} bits)')
  784. msg(f'Valid data size for this seed length: {valid_dlen} bytes')
  785. for sl in g.seed_lens:
  786. if dlen == self._get_incog_data_len(sl):
  787. die(1,f'Valid seed length for this data size: {sl} bits')
  788. msg(f'This data size ({dlen} bytes) is invalid for all available seed lengths')
  789. return False
  790. def _encrypt (self):
  791. self._get_first_pw_and_hp_and_encrypt_seed()
  792. if opt.old_incog_fmt:
  793. die(1,'Writing old-format incog wallets is unsupported')
  794. d = self.ssdata
  795. # IV is used BOTH to initialize counter and to salt password!
  796. d.iv = get_random(g.aesctr_iv_len)
  797. d.iv_id = self._make_iv_chksum(d.iv)
  798. msg('New Incog Wallet ID: {}'.format(d.iv_id))
  799. qmsg('Make a record of this value')
  800. vmsg(self.msg['record_incog_id'])
  801. d.salt = get_random(g.salt_len)
  802. key = make_key(d.passwd, d.salt, d.hash_preset, 'incog wallet key')
  803. chk = sha256(self.seed.data).digest()[:8]
  804. d.enc_seed = encrypt_data(chk+self.seed.data, key, g.aesctr_dfl_iv, 'seed')
  805. d.wrapper_key = make_key(d.passwd, d.iv, d.hash_preset, 'incog wrapper key')
  806. d.key_id = make_chksum_8(d.wrapper_key)
  807. vmsg('Key ID: {}'.format(d.key_id))
  808. d.target_data_len = self._get_incog_data_len(self.seed.bitlen)
  809. def _format(self):
  810. d = self.ssdata
  811. self.fmt_data = d.iv + encrypt_data(d.salt+d.enc_seed, d.wrapper_key, d.iv, self.desc)
  812. def _filename(self):
  813. s = self.seed
  814. d = self.ssdata
  815. return '{}-{}-{}[{},{}]{x}.{}'.format(
  816. s.fn_stem,
  817. d.key_id,
  818. d.iv_id,
  819. s.bitlen,
  820. d.hash_preset,
  821. self.ext,
  822. x='-α' if g.debug_utf8 else '')
  823. def _deformat(self):
  824. if not self._incog_data_size_chk():
  825. return False
  826. d = self.ssdata
  827. d.iv = self.fmt_data[0:g.aesctr_iv_len]
  828. d.incog_id = self._make_iv_chksum(d.iv)
  829. d.enc_incog_data = self.fmt_data[g.aesctr_iv_len:]
  830. msg('Incog Wallet ID: {}'.format(d.incog_id))
  831. qmsg('Check this value against your records')
  832. vmsg(self.msg['check_incog_id'])
  833. return True
  834. def _verify_seed_newfmt(self,data):
  835. chk,seed = data[:8],data[8:]
  836. if sha256(seed).digest()[:8] == chk:
  837. qmsg('Passphrase{} are correct'.format(self.msg['dec_chk'].format('and')))
  838. return seed
  839. else:
  840. msg('Incorrect passphrase{}'.format(self.msg['dec_chk'].format('or')))
  841. return False
  842. def _verify_seed_oldfmt(self,seed):
  843. m = 'Seed ID: {}. Is the Seed ID correct?'.format(make_chksum_8(seed))
  844. if keypress_confirm(m, True):
  845. return seed
  846. else:
  847. return False
  848. def _decrypt(self):
  849. d = self.ssdata
  850. self._get_hash_preset(add_desc=d.incog_id)
  851. self._get_passphrase(add_desc=d.incog_id)
  852. # IV is used BOTH to initialize counter and to salt password!
  853. key = make_key(d.passwd, d.iv, d.hash_preset, 'wrapper key')
  854. dd = decrypt_data(d.enc_incog_data, key, d.iv, 'incog data')
  855. d.salt = dd[0:g.salt_len]
  856. d.enc_seed = dd[g.salt_len:]
  857. key = make_key(d.passwd, d.salt, d.hash_preset, 'main key')
  858. qmsg('Key ID: {}'.format(make_chksum_8(key)))
  859. verify_seed = getattr(self,'_verify_seed_'+
  860. ('newfmt','oldfmt')[bool(opt.old_incog_fmt)])
  861. seed = verify_seed(decrypt_seed(d.enc_seed, key, '', ''))
  862. if seed:
  863. self.seed = Seed(seed)
  864. msg('Seed ID: {}'.format(self.seed.sid))
  865. return True
  866. else:
  867. return False
  868. class IncogWallet(IncogWalletBase):
  869. desc = 'incognito data'
  870. fmt_codes = ('mmincog','incog','icg','i')
  871. ext = 'mmincog'
  872. file_mode = 'binary'
  873. no_tty = True
  874. class IncogWalletHex(IncogWalletBase):
  875. desc = 'hex incognito data'
  876. fmt_codes = ('mmincox','incox','incog_hex','xincog','ix','xi')
  877. ext = 'mmincox'
  878. file_mode = 'text'
  879. no_tty = False
  880. def _deformat(self):
  881. ret = decode_pretty_hexdump(self.fmt_data)
  882. if ret:
  883. self.fmt_data = ret
  884. return super()._deformat()
  885. else:
  886. return False
  887. def _format(self):
  888. super()._format()
  889. self.fmt_data = pretty_hexdump(self.fmt_data)
  890. class IncogWalletHidden(IncogWalletBase):
  891. desc = 'hidden incognito data'
  892. fmt_codes = ('incog_hidden','hincog','ih','hi')
  893. ext = None
  894. file_mode = 'binary'
  895. no_tty = True
  896. _msg = {
  897. 'choose_file_size': """
  898. You must choose a size for your new hidden incog data. The minimum size is
  899. {} bytes, which puts the incog data right at the end of the file. Since you
  900. probably want to hide your data somewhere in the middle of the file where it's
  901. harder to find, you're advised to choose a much larger file size than this.
  902. """.strip(),
  903. 'check_incog_id': """
  904. Check generated Incog ID above against your records. If it doesn't
  905. match, then your incognito data is incorrect or corrupted, or you
  906. may have specified an incorrect offset.
  907. """,
  908. 'record_incog_id': """
  909. Make a record of the Incog ID but keep it secret. You will used it to
  910. identify the incog wallet data in the future and to locate the offset
  911. where the data is hidden in the event you forget it.
  912. """,
  913. 'dec_chk': ', hash preset, offset {} seed length'
  914. }
  915. def _get_hincog_params(self,wtype):
  916. a = getattr(opt,'hidden_incog_'+ wtype +'_params').split(',')
  917. return ','.join(a[:-1]),int(a[-1]) # permit comma in filename
  918. def _check_valid_offset(self,fn,action):
  919. d = self.ssdata
  920. m = ('Input','Destination')[action=='write']
  921. if fn.size < d.hincog_offset + d.target_data_len:
  922. fs = "{} file '{}' has length {}, too short to {} {} bytes of data at offset {}"
  923. die(1,fs.format(m,fn.name,fn.size,action,d.target_data_len,d.hincog_offset))
  924. def _get_data(self):
  925. d = self.ssdata
  926. d.hincog_offset = self._get_hincog_params('input')[1]
  927. qmsg("Getting hidden incog data from file '{}'".format(self.infile.name))
  928. # Already sanity-checked:
  929. d.target_data_len = self._get_incog_data_len(opt.seed_len or g.dfl_seed_len)
  930. self._check_valid_offset(self.infile,'read')
  931. flgs = os.O_RDONLY|os.O_BINARY if g.platform == 'win' else os.O_RDONLY
  932. fh = os.open(self.infile.name,flgs)
  933. os.lseek(fh,int(d.hincog_offset),os.SEEK_SET)
  934. self.fmt_data = os.read(fh,d.target_data_len)
  935. os.close(fh)
  936. qmsg("Data read from file '{}' at offset {}".format(self.infile.name,d.hincog_offset))
  937. # overrides method in Wallet
  938. def write_to_file(self):
  939. d = self.ssdata
  940. self._format()
  941. compare_or_die(d.target_data_len, 'target data length',
  942. len(self.fmt_data),'length of formatted ' + self.desc)
  943. k = ('output','input')[self.op=='pwchg_new']
  944. fn,d.hincog_offset = self._get_hincog_params(k)
  945. if opt.outdir and not os.path.dirname(fn):
  946. fn = os.path.join(opt.outdir,fn)
  947. check_offset = True
  948. try:
  949. os.stat(fn)
  950. except:
  951. if keypress_confirm("Requested file '{}' does not exist. Create?".format(fn),default_yes=True):
  952. min_fsize = d.target_data_len + d.hincog_offset
  953. msg(self.msg['choose_file_size'].format(min_fsize))
  954. while True:
  955. fsize = parse_bytespec(my_raw_input('Enter file size: '))
  956. if fsize >= min_fsize:
  957. break
  958. msg('File size must be an integer no less than {}'.format(min_fsize))
  959. from .tool import MMGenToolCmdFileUtil
  960. MMGenToolCmdFileUtil().rand2file(fn,str(fsize))
  961. check_offset = False
  962. else:
  963. die(1,'Exiting at user request')
  964. from .filename import Filename
  965. f = Filename(fn,ftype=type(self),write=True)
  966. dmsg('{} data len {}, offset {}'.format(capfirst(self.desc),d.target_data_len,d.hincog_offset))
  967. if check_offset:
  968. self._check_valid_offset(f,'write')
  969. if not opt.quiet:
  970. confirm_or_raise('',"alter file '{}'".format(f.name))
  971. flgs = os.O_RDWR|os.O_BINARY if g.platform == 'win' else os.O_RDWR
  972. fh = os.open(f.name,flgs)
  973. os.lseek(fh, int(d.hincog_offset), os.SEEK_SET)
  974. os.write(fh, self.fmt_data)
  975. os.close(fh)
  976. msg("{} written to file '{}' at offset {}".format(capfirst(self.desc),f.name,d.hincog_offset))