xmrseed.py 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2024 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. xmrseed: Monero mnemonic conversion class for the MMGen suite
  20. """
  21. from .baseconv import baseconv
  22. from .util import die
  23. def is_xmrseed(s):
  24. return bool(xmrseed().tobytes(s.split()))
  25. # implements a subset of the baseconv API
  26. class xmrseed(baseconv):
  27. desc = baseconv.dt('Monero mnemonic', 'Monero new-style mnemonic seed phrase')
  28. wl_chksum = '3c381ebb'
  29. seedlen_map = {32: 25}
  30. seedlen_map_rev = {25: 32}
  31. def __init__(self, wl_id='xmrseed'):
  32. assert wl_id == 'xmrseed', "initialize with 'xmrseed' for compatibility with baseconv API"
  33. from .wordlist.monero import words
  34. self.digits = words
  35. self.wl_id = 'xmrseed'
  36. @staticmethod
  37. def monero_mn_checksum(words):
  38. from binascii import crc32
  39. wstr = ''.join(word[:3] for word in words)
  40. return words[crc32(wstr.encode()) % len(words)]
  41. def tobytes(self, words_arg, pad=None):
  42. assert isinstance(words_arg, (list, tuple)), 'words must be list or tuple'
  43. assert pad is None, f"{pad}: invalid 'pad' argument (must be None)"
  44. words = words_arg
  45. desc = self.desc.short
  46. wl = self.digits
  47. base = len(wl)
  48. if not set(words) <= set(wl):
  49. die('MnemonicError', f'{words!r}: not in {desc} format')
  50. if len(words) not in self.seedlen_map_rev:
  51. die('MnemonicError', f'{len(words)}: invalid seed phrase length for {desc}')
  52. z = self.monero_mn_checksum(words[:-1])
  53. if z != words[-1]:
  54. die('MnemonicError', f'invalid {desc} checksum')
  55. words = tuple(words[:-1])
  56. def gen():
  57. for i in range(len(words)//3):
  58. w1, w2, w3 = [wl.index(w) for w in words[3*i:3*i+3]]
  59. x = w1 + base*((w2-w1)%base) + base*base*((w3-w2)%base)
  60. yield x.to_bytes(4, 'big')[::-1]
  61. return b''.join(gen())
  62. def frombytes(self, bytestr, pad=None, tostr=False):
  63. assert pad is None, f"{pad}: invalid 'pad' argument (must be None)"
  64. desc = self.desc.short
  65. wl = self.digits
  66. base = len(wl)
  67. if len(bytestr) not in self.seedlen_map:
  68. die('SeedLengthError', f'{len(bytestr)}: invalid seed byte length for {desc}')
  69. def num2base_monero(num):
  70. w1 = num % base
  71. w2 = (num//base + w1) % base
  72. w3 = (num//base//base + w2) % base
  73. return (wl[w1], wl[w2], wl[w3])
  74. def gen():
  75. for i in range(len(bytestr)//4):
  76. yield from num2base_monero(int.from_bytes(bytestr[i*4:i*4+4][::-1], 'big'))
  77. o = list(gen())
  78. o.append(self.monero_mn_checksum(o))
  79. return ' '.join(o) if tostr else tuple(o)