diff --git a/README.md b/README.md index 43ac71ab..e2ef1f51 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ This package allows generating mnemonics, seeds, private/public keys and address - Mnemonic and seed generation for [Substrate](https://wiki.polkadot.network/docs/learn-accounts#seed-generation) (Polkadot/Kusama ecosystem) - Keys derivation for [Substrate](https://wiki.polkadot.network/docs/learn-accounts#derivation-paths) (Polkadot/Kusama ecosystem, same as Polkadot-JS) - Keys and addresses generation for Cardano (Byron-Legacy, Byron-Icarus and Shelley, same as Ledger and AdaLite/Yoroi wallets) -- Mnemonic and seed generation for Monero +- Mnemonic and seed generation for Monero (legacy and Polyseed) - Keys and addresses/subaddresses generation for Monero (same as the official Monero wallet) - Mnemonic and seed generation for Algorand (Algorand 25-word mnemonic) - Mnemonic and seed generation like Electrum wallet (v1 and v2) diff --git a/bip_utils/__init__.py b/bip_utils/__init__.py index c7815ab5..dc136499 100644 --- a/bip_utils/__init__.py +++ b/bip_utils/__init__.py @@ -327,7 +327,7 @@ from bip_utils.monero.conf import MoneroCoins, MoneroConf # Monero mnemonic -from bip_utils.monero.mnemonic import ( +from bip_utils.monero.mnemonic_legacy import ( MoneroEntropyBitLen, MoneroEntropyGenerator, MoneroLanguages, @@ -342,6 +342,23 @@ MoneroWordsNum, ) +# Monero Polyseed mnemonic +from bip_utils.monero.mnemonic_polyseed import ( + MoneroPolyseedCoins, + MoneroPolyseedDecodedData, + MoneroPolyseedEntropyBitLen, + MoneroPolyseedEntropyGenerator, + MoneroPolyseedLanguages, + MoneroPolyseedMnemonic, + MoneroPolyseedMnemonicDecoder, + MoneroPolyseedMnemonicEncoder, + MoneroPolyseedMnemonicEncrypter, + MoneroPolyseedMnemonicGenerator, + MoneroPolyseedMnemonicValidator, + MoneroPolyseedSeedGenerator, + MoneroPolyseedWordsNum, +) + # SLIP32 from bip_utils.slip.slip32 import ( Slip32DeserializedKey, diff --git a/bip_utils/monero/mnemonic/__init__.py b/bip_utils/monero/mnemonic/__init__.py deleted file mode 100644 index aa961204..00000000 --- a/bip_utils/monero/mnemonic/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from bip_utils.monero.mnemonic.monero_entropy_generator import MoneroEntropyBitLen, MoneroEntropyGenerator -from bip_utils.monero.mnemonic.monero_mnemonic import MoneroLanguages, MoneroMnemonic, MoneroWordsNum -from bip_utils.monero.mnemonic.monero_mnemonic_decoder import MoneroMnemonicDecoder -from bip_utils.monero.mnemonic.monero_mnemonic_encoder import ( - MoneroMnemonicEncoder, - MoneroMnemonicNoChecksumEncoder, - MoneroMnemonicWithChecksumEncoder, -) -from bip_utils.monero.mnemonic.monero_mnemonic_generator import MoneroMnemonicGenerator -from bip_utils.monero.mnemonic.monero_mnemonic_validator import MoneroMnemonicValidator -from bip_utils.monero.mnemonic.monero_seed_generator import MoneroSeedGenerator diff --git a/bip_utils/monero/mnemonic_legacy/__init__.py b/bip_utils/monero/mnemonic_legacy/__init__.py new file mode 100644 index 00000000..41d4fecc --- /dev/null +++ b/bip_utils/monero/mnemonic_legacy/__init__.py @@ -0,0 +1,11 @@ +from bip_utils.monero.mnemonic_legacy.monero_entropy_generator import MoneroEntropyBitLen, MoneroEntropyGenerator +from bip_utils.monero.mnemonic_legacy.monero_mnemonic import MoneroLanguages, MoneroMnemonic, MoneroWordsNum +from bip_utils.monero.mnemonic_legacy.monero_mnemonic_decoder import MoneroMnemonicDecoder +from bip_utils.monero.mnemonic_legacy.monero_mnemonic_encoder import ( + MoneroMnemonicEncoder, + MoneroMnemonicNoChecksumEncoder, + MoneroMnemonicWithChecksumEncoder, +) +from bip_utils.monero.mnemonic_legacy.monero_mnemonic_generator import MoneroMnemonicGenerator +from bip_utils.monero.mnemonic_legacy.monero_mnemonic_validator import MoneroMnemonicValidator +from bip_utils.monero.mnemonic_legacy.monero_seed_generator import MoneroSeedGenerator diff --git a/bip_utils/monero/mnemonic/monero_entropy_generator.py b/bip_utils/monero/mnemonic_legacy/monero_entropy_generator.py similarity index 100% rename from bip_utils/monero/mnemonic/monero_entropy_generator.py rename to bip_utils/monero/mnemonic_legacy/monero_entropy_generator.py diff --git a/bip_utils/monero/mnemonic/monero_mnemonic.py b/bip_utils/monero/mnemonic_legacy/monero_mnemonic.py similarity index 100% rename from bip_utils/monero/mnemonic/monero_mnemonic.py rename to bip_utils/monero/mnemonic_legacy/monero_mnemonic.py diff --git a/bip_utils/monero/mnemonic/monero_mnemonic_decoder.py b/bip_utils/monero/mnemonic_legacy/monero_mnemonic_decoder.py similarity index 95% rename from bip_utils/monero/mnemonic/monero_mnemonic_decoder.py rename to bip_utils/monero/mnemonic_legacy/monero_mnemonic_decoder.py index e2b34770..0b1466b7 100644 --- a/bip_utils/monero/mnemonic/monero_mnemonic_decoder.py +++ b/bip_utils/monero/mnemonic_legacy/monero_mnemonic_decoder.py @@ -25,8 +25,8 @@ from typing_extensions import override -from bip_utils.monero.mnemonic.monero_mnemonic import MoneroLanguages, MoneroMnemonic, MoneroMnemonicConst -from bip_utils.monero.mnemonic.monero_mnemonic_utils import ( +from bip_utils.monero.mnemonic_legacy.monero_mnemonic import MoneroLanguages, MoneroMnemonic, MoneroMnemonicConst +from bip_utils.monero.mnemonic_legacy.monero_mnemonic_utils import ( MoneroMnemonicUtils, MoneroWordsListFinder, MoneroWordsListGetter, diff --git a/bip_utils/monero/mnemonic/monero_mnemonic_encoder.py b/bip_utils/monero/mnemonic_legacy/monero_mnemonic_encoder.py similarity index 95% rename from bip_utils/monero/mnemonic/monero_mnemonic_encoder.py rename to bip_utils/monero/mnemonic_legacy/monero_mnemonic_encoder.py index 7e5ada4b..18162e64 100644 --- a/bip_utils/monero/mnemonic/monero_mnemonic_encoder.py +++ b/bip_utils/monero/mnemonic_legacy/monero_mnemonic_encoder.py @@ -26,9 +26,9 @@ from typing_extensions import override -from bip_utils.monero.mnemonic.monero_entropy_generator import MoneroEntropyGenerator -from bip_utils.monero.mnemonic.monero_mnemonic import MoneroLanguages, MoneroMnemonic -from bip_utils.monero.mnemonic.monero_mnemonic_utils import MoneroMnemonicUtils, MoneroWordsListGetter +from bip_utils.monero.mnemonic_legacy.monero_entropy_generator import MoneroEntropyGenerator +from bip_utils.monero.mnemonic_legacy.monero_mnemonic import MoneroLanguages, MoneroMnemonic +from bip_utils.monero.mnemonic_legacy.monero_mnemonic_utils import MoneroMnemonicUtils, MoneroWordsListGetter from bip_utils.utils.mnemonic import Mnemonic, MnemonicEncoderBase, MnemonicUtils diff --git a/bip_utils/monero/mnemonic/monero_mnemonic_generator.py b/bip_utils/monero/mnemonic_legacy/monero_mnemonic_generator.py similarity index 93% rename from bip_utils/monero/mnemonic/monero_mnemonic_generator.py rename to bip_utils/monero/mnemonic_legacy/monero_mnemonic_generator.py index 34e9789d..8c1f455b 100644 --- a/bip_utils/monero/mnemonic/monero_mnemonic_generator.py +++ b/bip_utils/monero/mnemonic_legacy/monero_mnemonic_generator.py @@ -23,9 +23,9 @@ # Imports from typing import Dict, Union -from bip_utils.monero.mnemonic.monero_entropy_generator import MoneroEntropyBitLen, MoneroEntropyGenerator -from bip_utils.monero.mnemonic.monero_mnemonic import MoneroLanguages, MoneroMnemonicConst, MoneroWordsNum -from bip_utils.monero.mnemonic.monero_mnemonic_encoder import MoneroMnemonicEncoder +from bip_utils.monero.mnemonic_legacy.monero_entropy_generator import MoneroEntropyBitLen, MoneroEntropyGenerator +from bip_utils.monero.mnemonic_legacy.monero_mnemonic import MoneroLanguages, MoneroMnemonicConst, MoneroWordsNum +from bip_utils.monero.mnemonic_legacy.monero_mnemonic_encoder import MoneroMnemonicEncoder from bip_utils.utils.mnemonic import Mnemonic diff --git a/bip_utils/monero/mnemonic/monero_mnemonic_utils.py b/bip_utils/monero/mnemonic_legacy/monero_mnemonic_utils.py similarity index 97% rename from bip_utils/monero/mnemonic/monero_mnemonic_utils.py rename to bip_utils/monero/mnemonic_legacy/monero_mnemonic_utils.py index c3ad5e3d..4f58fe47 100644 --- a/bip_utils/monero/mnemonic/monero_mnemonic_utils.py +++ b/bip_utils/monero/mnemonic_legacy/monero_mnemonic_utils.py @@ -26,7 +26,7 @@ from typing_extensions import override -from bip_utils.monero.mnemonic.monero_mnemonic import MoneroLanguages, MoneroMnemonicConst +from bip_utils.monero.mnemonic_legacy.monero_mnemonic import MoneroLanguages, MoneroMnemonicConst from bip_utils.utils.crypto import Crc32 from bip_utils.utils.mnemonic import ( Mnemonic, diff --git a/bip_utils/monero/mnemonic/monero_mnemonic_validator.py b/bip_utils/monero/mnemonic_legacy/monero_mnemonic_validator.py similarity index 90% rename from bip_utils/monero/mnemonic/monero_mnemonic_validator.py rename to bip_utils/monero/mnemonic_legacy/monero_mnemonic_validator.py index c5e46ea3..c7d29faf 100644 --- a/bip_utils/monero/mnemonic/monero_mnemonic_validator.py +++ b/bip_utils/monero/mnemonic_legacy/monero_mnemonic_validator.py @@ -23,8 +23,8 @@ # Imports from typing import Optional -from bip_utils.monero.mnemonic.monero_mnemonic import MoneroLanguages -from bip_utils.monero.mnemonic.monero_mnemonic_decoder import MoneroMnemonicDecoder +from bip_utils.monero.mnemonic_legacy.monero_mnemonic import MoneroLanguages +from bip_utils.monero.mnemonic_legacy.monero_mnemonic_decoder import MoneroMnemonicDecoder from bip_utils.utils.mnemonic import MnemonicValidator diff --git a/bip_utils/monero/mnemonic/monero_seed_generator.py b/bip_utils/monero/mnemonic_legacy/monero_seed_generator.py similarity index 93% rename from bip_utils/monero/mnemonic/monero_seed_generator.py rename to bip_utils/monero/mnemonic_legacy/monero_seed_generator.py index dbc6e40c..a58c4f31 100644 --- a/bip_utils/monero/mnemonic/monero_seed_generator.py +++ b/bip_utils/monero/mnemonic_legacy/monero_seed_generator.py @@ -23,8 +23,8 @@ # Imports from typing import Optional, Union -from bip_utils.monero.mnemonic.monero_mnemonic import MoneroLanguages -from bip_utils.monero.mnemonic.monero_mnemonic_decoder import MoneroMnemonicDecoder +from bip_utils.monero.mnemonic_legacy.monero_mnemonic import MoneroLanguages +from bip_utils.monero.mnemonic_legacy.monero_mnemonic_decoder import MoneroMnemonicDecoder from bip_utils.utils.mnemonic import Mnemonic diff --git a/bip_utils/monero/mnemonic/wordlist/chinese_simplified.txt b/bip_utils/monero/mnemonic_legacy/wordlist/chinese_simplified.txt similarity index 100% rename from bip_utils/monero/mnemonic/wordlist/chinese_simplified.txt rename to bip_utils/monero/mnemonic_legacy/wordlist/chinese_simplified.txt diff --git a/bip_utils/monero/mnemonic/wordlist/dutch.txt b/bip_utils/monero/mnemonic_legacy/wordlist/dutch.txt similarity index 100% rename from bip_utils/monero/mnemonic/wordlist/dutch.txt rename to bip_utils/monero/mnemonic_legacy/wordlist/dutch.txt diff --git a/bip_utils/monero/mnemonic/wordlist/english.txt b/bip_utils/monero/mnemonic_legacy/wordlist/english.txt similarity index 100% rename from bip_utils/monero/mnemonic/wordlist/english.txt rename to bip_utils/monero/mnemonic_legacy/wordlist/english.txt diff --git a/bip_utils/monero/mnemonic/wordlist/french.txt b/bip_utils/monero/mnemonic_legacy/wordlist/french.txt similarity index 100% rename from bip_utils/monero/mnemonic/wordlist/french.txt rename to bip_utils/monero/mnemonic_legacy/wordlist/french.txt diff --git a/bip_utils/monero/mnemonic/wordlist/german.txt b/bip_utils/monero/mnemonic_legacy/wordlist/german.txt similarity index 100% rename from bip_utils/monero/mnemonic/wordlist/german.txt rename to bip_utils/monero/mnemonic_legacy/wordlist/german.txt diff --git a/bip_utils/monero/mnemonic/wordlist/italian.txt b/bip_utils/monero/mnemonic_legacy/wordlist/italian.txt similarity index 100% rename from bip_utils/monero/mnemonic/wordlist/italian.txt rename to bip_utils/monero/mnemonic_legacy/wordlist/italian.txt diff --git a/bip_utils/monero/mnemonic/wordlist/japanese.txt b/bip_utils/monero/mnemonic_legacy/wordlist/japanese.txt similarity index 100% rename from bip_utils/monero/mnemonic/wordlist/japanese.txt rename to bip_utils/monero/mnemonic_legacy/wordlist/japanese.txt diff --git a/bip_utils/monero/mnemonic/wordlist/portuguese.txt b/bip_utils/monero/mnemonic_legacy/wordlist/portuguese.txt similarity index 100% rename from bip_utils/monero/mnemonic/wordlist/portuguese.txt rename to bip_utils/monero/mnemonic_legacy/wordlist/portuguese.txt diff --git a/bip_utils/monero/mnemonic/wordlist/russian.txt b/bip_utils/monero/mnemonic_legacy/wordlist/russian.txt similarity index 100% rename from bip_utils/monero/mnemonic/wordlist/russian.txt rename to bip_utils/monero/mnemonic_legacy/wordlist/russian.txt diff --git a/bip_utils/monero/mnemonic/wordlist/spanish.txt b/bip_utils/monero/mnemonic_legacy/wordlist/spanish.txt similarity index 100% rename from bip_utils/monero/mnemonic/wordlist/spanish.txt rename to bip_utils/monero/mnemonic_legacy/wordlist/spanish.txt diff --git a/bip_utils/monero/mnemonic_polyseed/__init__.py b/bip_utils/monero/mnemonic_polyseed/__init__.py new file mode 100644 index 00000000..66c66540 --- /dev/null +++ b/bip_utils/monero/mnemonic_polyseed/__init__.py @@ -0,0 +1,17 @@ +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_entropy_generator import ( + MoneroPolyseedEntropyBitLen, + MoneroPolyseedEntropyGenerator, +) +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_mnemonic import ( + MoneroPolyseedCoins, + MoneroPolyseedLanguages, + MoneroPolyseedMnemonic, + MoneroPolyseedWordsNum, +) +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_mnemonic_decoder import MoneroPolyseedMnemonicDecoder +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_mnemonic_encoder import MoneroPolyseedMnemonicEncoder +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_mnemonic_encrypter import MoneroPolyseedMnemonicEncrypter +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_mnemonic_generator import MoneroPolyseedMnemonicGenerator +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_mnemonic_utils import MoneroPolyseedDecodedData +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_mnemonic_validator import MoneroPolyseedMnemonicValidator +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_seed_generator import MoneroPolyseedSeedGenerator diff --git a/bip_utils/monero/mnemonic_polyseed/monero_polyseed_entropy_generator.py b/bip_utils/monero/mnemonic_polyseed/monero_polyseed_entropy_generator.py new file mode 100644 index 00000000..13912e70 --- /dev/null +++ b/bip_utils/monero/mnemonic_polyseed/monero_polyseed_entropy_generator.py @@ -0,0 +1,107 @@ +# Copyright (c) 2026 Emanuele Bellocchia +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""Module for Monero Polyseed entropy generation.""" + +# Imports +import os +from enum import IntEnum, unique +from typing import List, Union + +from typing_extensions import override + +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_mnemonic import MoneroPolyseedMnemonicConst +from bip_utils.utils.mnemonic import EntropyGenerator + + +@unique +class MoneroPolyseedEntropyBitLen(IntEnum): + """Enumerative for Monero Polyseed entropy bit lengths.""" + + BIT_LEN_150 = 150 + + +class MoneroPolyseedEntropyGeneratorConst: + """Class container for Monero Polyseed entropy generator constants.""" + + # Accepted entropy lengths in bit + ENTROPY_BIT_LEN: List[MoneroPolyseedEntropyBitLen] = [ + MoneroPolyseedEntropyBitLen.BIT_LEN_150, + ] + + +class MoneroPolyseedEntropyGenerator(EntropyGenerator): + """ + Monero Polyseed entropy generator class. + It generates random entropy bytes (19 bytes, 150 bits) with top 2 bits of the last byte cleared. + """ + + def __init__(self, + bit_len: Union[int, MoneroPolyseedEntropyBitLen] = MoneroPolyseedEntropyBitLen.BIT_LEN_150) -> None: + """ + Construct class. + + Args: + bit_len (int or MoneroPolyseedEntropyBitLen): Entropy length in bits + + Raises: + ValueError: If the bit length is not valid + """ + if not self.IsValidEntropyBitLen(bit_len): + raise ValueError(f"Entropy bit length is not valid ({bit_len})") + super().__init__(bit_len) + + @override + def Generate(self) -> bytes: + """ + Generate random entropy bytes (19 bytes with top 2 bits of the last byte cleared). + + Returns: + bytes: Generated entropy bytes + """ + entropy = bytearray(os.urandom(MoneroPolyseedMnemonicConst.SECRET_SIZE)) + entropy[MoneroPolyseedMnemonicConst.SECRET_SIZE - 1] &= MoneroPolyseedMnemonicConst.CLEAR_MASK + return bytes(entropy) + + @staticmethod + def IsValidEntropyBitLen(bit_len: Union[int, MoneroPolyseedEntropyBitLen]) -> bool: + """ + Get if the specified entropy bit length is valid. + + Args: + bit_len (int or MoneroPolyseedEntropyBitLen): Entropy length in bits + + Returns: + bool: True if valid, false otherwise + """ + return bit_len in MoneroPolyseedEntropyGeneratorConst.ENTROPY_BIT_LEN + + @staticmethod + def IsValidEntropyByteLen(byte_len: int) -> bool: + """ + Get if the specified entropy byte length is valid. + + Args: + byte_len (int): Entropy length in bytes + + Returns: + bool: True if valid, false otherwise + """ + return byte_len == MoneroPolyseedMnemonicConst.SECRET_SIZE diff --git a/bip_utils/monero/mnemonic_polyseed/monero_polyseed_mnemonic.py b/bip_utils/monero/mnemonic_polyseed/monero_polyseed_mnemonic.py new file mode 100644 index 00000000..d4261739 --- /dev/null +++ b/bip_utils/monero/mnemonic_polyseed/monero_polyseed_mnemonic.py @@ -0,0 +1,112 @@ +# Copyright (c) 2026 Emanuele Bellocchia +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""Module for Monero Polyseed mnemonic.""" + +# Imports +from enum import IntEnum, unique +from typing import List + +from bip_utils.bip.bip39 import Bip39Languages +from bip_utils.utils.mnemonic import Mnemonic, MnemonicLanguages + + +@unique +class MoneroPolyseedWordsNum(IntEnum): + """Enumerative for Monero Polyseed words number.""" + + WORDS_NUM_16 = 16 + + +@unique +class MoneroPolyseedLanguages(MnemonicLanguages): + """Enumerative for Monero Polyseed languages.""" + + CHINESE_SIMPLIFIED = Bip39Languages.CHINESE_SIMPLIFIED + CHINESE_TRADITIONAL = Bip39Languages.CHINESE_TRADITIONAL + ENGLISH = Bip39Languages.ENGLISH + FRENCH = Bip39Languages.FRENCH + ITALIAN = Bip39Languages.ITALIAN + KOREAN = Bip39Languages.KOREAN + PORTUGUESE = Bip39Languages.PORTUGUESE + + +@unique +class MoneroPolyseedCoins(IntEnum): + """Enumerative for Monero Polyseed coins.""" + + MONERO = 0 + AEON = 1 + WOWNERO = 2 + + +class MoneroPolyseedMnemonicConst: + """Class container for Monero Polyseed mnemonic constants.""" + + # Accepted mnemonic word numbers + MNEMONIC_WORD_NUM: List[MoneroPolyseedWordsNum] = [ + MoneroPolyseedWordsNum.WORDS_NUM_16, + ] + + # GF(2048) parameters + GF_BITS: int = 11 + GF_SIZE: int = 2048 + GF_MASK: int = 2047 + POLY_NUM_CHECK_DIGITS: int = 1 + NUM_WORDS: int = 16 + + # Secret parameters + SECRET_BITS: int = 150 + SECRET_SIZE: int = 19 + SECRET_BUFFER_SIZE: int = 32 + CLEAR_BITS: int = 2 + CLEAR_MASK: int = 0x3F + + # Date parameters + DATE_BITS: int = 10 + DATE_MASK: int = 1023 + EPOCH: int = 1635768000 + TIME_STEP: int = 2629746 + + # Feature parameters + FEATURE_BITS: int = 5 + FEATURE_MASK: int = 31 + USER_FEATURES: int = 3 + USER_FEATURES_MASK: int = 7 + ENCRYPTED_MASK: int = 16 + + # Share bits per word (from secret) + SHARE_BITS: int = 10 + + # Data words (non-check) + DATA_WORDS: int = 15 + + # KDF parameters + KDF_NUM_ITERATIONS: int = 10000 + KDF_KEY_SIZE: int = 32 + KDF_KEY_SALT_PREFIX: bytes = b"POLYSEED key" + KDF_MASK_SALT: bytes = b"POLYSEED mask\x00\xff\xff" + + # GF(2048) multiplication-by-2 lookup table + MUL2_TABLE: List[int] = [5, 7, 1, 3, 13, 15, 9, 11] + + +class MoneroPolyseedMnemonic(Mnemonic): + """Monero Polyseed mnemonic class (alias for Mnemonic).""" diff --git a/bip_utils/monero/mnemonic_polyseed/monero_polyseed_mnemonic_decoder.py b/bip_utils/monero/mnemonic_polyseed/monero_polyseed_mnemonic_decoder.py new file mode 100644 index 00000000..731d823f --- /dev/null +++ b/bip_utils/monero/mnemonic_polyseed/monero_polyseed_mnemonic_decoder.py @@ -0,0 +1,130 @@ +# Copyright (c) 2026 Emanuele Bellocchia +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""Module for Monero Polyseed mnemonic decoding.""" + +# Imports +from typing import Optional, Union + +from typing_extensions import override + +from bip_utils.bip.bip39.bip39_mnemonic_utils import Bip39WordsListFinder, Bip39WordsListGetter +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_mnemonic import ( + MoneroPolyseedCoins, + MoneroPolyseedLanguages, + MoneroPolyseedMnemonic, + MoneroPolyseedMnemonicConst, +) +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_mnemonic_utils import ( + MoneroPolyseedDecodedData, + MoneroPolyseedGf, + MoneroPolyseedMnemonicUtils, +) +from bip_utils.utils.mnemonic import ( + Mnemonic, + MnemonicChecksumError, + MnemonicDecoderBase, +) + + +class MoneroPolyseedMnemonicDecoder(MnemonicDecoderBase): + """ + Monero Polyseed mnemonic decoder class. + It decodes a 16-word mnemonic phrase to polyseed data. + """ + + m_coin: MoneroPolyseedCoins + + def __init__(self, + coin: MoneroPolyseedCoins = MoneroPolyseedCoins.MONERO, + lang: Optional[MoneroPolyseedLanguages] = None) -> None: + """ + Construct class. + + Args: + coin (MoneroPolyseedCoins, optional) : Coin for domain separation (default: MONERO) + lang (MoneroPolyseedLanguages, optional): Language, None for automatic detection + + Raises: + TypeError: If the language is not a MoneroPolyseedLanguages enum + ValueError: If loaded words list is not valid + """ + if lang is not None and not isinstance(lang, MoneroPolyseedLanguages): + raise TypeError("Language is not a MoneroPolyseedLanguages enum") + super().__init__(lang.value if lang is not None else lang, + Bip39WordsListFinder, + Bip39WordsListGetter) + self.m_coin = coin + + @override + def Decode(self, + mnemonic: Union[str, Mnemonic]) -> bytes: + """ + Decode a mnemonic phrase to secret bytes (19 bytes). + + Args: + mnemonic (str or Mnemonic object): Mnemonic + + Returns: + bytes: Decoded secret bytes (19 bytes) + + Raises: + MnemonicChecksumError: If checksum is not valid + ValueError: If mnemonic is not valid + """ + return self.DecodeWithData(mnemonic).secret + + def DecodeWithData(self, + mnemonic: Union[str, Mnemonic]) -> MoneroPolyseedDecodedData: + """ + Decode a mnemonic phrase to full polyseed data. + + Args: + mnemonic (str or Mnemonic object): Mnemonic + + Returns: + MoneroPolyseedDecodedData: Decoded polyseed data + + Raises: + MnemonicChecksumError: If checksum is not valid + ValueError: If mnemonic is not valid + """ + mnemonic_obj = MoneroPolyseedMnemonic.FromString(mnemonic) if isinstance(mnemonic, str) else mnemonic + + # Check mnemonic length + if mnemonic_obj.WordsCount() not in MoneroPolyseedMnemonicConst.MNEMONIC_WORD_NUM: + raise ValueError(f"Mnemonic words count is not valid ({mnemonic_obj.WordsCount()})") + + # Detect language if it was not specified at construction + words_list, _ = self._FindLanguage(mnemonic_obj) + + # Convert words to polynomial coefficients + words = mnemonic_obj.ToList() + coeffs = [words_list.GetWordIdx(w) for w in words] + + # Undo coin domain separation + coeffs[MoneroPolyseedMnemonicConst.POLY_NUM_CHECK_DIGITS] ^= int(self.m_coin) + + # Verify checksum + if not MoneroPolyseedGf.PolyCheck(coeffs): + raise MnemonicChecksumError("Invalid Polyseed checksum") + + # Extract data + return MoneroPolyseedMnemonicUtils.PolyToData(coeffs) diff --git a/bip_utils/monero/mnemonic_polyseed/monero_polyseed_mnemonic_encoder.py b/bip_utils/monero/mnemonic_polyseed/monero_polyseed_mnemonic_encoder.py new file mode 100644 index 00000000..04e7c14b --- /dev/null +++ b/bip_utils/monero/mnemonic_polyseed/monero_polyseed_mnemonic_encoder.py @@ -0,0 +1,163 @@ +# Copyright (c) 2026 Emanuele Bellocchia +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""Module for Monero Polyseed mnemonic encoding.""" + +# Imports +import time + +from typing_extensions import override + +from bip_utils.bip.bip39.bip39_mnemonic_utils import Bip39WordsListGetter +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_mnemonic import ( + MoneroPolyseedCoins, + MoneroPolyseedLanguages, + MoneroPolyseedMnemonic, + MoneroPolyseedMnemonicConst, +) +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_mnemonic_utils import ( + MoneroPolyseedDecodedData, + MoneroPolyseedGf, + MoneroPolyseedMnemonicUtils, +) +from bip_utils.utils.mnemonic import Mnemonic, MnemonicEncoderBase + + +class MoneroPolyseedMnemonicEncoder(MnemonicEncoderBase): + """ + Monero Polyseed mnemonic encoder class. + It encodes seed data to a 16-word mnemonic phrase. + """ + + m_lang: MoneroPolyseedLanguages + + def __init__(self, + lang: MoneroPolyseedLanguages = MoneroPolyseedLanguages.ENGLISH) -> None: + """ + Construct class. + + Args: + lang (MoneroPolyseedLanguages, optional): Language (default: English) + + Raises: + TypeError: If the language is not a MoneroPolyseedLanguages enum + ValueError: If loaded words list is not valid + """ + if not isinstance(lang, MoneroPolyseedLanguages): + raise TypeError("Language is not a MoneroPolyseedLanguages enum") + super().__init__(lang.value, Bip39WordsListGetter) + self.m_lang = lang + + @override + def Encode(self, + entropy_bytes: bytes) -> Mnemonic: + """ + Encode entropy bytes to mnemonic phrase using current time as birthday. + + Args: + entropy_bytes (bytes): Secret bytes (19 bytes) + + Returns: + Mnemonic: Encoded mnemonic phrase + + Raises: + ValueError: If the entropy length is not valid + """ + return self.EncodeWithData(entropy_bytes, int(time.time())) + + def EncodeWithData(self, + entropy_bytes: bytes, + birthday: int, + features: int = 0, + coin: MoneroPolyseedCoins = MoneroPolyseedCoins.MONERO) -> Mnemonic: + """ + Encode seed data to mnemonic phrase. + + Args: + entropy_bytes (bytes) : Secret bytes (19 bytes) + birthday (int) : Unix timestamp or raw 10-bit encoded birthday + features (int, optional) : User feature flags (0-7, default: 0) + coin (MoneroPolyseedCoins, optional): Target coin for domain separation (default: MONERO) + + Returns: + Mnemonic: Encoded mnemonic phrase + + Raises: + ValueError: If the entropy length is not valid + """ + # Validate entropy length + if len(entropy_bytes) != MoneroPolyseedMnemonicConst.SECRET_SIZE: + raise ValueError(f"Entropy byte length ({len(entropy_bytes)}) is not valid") + + # Clear top 2 bits of last byte + secret = bytearray(entropy_bytes) + secret[MoneroPolyseedMnemonicConst.SECRET_SIZE - 1] &= MoneroPolyseedMnemonicConst.CLEAR_MASK + + # Encode birthday from timestamp if needed + if birthday > MoneroPolyseedMnemonicConst.DATE_MASK: + birthday = MoneroPolyseedMnemonicUtils.BirthdayEncode(birthday) + + # Sanitize features to user-accessible bits only + features = features & MoneroPolyseedMnemonicConst.USER_FEATURES_MASK + + # Build data structure + data = MoneroPolyseedDecodedData( + secret=bytes(secret), + birthday=birthday, + features=features, + checksum=0, + ) + + # Convert to polynomial and calculate checksum + coeffs = MoneroPolyseedMnemonicUtils.DataToPoly(data) + coeffs = MoneroPolyseedGf.PolyEncode(coeffs) + + # Apply coin domain separation + coeffs[MoneroPolyseedMnemonicConst.POLY_NUM_CHECK_DIGITS] ^= int(coin) + + # Map coefficients to words + words = [self.m_words_list.GetWordAtIdx(c) for c in coeffs] + + return MoneroPolyseedMnemonic.FromList(words) + + def EncodeData(self, + data: MoneroPolyseedDecodedData, + coin: MoneroPolyseedCoins = MoneroPolyseedCoins.MONERO) -> Mnemonic: + """ + Encode decoded data to mnemonic phrase. + + Args: + data (MoneroPolyseedDecodedData) : Decoded polyseed data + coin (MoneroPolyseedCoins, optional): Target coin for domain separation (default: MONERO) + + Returns: + Mnemonic: Encoded mnemonic phrase + """ + # Convert to polynomial with existing checksum + coeffs = MoneroPolyseedMnemonicUtils.DataToPoly(data) + coeffs[0] = data.checksum + + # Apply coin domain separation + coeffs[MoneroPolyseedMnemonicConst.POLY_NUM_CHECK_DIGITS] ^= int(coin) + + # Map coefficients to words + words = [self.m_words_list.GetWordAtIdx(c) for c in coeffs] + + return MoneroPolyseedMnemonic.FromList(words) diff --git a/bip_utils/monero/mnemonic_polyseed/monero_polyseed_mnemonic_encrypter.py b/bip_utils/monero/mnemonic_polyseed/monero_polyseed_mnemonic_encrypter.py new file mode 100644 index 00000000..f062ce31 --- /dev/null +++ b/bip_utils/monero/mnemonic_polyseed/monero_polyseed_mnemonic_encrypter.py @@ -0,0 +1,86 @@ +# Copyright (c) 2026 Emanuele Bellocchia +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""Module for Monero Polyseed mnemonic encryption.""" + +# Imports +import unicodedata + +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_mnemonic import MoneroPolyseedMnemonicConst +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_mnemonic_utils import ( + MoneroPolyseedDecodedData, + MoneroPolyseedGf, + MoneroPolyseedMnemonicUtils, +) +from bip_utils.utils.crypto import Pbkdf2HmacSha256 + + +class MoneroPolyseedMnemonicEncrypter: + """ + Monero Polyseed mnemonic encrypter class. + It encrypts or decrypts polyseed data with a password (toggle operation). + Calling Crypt twice with the same password returns the original data. + """ + + @staticmethod + def Crypt(data: MoneroPolyseedDecodedData, + password: str) -> MoneroPolyseedDecodedData: + """ + Toggle encryption/decryption on the seed data. + + Args: + data (MoneroPolyseedDecodedData): Decoded polyseed data + password (str) : Password for encryption/decryption + + Returns: + MoneroPolyseedDecodedData: Encrypted or decrypted polyseed data + """ + # NFKD normalize password + pass_norm = unicodedata.normalize("NFKD", password).encode("utf-8") + + # Derive 32-byte encryption mask + mask = Pbkdf2HmacSha256.DeriveKey( + password=pass_norm, + salt=MoneroPolyseedMnemonicConst.KDF_MASK_SALT, + itr_num=MoneroPolyseedMnemonicConst.KDF_NUM_ITERATIONS, + dklen=MoneroPolyseedMnemonicConst.SECRET_BUFFER_SIZE, + ) + + # XOR secret with mask + new_secret = bytearray(data.secret) + for i in range(MoneroPolyseedMnemonicConst.SECRET_SIZE): + new_secret[i] ^= mask[i] + new_secret[MoneroPolyseedMnemonicConst.SECRET_SIZE - 1] &= MoneroPolyseedMnemonicConst.CLEAR_MASK + + # Toggle encrypted flag + new_features = data.features ^ MoneroPolyseedMnemonicConst.ENCRYPTED_MASK + + # Recalculate checksum + new_data = MoneroPolyseedDecodedData( + secret=bytes(new_secret), + birthday=data.birthday, + features=new_features, + checksum=0, + ) + coeffs = MoneroPolyseedMnemonicUtils.DataToPoly(new_data) + coeffs = MoneroPolyseedGf.PolyEncode(coeffs) + new_data.checksum = coeffs[0] + + return new_data diff --git a/bip_utils/monero/mnemonic_polyseed/monero_polyseed_mnemonic_generator.py b/bip_utils/monero/mnemonic_polyseed/monero_polyseed_mnemonic_generator.py new file mode 100644 index 00000000..a16ecea3 --- /dev/null +++ b/bip_utils/monero/mnemonic_polyseed/monero_polyseed_mnemonic_generator.py @@ -0,0 +1,98 @@ +# Copyright (c) 2026 Emanuele Bellocchia +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""Module for Monero Polyseed mnemonic generation.""" + +# Imports +import time +from typing import Optional + +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_entropy_generator import MoneroPolyseedEntropyGenerator +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_mnemonic import ( + MoneroPolyseedCoins, + MoneroPolyseedLanguages, +) +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_mnemonic_encoder import MoneroPolyseedMnemonicEncoder +from bip_utils.utils.mnemonic import Mnemonic + + +class MoneroPolyseedMnemonicGenerator: + """ + Monero Polyseed mnemonic generator class. + It generates Polyseed mnemonics from entropy or randomly. + """ + + m_mnemonic_encoder: MoneroPolyseedMnemonicEncoder + + def __init__(self, + lang: MoneroPolyseedLanguages = MoneroPolyseedLanguages.ENGLISH) -> None: + """ + Construct class. + + Args: + lang (MoneroPolyseedLanguages, optional): Language (default: English) + + Raises: + TypeError: If the language is not a MoneroPolyseedLanguages enum + ValueError: If loaded words list is not valid + """ + self.m_mnemonic_encoder = MoneroPolyseedMnemonicEncoder(lang) + + def FromEntropy(self, + entropy_bytes: bytes, + birthday: Optional[int] = None, + features: int = 0, + coin: MoneroPolyseedCoins = MoneroPolyseedCoins.MONERO) -> Mnemonic: + """ + Generate mnemonic from provided entropy bytes. + + Args: + entropy_bytes (bytes) : Secret bytes (19 bytes) + birthday (int, optional) : Unix timestamp or raw 10-bit encoded birthday (default: current time) + features (int, optional) : User feature flags (0-7, default: 0) + coin (MoneroPolyseedCoins, optional): Target coin (default: MONERO) + + Returns: + Mnemonic: Generated mnemonic + + Raises: + ValueError: If the entropy length is not valid + """ + if birthday is None: + birthday = int(time.time()) + return self.m_mnemonic_encoder.EncodeWithData(entropy_bytes, birthday, features, coin) + + def FromRandom(self, + birthday: Optional[int] = None, + features: int = 0, + coin: MoneroPolyseedCoins = MoneroPolyseedCoins.MONERO) -> Mnemonic: + """ + Generate mnemonic from random entropy. + + Args: + birthday (int, optional) : Unix timestamp or raw 10-bit encoded birthday (default: current time) + features (int, optional) : User feature flags (0-7, default: 0) + coin (MoneroPolyseedCoins, optional): Target coin (default: MONERO) + + Returns: + Mnemonic: Generated mnemonic + """ + entropy = MoneroPolyseedEntropyGenerator().Generate() + return self.FromEntropy(entropy, birthday, features, coin) diff --git a/bip_utils/monero/mnemonic_polyseed/monero_polyseed_mnemonic_utils.py b/bip_utils/monero/mnemonic_polyseed/monero_polyseed_mnemonic_utils.py new file mode 100644 index 00000000..adf158ae --- /dev/null +++ b/bip_utils/monero/mnemonic_polyseed/monero_polyseed_mnemonic_utils.py @@ -0,0 +1,251 @@ +# Copyright (c) 2026 Emanuele Bellocchia +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""Module for Monero Polyseed mnemonic utility classes.""" + +# Imports +from dataclasses import dataclass +from typing import List + +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_mnemonic import ( + MoneroPolyseedMnemonicConst, +) + + +@dataclass +class MoneroPolyseedDecodedData: + """Dataclass holding decoded Polyseed data.""" + + secret: bytes + birthday: int + features: int + checksum: int + + @property + def birthday_timestamp(self) -> int: + """Decode birthday to Unix timestamp.""" + return MoneroPolyseedMnemonicConst.EPOCH + self.birthday * MoneroPolyseedMnemonicConst.TIME_STEP + + @property + def is_encrypted(self) -> bool: + """Get if the seed is encrypted.""" + return (self.features & MoneroPolyseedMnemonicConst.ENCRYPTED_MASK) != 0 + + @property + def user_features(self) -> int: + """Get user feature flags (3 bits).""" + return self.features & MoneroPolyseedMnemonicConst.USER_FEATURES_MASK + + +class MoneroPolyseedGf: + """GF(2048) arithmetic for Polyseed.""" + + @staticmethod + def ElemMul2(x: int) -> int: + """ + Multiply element by 2 in GF(2048). + + Args: + x (int): GF(2048) element + + Returns: + int: Result of multiplication by 2 + """ + if x < 1024: + return 2 * x + return MoneroPolyseedMnemonicConst.MUL2_TABLE[x % 8] + 16 * ((x - 1024) // 8) + + @staticmethod + def PolyEval(coeffs: List[int]) -> int: + """ + Evaluate polynomial at x=2 using Horner's method in GF(2048). + + Args: + coeffs (list[int]): Polynomial coefficients (16 elements) + + Returns: + int: Evaluation result + """ + result = coeffs[MoneroPolyseedMnemonicConst.NUM_WORDS - 1] + for i in range(MoneroPolyseedMnemonicConst.NUM_WORDS - 2, -1, -1): + result = MoneroPolyseedGf.ElemMul2(result) ^ coeffs[i] + return result + + @staticmethod + def PolyEncode(coeffs: List[int]) -> List[int]: + """ + Calculate checksum and set coeffs[0]. + + Args: + coeffs (list[int]): Polynomial coefficients (16 elements), coeffs[0] should be 0 + + Returns: + list[int]: Coefficients with checksum set at index 0 + """ + coeffs[0] = MoneroPolyseedGf.PolyEval(coeffs) + return coeffs + + @staticmethod + def PolyCheck(coeffs: List[int]) -> bool: + """ + Verify polynomial checksum (evaluation should be 0). + + Args: + coeffs (list[int]): Polynomial coefficients (16 elements) + + Returns: + bool: True if checksum is valid + """ + return MoneroPolyseedGf.PolyEval(coeffs) == 0 + + +class MoneroPolyseedMnemonicUtils: + """Utility functions for Monero Polyseed mnemonic.""" + + @staticmethod + def BirthdayEncode(timestamp: int) -> int: + """ + Encode a Unix timestamp to a 10-bit birthday value. + + Args: + timestamp (int): Unix timestamp + + Returns: + int: Encoded birthday (0-1023) + """ + if timestamp < MoneroPolyseedMnemonicConst.EPOCH: + return 0 + return ((timestamp - MoneroPolyseedMnemonicConst.EPOCH) + // MoneroPolyseedMnemonicConst.TIME_STEP) & MoneroPolyseedMnemonicConst.DATE_MASK + + @staticmethod + def BirthdayDecode(birthday: int) -> int: + """ + Decode a 10-bit birthday value to a Unix timestamp. + + Args: + birthday (int): Encoded birthday (0-1023) + + Returns: + int: Unix timestamp + """ + return MoneroPolyseedMnemonicConst.EPOCH + birthday * MoneroPolyseedMnemonicConst.TIME_STEP + + @staticmethod + def DataToPoly(data: MoneroPolyseedDecodedData) -> List[int]: + """ + Convert decoded data to 16 polynomial coefficients. + Faithful port of polyseed_data_to_poly from gf.c. + + Args: + data (MoneroPolyseedDecodedData): Decoded polyseed data + + Returns: + list[int]: 16 polynomial coefficients + """ + extra_val = (data.features << MoneroPolyseedMnemonicConst.DATE_BITS) | data.birthday + extra_bits = MoneroPolyseedMnemonicConst.FEATURE_BITS + MoneroPolyseedMnemonicConst.DATE_BITS + + word_bits = 0 + word_val = 0 + + secret_idx = 0 + secret_val = data.secret[secret_idx] + secret_bits = 8 + seed_rem_bits = MoneroPolyseedMnemonicConst.SECRET_BITS - 8 + + coeffs = [0] * MoneroPolyseedMnemonicConst.NUM_WORDS + + for i in range(MoneroPolyseedMnemonicConst.DATA_WORDS): + while word_bits < MoneroPolyseedMnemonicConst.SHARE_BITS: + if secret_bits == 0: + secret_idx += 1 + secret_bits = min(seed_rem_bits, 8) + secret_val = data.secret[secret_idx] + seed_rem_bits -= secret_bits + chunk_bits = min(secret_bits, MoneroPolyseedMnemonicConst.SHARE_BITS - word_bits) + secret_bits -= chunk_bits + word_bits += chunk_bits + word_val <<= chunk_bits + word_val |= (secret_val >> secret_bits) & ((1 << chunk_bits) - 1) + + word_val <<= 1 + extra_bits -= 1 + word_val |= (extra_val >> extra_bits) & 1 + coeffs[MoneroPolyseedMnemonicConst.POLY_NUM_CHECK_DIGITS + i] = word_val + word_val = 0 + word_bits = 0 + + return coeffs + + @staticmethod + def PolyToData(coeffs: List[int]) -> MoneroPolyseedDecodedData: + """ + Convert 16 polynomial coefficients to decoded data. + Faithful port of polyseed_poly_to_data from gf.c. + + Args: + coeffs (list[int]): 16 polynomial coefficients + + Returns: + MoneroPolyseedDecodedData: Decoded polyseed data + """ + checksum = coeffs[0] + + extra_val = 0 + extra_bits = 0 + + secret = bytearray(MoneroPolyseedMnemonicConst.SECRET_SIZE) + secret_idx = 0 + secret_bits = 0 + + for i in range(MoneroPolyseedMnemonicConst.POLY_NUM_CHECK_DIGITS, + MoneroPolyseedMnemonicConst.NUM_WORDS): + word_val = coeffs[i] + + extra_val <<= 1 + extra_val |= word_val & 1 + word_val >>= 1 + word_bits = MoneroPolyseedMnemonicConst.GF_BITS - 1 + extra_bits += 1 + + while word_bits > 0: + if secret_bits == 8: + secret_idx += 1 + secret_bits = 0 + chunk_bits = min(word_bits, 8 - secret_bits) + word_bits -= chunk_bits + chunk_mask = (1 << chunk_bits) - 1 + if chunk_bits < 8: + secret[secret_idx] <<= chunk_bits + secret[secret_idx] |= (word_val >> word_bits) & chunk_mask + secret_bits += chunk_bits + + birthday = extra_val & MoneroPolyseedMnemonicConst.DATE_MASK + features = extra_val >> MoneroPolyseedMnemonicConst.DATE_BITS + + return MoneroPolyseedDecodedData( + secret=bytes(secret), + birthday=birthday, + features=features, + checksum=checksum, + ) + + diff --git a/bip_utils/monero/mnemonic_polyseed/monero_polyseed_mnemonic_validator.py b/bip_utils/monero/mnemonic_polyseed/monero_polyseed_mnemonic_validator.py new file mode 100644 index 00000000..e290d92e --- /dev/null +++ b/bip_utils/monero/mnemonic_polyseed/monero_polyseed_mnemonic_validator.py @@ -0,0 +1,54 @@ +# Copyright (c) 2026 Emanuele Bellocchia +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""Module for Monero Polyseed mnemonic validation.""" + +# Imports +from typing import Optional + +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_mnemonic import ( + MoneroPolyseedCoins, + MoneroPolyseedLanguages, +) +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_mnemonic_decoder import MoneroPolyseedMnemonicDecoder +from bip_utils.utils.mnemonic import MnemonicValidator + + +class MoneroPolyseedMnemonicValidator(MnemonicValidator): + """ + Monero Polyseed mnemonic validator class. + It validates a Polyseed mnemonic phrase. + """ + + def __init__(self, + coin: MoneroPolyseedCoins = MoneroPolyseedCoins.MONERO, + lang: Optional[MoneroPolyseedLanguages] = None) -> None: + """ + Construct class. + + Args: + coin (MoneroPolyseedCoins, optional) : Coin for domain separation (default: MONERO) + lang (MoneroPolyseedLanguages, optional): Language, None for automatic detection + + Raises: + TypeError: If the language is not a MoneroPolyseedLanguages enum + ValueError: If loaded words list is not valid + """ + super().__init__(MoneroPolyseedMnemonicDecoder(coin, lang)) diff --git a/bip_utils/monero/mnemonic_polyseed/monero_polyseed_seed_generator.py b/bip_utils/monero/mnemonic_polyseed/monero_polyseed_seed_generator.py new file mode 100644 index 00000000..091a3e61 --- /dev/null +++ b/bip_utils/monero/mnemonic_polyseed/monero_polyseed_seed_generator.py @@ -0,0 +1,97 @@ +# Copyright (c) 2026 Emanuele Bellocchia +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""Module for Monero Polyseed seed generation.""" + +# Imports +import struct +from typing import Optional, Union + +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_mnemonic import ( + MoneroPolyseedCoins, + MoneroPolyseedLanguages, + MoneroPolyseedMnemonicConst, +) +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_mnemonic_decoder import MoneroPolyseedMnemonicDecoder +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_mnemonic_utils import MoneroPolyseedDecodedData +from bip_utils.utils.crypto import Pbkdf2HmacSha256 +from bip_utils.utils.mnemonic import Mnemonic + + +class MoneroPolyseedSeedGenerator: + """ + Monero Polyseed seed generator class. + It generates a 32-byte key from a Polyseed mnemonic via PBKDF2-HMAC-SHA256. + """ + + m_data: MoneroPolyseedDecodedData + m_coin: MoneroPolyseedCoins + + def __init__(self, + mnemonic: Union[str, Mnemonic], + coin: MoneroPolyseedCoins = MoneroPolyseedCoins.MONERO, + lang: Optional[MoneroPolyseedLanguages] = None) -> None: + """ + Construct class. + + Args: + mnemonic (str or Mnemonic object) : Mnemonic + coin (MoneroPolyseedCoins, optional) : Coin for domain separation (default: MONERO) + lang (MoneroPolyseedLanguages, optional): Language, None for automatic detection + + Raises: + MnemonicChecksumError: If checksum is not valid + ValueError: If mnemonic is not valid + """ + self.m_data = MoneroPolyseedMnemonicDecoder(coin, lang).DecodeWithData(mnemonic) + self.m_coin = coin + + def Generate(self) -> bytes: + """ + Generate 32-byte seed key via PBKDF2-HMAC-SHA256. + + The password is the 19-byte secret zero-padded to 32 bytes. + The salt is: "POLYSEED key 0x00 0xff 0xff 0xff" + coin(4 LE) + birthday(4 LE) + features(4 LE) + zeros(4) + + Returns: + bytes: Generated 32-byte seed + """ + # Build password: secret padded to 32 bytes + padding = b"\x00" * (MoneroPolyseedMnemonicConst.SECRET_BUFFER_SIZE - MoneroPolyseedMnemonicConst.SECRET_SIZE) + password = self.m_data.secret + padding + + # Build salt (32 bytes) + salt = bytearray(32) + salt[0:12] = MoneroPolyseedMnemonicConst.KDF_KEY_SALT_PREFIX + salt[12] = 0x00 + salt[13] = 0xFF + salt[14] = 0xFF + salt[15] = 0xFF + struct.pack_into(" bytes: + """ + Derive a key. + + Args: + password (str or bytes): Password + salt (str or bytes) : Salt + itr_num (int) : Iteration number + dklen (int, optional) : Length of the derived key (default: SHA-256 output length) + + Returns: + bytes: Computed result + """ + if HASHLIB_USE_PBKDF2: + return hashlib.pbkdf2_hmac("sha256", AlgoUtils.Encode(password), AlgoUtils.Encode(salt), itr_num, dklen) + # Use Cryptodome if not implemented in hashlib + return PBKDF2(AlgoUtils.Encode(password), # type: ignore [arg-type] + AlgoUtils.Encode(salt), + dklen or SHA256.digest_size, + count=itr_num, + hmac_hash_module=SHA256) diff --git a/examples/monero.py b/examples/monero.py index fedee6f7..ab11ea72 100644 --- a/examples/monero.py +++ b/examples/monero.py @@ -1,13 +1,32 @@ """Example of keys derivation for Monero (same addresses of official wallet).""" -from bip_utils import BytesUtils, Monero, MoneroMnemonicGenerator, MoneroSeedGenerator, MoneroWordsNum +from bip_utils import ( + BytesUtils, + Monero, + MoneroMnemonicGenerator, + MoneroPolyseedMnemonicDecoder, + MoneroPolyseedMnemonicEncoder, + MoneroPolyseedMnemonicEncrypter, + MoneroPolyseedMnemonicGenerator, + MoneroPolyseedSeedGenerator, + MoneroSeedGenerator, + MoneroWordsNum, +) +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_mnemonic import MoneroPolyseedLanguages +# +# Legacy Monero mnemonic (25 words) +# + +print("--- Legacy Monero mnemonic ---") + # Generate random mnemonic mnemonic = MoneroMnemonicGenerator().FromWordsNumber(MoneroWordsNum.WORDS_NUM_25) print(f"Mnemonic string: {mnemonic}") # Generate seed from mnemonic seed_bytes = MoneroSeedGenerator(mnemonic).Generate() +print(f"Seed: {seed_bytes.hex()}") # Construct from seed monero = Monero.FromSeed(seed_bytes) @@ -27,3 +46,49 @@ for acc_idx in range(2): for subaddr_idx in range(5): print(f"Subaddress (account: {acc_idx}, index: {subaddr_idx}): {monero.Subaddress(subaddr_idx, acc_idx)}") + +# +# Polyseed mnemonic (16 words), e.g. Cake Wallet +# + +print("\n--- Polyseed mnemonic ---") + +# Generate random Polyseed mnemonic (birthday defaults to current time) +mnemonic = MoneroPolyseedMnemonicGenerator(MoneroPolyseedLanguages.ENGLISH).FromRandom() +print(f"Mnemonic string: {mnemonic}") + +# Decode to inspect data +data = MoneroPolyseedMnemonicDecoder().DecodeWithData(mnemonic) +print(f"Secret: {data.secret.hex()}") +print(f"Birthday timestamp: {data.birthday_timestamp}") +print(f"Is encrypted: {data.is_encrypted}") +print(f"User features: {data.user_features}") + +# Generate 32-byte seed via PBKDF2-HMAC-SHA256 +seed_bytes = MoneroPolyseedSeedGenerator(mnemonic).Generate() +print(f"Seed: {seed_bytes.hex()}") + +# Construct from seed +monero = Monero.FromSeed(seed_bytes) + +# Print keys +print(f"Monero private spend key: {monero.PrivateSpendKey().Raw().ToHex()}") +print(f"Monero private view key: {monero.PrivateViewKey().Raw().ToHex()}") +print(f"Monero public spend key: {monero.PublicSpendKey().RawCompressed().ToHex()}") +print(f"Monero public view key: {monero.PublicViewKey().RawCompressed().ToHex()}") + +# Print the first 5 subaddresses for account 0 and 1 +for acc_idx in range(1): + for subaddr_idx in range(5): + print(f"Subaddress (account: {acc_idx}, index: {subaddr_idx}): {monero.Subaddress(subaddr_idx, acc_idx)}") + +# Encrypt the mnemonic with a password +encrypted_data = MoneroPolyseedMnemonicEncrypter.Crypt(data, "my_password") +encrypted_mnemonic = MoneroPolyseedMnemonicEncoder().EncodeData(encrypted_data) +print(f"Encrypted mnemonic: {encrypted_mnemonic}") + +# Decrypt: decode the encrypted mnemonic, then decrypt +enc_data = MoneroPolyseedMnemonicDecoder().DecodeWithData(encrypted_mnemonic) +decrypted_data = MoneroPolyseedMnemonicEncrypter.Crypt(enc_data, "my_password") +decrypted_mnemonic = MoneroPolyseedMnemonicEncoder().EncodeData(decrypted_data) +print(f"Decrypted mnemonic: {decrypted_mnemonic}") diff --git a/examples/ton.py b/examples/ton.py index f85fff41..dfd108bc 100644 --- a/examples/ton.py +++ b/examples/ton.py @@ -8,6 +8,7 @@ Bip44, Bip44Coins, Bip44ConfGetter, + Ton, TonAddrEncoder, TonAddrVersions, TonMnemonicGenerator, @@ -15,13 +16,15 @@ TonSeedGenerator, TonSeedTypes, TonWordsNum, - Ton, ) + # # Generation like ton-crypto # +print("\n--- Ton-Crypto ---") + # Generate random mnemonic mnemonic = TonMnemonicGenerator().FromWordsNumber(TonWordsNum.WORDS_NUM_24) print(f"Mnemonic: {mnemonic}") @@ -40,15 +43,17 @@ # Generate keypair and address ton = Ton.FromSeed(seed_bytes) -print(f"Ton public key (ton-crypto): {ton.PublicKey().RawCompressed().ToHex()}") -print(f"Ton private key (ton-crypto): {ton.PrivateKey().Raw().ToHex()}") -print(f"Ton address (ton-crypto, V5R1): {ton.GetAddress(version=TonAddrVersions.V5R1)}") +print(f"Ton public key: {ton.PublicKey().RawCompressed().ToHex()}") +print(f"Ton private key: {ton.PrivateKey().Raw().ToHex()}") +print(f"Ton address: {ton.GetAddress(version=TonAddrVersions.V5R1)}") # # Generation like Tonkeeper, Tonwallet, Trustwallet # +print("\n--- Tonkeeper, Tonwallet, Trustwallet ---") + # Generate random mnemonic mnemonic = Bip39MnemonicGenerator().FromWordsNumber(Bip39WordsNum.WORDS_NUM_24) print(f"Mnemonic string: {mnemonic}") @@ -74,6 +79,8 @@ # Generation like Ledger # +print("\n--- Ledger ---") + # Get coin index from configuration coin_idx = Bip44ConfGetter.GetConfig(Bip44Coins.TON).CoinIndex() # Derive @@ -84,4 +91,4 @@ priv_key_bytes = bip32_ctx.PrivateKey().Raw().ToBytes() bip44_ctx = Bip44.FromPrivateKey(priv_key_bytes, Bip44Coins.TON) # Print address -print(f"Ton address (Ledger, V4): {bip44_ctx.PublicKey().ToAddress()}") +print(f"Ton address (V4): {bip44_ctx.PublicKey().ToAddress()}") diff --git a/pyproject.toml b/pyproject.toml index 97024308..01ae4241 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -168,7 +168,7 @@ packages = {find = {exclude = ["benchmark*", "build*", "dist*", "docs*", "examp bip_utils = [ "bip/bip39/wordlist/*.txt", "electrum/mnemonic_v1/wordlist/*.txt", - "monero/mnemonic/wordlist/*.txt", + "monero/mnemonic_legacy/wordlist/*.txt", ] [tool.setuptools.dynamic] diff --git a/pyproject_legacy.toml b/pyproject_legacy.toml index 982b0b36..91a31c88 100644 --- a/pyproject_legacy.toml +++ b/pyproject_legacy.toml @@ -176,7 +176,7 @@ packages = {find = {exclude = ["benchmark*", "build*", "dist*", "docs*", "examp bip_utils = [ "bip/bip39/wordlist/*.txt", "electrum/mnemonic_v1/wordlist/*.txt", - "monero/mnemonic/wordlist/*.txt", + "monero/mnemonic_legacy/wordlist/*.txt", ] [tool.setuptools.dynamic] diff --git a/readme/monero_mnemonic.md b/readme/monero_mnemonic.md index d29fdf30..5eb80063 100644 --- a/readme/monero_mnemonic.md +++ b/readme/monero_mnemonic.md @@ -3,7 +3,7 @@ If you use the official Monero wallet, you'll probably notice that Monero generates mnemonic in its own way, which is different from BIP-0039.\ In fact, it uses different words lists (with 1626 words instead of 2048) and a different algorithm for encoding/decoding the mnemonic string. -The functionalities of this library are the same of the [BIP-0039](https://github.com/ebellocchia/bip_utils/tree/master/readme/bip39.md) one but with Monero-style mnemonics: +The functionalities of this library are the same as the [BIP-0039](https://github.com/ebellocchia/bip_utils/tree/master/readme/bip39.md) one but with Monero-style mnemonics: - Generate mnemonics from words number or entropy bytes - Validate a mnemonic - Get back the entropy bytes from a mnemonic @@ -60,11 +60,11 @@ Supported languages: MoneroEntropyBitLen, MoneroEntropyGenerator, MoneroLanguages, MoneroWordsNum, MoneroMnemonicEncoder, MoneroMnemonicGenerator ) - + # Generate a random mnemonic string of 25 words with default language (English) # A Mnemonic object will be returned mnemonic = MoneroMnemonicGenerator().FromWordsNumber(MoneroWordsNum.WORDS_NUM_25) - + # Get words count print(mnemonic.WordsCount()) # Get as string @@ -72,20 +72,20 @@ Supported languages: print(str(mnemonic)) # Get as list of strings print(mnemonic.ToList()) - + # Generate a random mnemonic string of 13 words by specifying the language mnemonic = MoneroMnemonicGenerator(MoneroLanguages.ITALIAN).FromWordsNumber(MoneroWordsNum.WORDS_NUM_13) - + # Generate the mnemonic string from entropy bytes entropy_bytes = binascii.unhexlify(b"00000000000000000000000000000000") mnemonic = MoneroMnemonicGenerator().FromEntropyNoChecksum(entropy_bytes) mnemonic = MoneroMnemonicGenerator(MoneroLanguages.FRENCH).FromEntropyWithChecksum(entropy_bytes) - + # Generate mnemonic from random 256-bit entropy (with and without checksum) entropy_bytes = MoneroEntropyGenerator(MoneroEntropyBitLen.BIT_LEN_256).Generate() mnemonic = MoneroMnemonicGenerator().FromEntropyNoChecksum(entropy_bytes) mnemonic = MoneroMnemonicGenerator().FromEntropyWithChecksum(entropy_bytes) - + # Alternatively, the mnemonic can be generated from entropy using the encoder mnemonic = MoneroMnemonicEncoder(MoneroLanguages.ENGLISH).EncodeNoChecksum(entropy_bytes) mnemonic = MoneroMnemonicEncoder(MoneroLanguages.ENGLISH).EncodeWithChecksum(entropy_bytes) @@ -98,14 +98,14 @@ Supported languages: MnemonicChecksumError, MoneroLanguages, MoneroWordsNum, MoneroMnemonic, MoneroMnemonicGenerator, MoneroMnemonicValidator, MoneroMnemonicDecoder ) - + # Mnemonic can be generated with MoneroMnemonicGenerator mnemonic = MoneroMnemonicGenerator().FromWordsNumber(MoneroWordsNum.WORDS_NUM_25) # Or it can be a string mnemonic = "vials licks gulp people reorder tulips acquire cool lunar upwards recipe against ambush february shelter textbook annoyed veered getting swagger paradise total dawn duets getting" # Or from a list mnemonic = MoneroMnemonic.FromList(mnemonic.split()) - + # Get if a mnemonic is valid with automatic language detection, return bool is_valid = MoneroMnemonicValidator().IsValid(mnemonic) # Same but specifying the language @@ -120,7 +120,7 @@ Supported languages: except ValueError: # Invalid length or language... pass - + # Use MoneroMnemonicDecoder to get back the entropy bytes from a mnemonic, specifying the language entropy_bytes = MoneroMnemonicDecoder(MoneroLanguages.ENGLISH).Decode(mnemonic) # Like before with automatic language detection @@ -129,14 +129,143 @@ Supported languages: **Code example (mnemonic seed generation)** from bip_utils import MoneroLanguages, MoneroWordsNum, MoneroMnemonicGenerator, MoneroSeedGenerator - + # Mnemonic can be generated with MoneroMnemonicGenerator mnemonic = MoneroMnemonicGenerator().FromWordsNumber(MoneroWordsNum.WORDS_NUM_25) # Or it can be a string mnemonic = "ockhuizen essing brevet symboliek kart slordig hoeve olifant rodijk altsax creatie kneedbaar vetstaart exotherm laxeerpil lekdicht luikenaar bemiddeld oudachtig josua elburg kieviet escort dimbaar kieviet" - + # Generate with automatic language detection # Like before, the mnemonic can be a string or a Mnemonic object seed_bytes = MoneroSeedGenerator(mnemonic).Generate() # Generate specifying the language seed_bytes = MoneroSeedGenerator(mnemonic, MoneroLanguages.DUTCH).Generate() + +## Monero Polyseed mnemonic library + +Polyseed is a newer mnemonic format for Monero that encodes a 150-bit secret, a wallet birthday, and feature flags into a 16-word phrase using the BIP-39 English wordlist (2048 words). +It includes a Reed-Solomon checksum over GF(2048) for error detection and supports coin domain separation (Monero, Aeon, Wownero) and optional password-based encryption. + +Key differences from legacy Monero mnemonics: +- 16 words (vs 25) +- Uses BIP-39 English wordlist (vs Monero-specific 1626-word lists) +- Encodes wallet birthday (creation date) for faster wallet sync +- Supports encryption of the mnemonic with a password +- Seed derivation uses PBKDF2-HMAC-SHA256 + +Supported words number: + +|Words number|Enum| +|---|---| +|16|`MoneroPolyseedWordsNum.WORDS_NUM_16`| + +Supported entropy bits: + +|Entropy bits|Enum| +|---|---| +|150|`MoneroPolyseedEntropyBitLen.BIT_LEN_150`| + +Supported languages: + +|Language|Enum| +|---|---| +|Chinese (simplified)|`MoneroPolyseedLanguages.CHINESE_SIMPLIFIED`| +|Chinese (traditional)|`MoneroPolyseedLanguages.CHINESE_TRADITIONAL`| +|English|`MoneroPolyseedLanguages.ENGLISH`| +|French|`MoneroPolyseedLanguages.FRENCH`| +|Italian|`MoneroPolyseedLanguages.ITALIAN`| +|Korean|`MoneroPolyseedLanguages.KOREAN`| +|Portuguese|`MoneroPolyseedLanguages.PORTUGUESE`| + +Supported coins: + +|Coin|Enum| +|---|---| +|Monero|`MoneroPolyseedCoins.MONERO`| +|Aeon|`MoneroPolyseedCoins.AEON`| +|Wownero|`MoneroPolyseedCoins.WOWNERO`| + +**Code example (Polyseed mnemonic generation)** + + import binascii + import time + from bip_utils import ( + MoneroPolyseedLanguages, MoneroPolyseedCoins, + MoneroPolyseedMnemonicGenerator, MoneroPolyseedMnemonicEncoder, + MoneroPolyseedEntropyGenerator + ) + + # Generate a random Polyseed mnemonic with current time as birthday + mnemonic = MoneroPolyseedMnemonicGenerator().FromRandom(int(time.time())) + print(mnemonic.ToStr()) + print(mnemonic.WordsCount()) + + # Generate from specific entropy bytes (19 bytes) + entropy = MoneroPolyseedEntropyGenerator().Generate() + mnemonic = MoneroPolyseedMnemonicGenerator().FromEntropy(entropy, int(time.time())) + + # Use the encoder directly for more control + encoder = MoneroPolyseedMnemonicEncoder(MoneroPolyseedLanguages.ENGLISH) + mnemonic = encoder.EncodeWithData( + entropy, + birthday=int(time.time()), # Unix timestamp + features=0, # User feature flags (0-7) + coin=MoneroPolyseedCoins.MONERO, + ) + +**Code example (Polyseed mnemonic validation and decoding)** + + from bip_utils import ( + MnemonicChecksumError, MoneroPolyseedCoins, + MoneroPolyseedMnemonicValidator, MoneroPolyseedMnemonicDecoder + ) + + mnemonic = "raven tail swear infant grief assist regular lamp duck valid someone little harsh puppy airport language" + + # Validate + is_valid = MoneroPolyseedMnemonicValidator().IsValid(mnemonic) + + # Decode to get just the secret bytes + secret = MoneroPolyseedMnemonicDecoder().Decode(mnemonic) + + # Decode to get full data (secret, birthday, features, checksum) + data = MoneroPolyseedMnemonicDecoder().DecodeWithData(mnemonic) + print(f"Birthday timestamp: {data.birthday_timestamp}") + print(f"Is encrypted: {data.is_encrypted}") + print(f"User features: {data.user_features}") + +**Code example (Polyseed seed generation)** + + from bip_utils import MoneroPolyseedCoins, MoneroPolyseedSeedGenerator + + mnemonic = "raven tail swear infant grief assist regular lamp duck valid someone little harsh puppy airport language" + + # Generate 32-byte seed via PBKDF2-HMAC-SHA256 + seed_bytes = MoneroPolyseedSeedGenerator(mnemonic).Generate() + +**Code example (Polyseed mnemonic encryption)** + + from bip_utils import ( + MoneroPolyseedCoins, MoneroPolyseedMnemonicDecoder, + MoneroPolyseedMnemonicEncoder, MoneroPolyseedMnemonicEncrypter, + MoneroPolyseedLanguages + ) + + mnemonic = "raven tail swear infant grief assist regular lamp duck valid someone little harsh puppy airport language" + + # Decode the mnemonic + decoder = MoneroPolyseedMnemonicDecoder() + data = decoder.DecodeWithData(mnemonic) + + # Encrypt with a password + encrypted_data = MoneroPolyseedMnemonicEncrypter.Crypt(data, "my_password") + print(f"Is encrypted: {encrypted_data.is_encrypted}") # True + + # Encode the encrypted data back to a mnemonic + encoder = MoneroPolyseedMnemonicEncoder(MoneroPolyseedLanguages.ENGLISH) + encrypted_mnemonic = encoder.EncodeData(encrypted_data) + + # Decrypt: decode the encrypted mnemonic, then decrypt + enc_data = decoder.DecodeWithData(encrypted_mnemonic) + decrypted_data = MoneroPolyseedMnemonicEncrypter.Crypt(enc_data, "my_password") + print(f"Is encrypted: {decrypted_data.is_encrypted}") # False diff --git a/tests/monero/mnemonic/__init__.py b/tests/monero/mnemonic_legacy/__init__.py similarity index 100% rename from tests/monero/mnemonic/__init__.py rename to tests/monero/mnemonic_legacy/__init__.py diff --git a/tests/monero/mnemonic/test_monero_mnemonic.py b/tests/monero/mnemonic_legacy/test_monero_mnemonic.py similarity index 100% rename from tests/monero/mnemonic/test_monero_mnemonic.py rename to tests/monero/mnemonic_legacy/test_monero_mnemonic.py diff --git a/tests/monero/mnemonic_polyseed/__init__.py b/tests/monero/mnemonic_polyseed/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/monero/mnemonic_polyseed/test_monero_polyseed_mnemonic.py b/tests/monero/mnemonic_polyseed/test_monero_polyseed_mnemonic.py new file mode 100644 index 00000000..3d3d4c60 --- /dev/null +++ b/tests/monero/mnemonic_polyseed/test_monero_polyseed_mnemonic.py @@ -0,0 +1,527 @@ +# Copyright (c) 2026 Emanuele Bellocchia +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# Imports +import binascii +import unittest + +from bip_utils import ( + MnemonicChecksumError, + Monero, + MoneroPolyseedCoins, + MoneroPolyseedDecodedData, + MoneroPolyseedEntropyGenerator, + MoneroPolyseedLanguages, + MoneroPolyseedMnemonicDecoder, + MoneroPolyseedMnemonicEncoder, + MoneroPolyseedMnemonicEncrypter, + MoneroPolyseedMnemonicGenerator, + MoneroPolyseedMnemonicValidator, + MoneroPolyseedSeedGenerator, +) +from bip_utils.monero.mnemonic_polyseed.monero_polyseed_mnemonic_utils import ( + MoneroPolyseedGf, + MoneroPolyseedMnemonicUtils, +) + + +# Verified with polyseed C reference (tests.c) and Cake Wallet +TEST_VECT = [ + # English + { + "entropy": b"64b61f808a53fd0033c59f7cbefa40040ede30", + "mnemonic": "goddess goose success way card fatigue village adapt vanish palm very use mosquito advice derive umbrella", + "seed": b"b4065b99aab59820d945e9fb373f8348e4b9b9256b469634f7bb8925720785db", + "priv_skey": "ab43dfe053ada9a7f64e56b5e98e3039e3b9b9256b469634f7bb89257207850b", + "birthday": 1769885046, + "features": 0, + "coin": MoneroPolyseedCoins.MONERO, + "lang": MoneroPolyseedLanguages.ENGLISH, + }, + { + "entropy": b"119366f48584c57e1f08e434e0e799a2d3ef0c", + "mnemonic": "pill balance eternal hungry candy luggage beyond tide search tomato squirrel atom toy pepper exile veteran", + "seed": b"c16f2c8a509758d2e15647811deaa887b403a21fd21866e8b631c4891bcd9699", + "priv_skey": "6cfc8745631bb3b958d392c64921d2cbb303a21fd21866e8b631c4891bcd9609", + "birthday": 1759366062, + "features": 0, + "coin": MoneroPolyseedCoins.AEON, + "lang": MoneroPolyseedLanguages.ENGLISH, + }, + { + "entropy": b"45ad3896a46b3f7e882e2432f8bf1b435bdf20", + "mnemonic": "airport easy regular matter poverty help worry trigger argue catch slogan mesh shoulder drop hunt wealth", + "seed": b"c0a931e820ea44817e9659171e752bcb02f5d916beaf24395ef61384237fb2ed", + "priv_skey": "ca12c0d2af7e43b0c502cf2df1caf9a601f5d916beaf24395ef61384237fb20d", + "birthday": 1759366062, + "features": 0, + "coin": MoneroPolyseedCoins.WOWNERO, + "lang": MoneroPolyseedLanguages.ENGLISH, + }, + # French + { + "entropy": b"d9b8bf3539b5e67dcc2db6fa820013d535f513", + "mnemonic": "aménager séjour strict unifier édifier pénétrer mensonge signal ambre période terrible bolide adepte saisir science enlever", + "seed": b"30b6e09804593c2f087f7c6237f8bb1be50be9235121e5d9d1adbeeaed65f7a2", + "priv_skey": "ee6e46f7fc7984bea85ed0048535064be40be9235121e5d9d1adbeeaed65f702", + "birthday": 1769885046, + "features": 0, + "coin": MoneroPolyseedCoins.MONERO, + "lang": MoneroPolyseedLanguages.FRENCH, + }, + # French + { + "entropy": b"d9b8bf3539b5e67dcc2db6fa820013d535f513", + "mnemonic": "aménager séjour strict unifier édifier pénétrer mensonge signal ambre période terrible bolide adepte saisir science enlever", + "seed": b"30b6e09804593c2f087f7c6237f8bb1be50be9235121e5d9d1adbeeaed65f7a2", + "priv_skey": "ee6e46f7fc7984bea85ed0048535064be40be9235121e5d9d1adbeeaed65f702", + "birthday": 1769885046, + "features": 0, + "coin": MoneroPolyseedCoins.MONERO, + "lang": MoneroPolyseedLanguages.FRENCH, + }, + # Italian + { + "entropy": b"24ee6a3994ff98ee5a41b6e9a55aef49ba0136", + "mnemonic": "magico canapa ritardo poderoso illogico zucchero icona tesi ombra rinforzo potassio foresta rollio esercito tombola birra", + "seed": b"951aefdc90c9099f3c6db65f885cc400d3d5bed9599fd7ff894e70b52caddba4", + "priv_skey": "53d3543b89ea512edd4c0a02d6990e30d2d5bed9599fd7ff894e70b52caddb04", + "birthday": 1748847078, + "features": 0, + "coin": MoneroPolyseedCoins.MONERO, + "lang": MoneroPolyseedLanguages.ITALIAN, + }, + # Portuguese + { + "entropy": b"0b8a11623f108d9378b4898cc6b4f206afd302", + "mnemonic": "sonhador alecrim boreal argola lesma anagrama chave chumbo caixote juba caule foguete comando adjunto poluente capricho", + "seed": b"268f74831e2c36ab2ddcd2906501c7b6340d2af41713ea0cd68a287cb9cd10c4", + "priv_skey": "0aa0ee27e286598a218237edf54a53bc330d2af41713ea0cd68a287cb9cd1004", + "birthday": 1748847078, + "features": 0, + "coin": MoneroPolyseedCoins.MONERO, + "lang": MoneroPolyseedLanguages.PORTUGUESE, + }, + # Korean + { + "entropy": b"61b026a390ceffaafafa0d91a0a73decdae00e", + "mnemonic": "한문 삼십 제작 성별 퇴근 차라리 흐름 자동 정성 계층 박수 결정 차선 하숙집 센티미터 갈증", + "seed": b"7f8c6afe5ccb2bdb17921f09d8d374b6061026d08d078bead1257ddc41de803f", + "priv_skey": "b81089e70da2f4d294bb38203ce6d777061026d08d078bead1257ddc41de800f", + "birthday": 1769885046, + "features": 0, + "coin": MoneroPolyseedCoins.MONERO, + "lang": MoneroPolyseedLanguages.KOREAN, + }, + # Chinese (simplified) + { + "entropy": b"5945337c834b7265b9086e82267f35ad711627", + "mnemonic": "录 罗 意 标 色 范 锁 汽 举 倍 部 乱 驱 栏 浅 迫", + "seed": b"2e4ae471382d7bd0f46a34062c0441bdaf035e5994cc6c57c76c6935e0ae2839", + "priv_skey": "67ce025be90344c871944d1d9016a47eaf035e5994cc6c57c76c6935e0ae2809", + "birthday": 1769885046, + "features": 0, + "coin": MoneroPolyseedCoins.MONERO, + "lang": MoneroPolyseedLanguages.CHINESE_SIMPLIFIED, + }, + # Chinese (traditional) + { + "entropy": b"0607a103aa98cfce38c1fa67ce5eae71548320", + "mnemonic": "善 產 放 表 仗 藏 蘇 弓 音 徙 盪 穆 緣 套 站 候", + "seed": b"a81fe6d22722a6bed00d2dda64fe9c09eac37feabeb2845c6590e21ce75f3c92", + "priv_skey": "53ac418e3aa600a6478a781f9135c64de9c37feabeb2845c6590e21ce75f3c02", + "birthday": 1769885046, + "features": 0, + "coin": MoneroPolyseedCoins.MONERO, + "lang": MoneroPolyseedLanguages.CHINESE_TRADITIONAL, + }, +] + +# Test for mnemonic encryption/decryption +TEST_VECT_ENCRYPT_DECRYPT = [ + { + "mnemonic_dec": "goddess goose success way card fatigue village adapt vanish palm very use mosquito advice derive umbrell", + "mnemonic_enc": "science shell void cruel traffic travel ribbon adult speak seat cup view knock oak lens early", + "password": "my_password", + "lang": MoneroPolyseedLanguages.ENGLISH, + }, + { + "mnemonic_dec": "grotte malheur féroce tituber vérin forgeron blague déclarer joindre sanction suggérer chiot cribler miauler étoile numéro", + "mnemonic_enc": "nrouille coiffer domaine bonheur cuisine soleil enfance débrider nourrir jupon brillant cellule rocheux ailier tapis antidote", + "password": "my_password", + "lang": MoneroPolyseedLanguages.FRENCH, + }, + { + "mnemonic_dec": "magico canapa ritardo poderoso illogico zucchero icona tesi ombra rinforzo potassio foresta rollio esercito tombola birra", + "mnemonic_enc": "sociale mulatto oscurare lanterna pimpante etnico colmato terrazzo renna trespolo gregge esigente femmina spronato florido ridicolo", + "password": "my_password", + "lang": MoneroPolyseedLanguages.ITALIAN, + }, + { + "mnemonic_dec": "sonhador alecrim boreal argola lesma anagrama chave chumbo caixote juba caule foguete comando adjunto poluente capricho", + "mnemonic_enc": "cultura nupcial adepto secular desviar muscular hesitar chocalho alicate roseira urso gralha sagrada luva aclamar marmita", + "password": "my_password", + "lang": MoneroPolyseedLanguages.PORTUGUESE, + }, + { + "mnemonic_dec": "한문 삼십 제작 성별 퇴근 차라리 흐름 자동 정성 계층 박수 결정 차선 하숙집 센티미터 갈증", + "mnemonic_enc": "본인 제일 하천 이별 기본 시중 장래 자격 원인 별명 아나운서 교장 낙엽 실수 창가 인체", + "password": "my_password", + "lang": MoneroPolyseedLanguages.KOREAN, + }, + { + "mnemonic_dec": "录 罗 意 标 色 范 锁 汽 举 倍 部 乱 驱 栏 浅 迫", + "mnemonic_enc": "陕 萄 查 莲 叙 卿 刀 终 啊 青 叛 赶 更 官 堂 疗", + "password": "my_password", + "lang": MoneroPolyseedLanguages.CHINESE_SIMPLIFIED, + }, + { + "mnemonic_dec": "善 產 放 表 仗 藏 蘇 弓 音 徙 盪 穆 緣 套 站 候", + "mnemonic_enc": "臂 壯 半 虧 律 況 憲 酷 你 拖 測 檔 談 煮 脆 婦", + "password": "my_password", + "lang": MoneroPolyseedLanguages.CHINESE_TRADITIONAL, + }, +] + +# Tests for invalid mnemonics +TEST_VECT_MNEMONIC_INVALID = [ + # Wrong length + { + "mnemonic": "abandon abandon abandon", + "exception": ValueError, + }, + # Wrong checksum (last word changed) + { + "mnemonic": "raven tail swear infant grief assist regular lamp duck valid someone little harsh puppy airport abandon", + "exception": MnemonicChecksumError, + }, + # Not existent word + { + "mnemonic": "raven tail swear infant grief assist regular lamp duck valid someone little harsh puppy airport notexistent", + "exception": ValueError, + }, + # Wrong coin + { + "mnemonic": "raven tail swear infant grief assist regular lamp duck valid someone little harsh puppy airport language", + "coin": MoneroPolyseedCoins.AEON, + "exception": MnemonicChecksumError, + }, +] + + +# +# Tests +# +class MoneroPolyseedMnemonicTests(unittest.TestCase): + # Run all tests in test vector + def test_vector(self): + for test in TEST_VECT: + coin = test["coin"] + lang = test["lang"] + + # Test mnemonic encoder + encoder = MoneroPolyseedMnemonicEncoder(lang) + mnemonic = encoder.EncodeWithData( + binascii.unhexlify(test["entropy"]), + test["birthday"], + test["features"], + coin, + ) + self.assertEqual(test["mnemonic"], mnemonic.ToStr()) + self.assertEqual(test["mnemonic"], str(mnemonic)) + self.assertEqual(test["mnemonic"].split(" "), mnemonic.ToList()) + self.assertEqual(len(test["mnemonic"].split(" ")), mnemonic.WordsCount()) + + # Test mnemonic validator + mnemonic_validator = MoneroPolyseedMnemonicValidator(coin, lang) + self.assertTrue(mnemonic_validator.IsValid(mnemonic)) + mnemonic_validator.Validate(mnemonic) + + # Test mnemonic decoder + decoder = MoneroPolyseedMnemonicDecoder(coin, lang) + data = decoder.DecodeWithData(mnemonic) + self.assertEqual(test["entropy"], binascii.hexlify(data.secret)) + self.assertEqual(test["birthday"], data.birthday_timestamp) + self.assertEqual(test["features"], data.user_features) + + # Test decoder (returns just secret bytes) + entropy = decoder.Decode(mnemonic) + self.assertEqual(test["entropy"], binascii.hexlify(entropy)) + + # Test seed generator + seed = MoneroPolyseedSeedGenerator(mnemonic, coin, lang).Generate() + self.assertEqual(test["seed"], binascii.hexlify(seed)) + + # Test private key + monero = Monero.FromSeed(seed) + self.assertEqual(test["priv_skey"], monero.PrivateSpendKey().Raw().ToHex()) + + # Test entropy generator + def test_entropy_generator(self): + gen = MoneroPolyseedEntropyGenerator() + entropy = gen.Generate() + self.assertEqual(len(entropy), 19) + # Top 2 bits of last byte should be cleared + self.assertEqual(entropy[-1] & 0xC0, 0) + + # Test entropy generator and construction from valid entropy bit lengths + def test_entropy_valid_bitlen(self): + self.assertTrue(MoneroPolyseedEntropyGenerator.IsValidEntropyBitLen(150)) + self.assertFalse(MoneroPolyseedEntropyGenerator.IsValidEntropyBitLen(128)) + self.assertFalse(MoneroPolyseedEntropyGenerator.IsValidEntropyBitLen(256)) + self.assertTrue(MoneroPolyseedEntropyGenerator.IsValidEntropyByteLen(19)) + self.assertFalse(MoneroPolyseedEntropyGenerator.IsValidEntropyByteLen(18)) + self.assertFalse(MoneroPolyseedEntropyGenerator.IsValidEntropyByteLen(20)) + + # Test entropy generator and construction from invalid entropy bit lengths + def test_entropy_invalid_bitlen(self): + self.assertRaises(ValueError, MoneroPolyseedEntropyGenerator, 128) + + # Test construction from valid words number + def test_from_valid_words_num(self): + gen = MoneroPolyseedMnemonicGenerator(MoneroPolyseedLanguages.ENGLISH) + mnemonic = gen.FromRandom() + self.assertEqual(mnemonic.WordsCount(), 16) + + # Verify it can be decoded + decoder = MoneroPolyseedMnemonicDecoder(MoneroPolyseedCoins.MONERO) + data = decoder.DecodeWithData(mnemonic) + self.assertEqual(data.features, 0) + + # Test Encode (uses current time as birthday) + def test_encode(self): + test = TEST_VECT[0] + encoder = MoneroPolyseedMnemonicEncoder(test["lang"]) + mnemonic = encoder.Encode(binascii.unhexlify(test["entropy"])) + self.assertEqual(mnemonic.WordsCount(), 16) + + # Verify it can be decoded and has the correct secret + decoder = MoneroPolyseedMnemonicDecoder(test["coin"]) + data = decoder.DecodeWithData(mnemonic) + self.assertEqual(data.secret, binascii.unhexlify(test["entropy"])) + self.assertEqual(data.features, 0) + + # Test construction from invalid words number (invalid entropy length) + def test_from_invalid_entropy_len(self): + encoder = MoneroPolyseedMnemonicEncoder() + self.assertRaises(ValueError, encoder.Encode, b"\x00" * 16) + self.assertRaises(ValueError, encoder.Encode, b"\x00" * 20) + self.assertRaises(ValueError, encoder.EncodeWithData, b"\x00" * 16, 0, 0) + self.assertRaises(ValueError, encoder.EncodeWithData, b"\x00" * 20, 0, 0) + + # Tests invalid mnemonic + def test_invalid_mnemonic(self): + for test in TEST_VECT_MNEMONIC_INVALID: + coin = test.get("coin", MoneroPolyseedCoins.MONERO) + + self.assertFalse(MoneroPolyseedMnemonicValidator(coin).IsValid(test["mnemonic"])) + self.assertRaises(test["exception"], MoneroPolyseedMnemonicValidator(coin).Validate, test["mnemonic"]) + self.assertRaises(test["exception"], MoneroPolyseedMnemonicDecoder(coin).Decode, test["mnemonic"]) + + # Tests invalid parameters + def test_invalid_params(self): + self.assertRaises(TypeError, MoneroPolyseedMnemonicEncoder, 0) + self.assertRaises(TypeError, MoneroPolyseedMnemonicDecoder, MoneroPolyseedCoins.MONERO, 0) + + # Test encoding/decoding roundtrip for all supported languages + def test_roundtrip_all_languages(self): + test = TEST_VECT[0] + secret = binascii.unhexlify(test["entropy"]) + coin = test["coin"] + + for lang in MoneroPolyseedLanguages: + encoder = MoneroPolyseedMnemonicEncoder(lang) + mnemonic = encoder.EncodeWithData( + secret, + test["birthday"], + test["features"], + coin, + ) + self.assertEqual(mnemonic.WordsCount(), 16) + + decoder = MoneroPolyseedMnemonicDecoder(coin) + data = decoder.DecodeWithData(mnemonic) + self.assertEqual(data.secret, secret) + self.assertEqual(data.birthday_timestamp, test["birthday"]) + self.assertEqual(data.user_features, test["features"]) + + # Test coin domain separation + def test_coin_domain_separation(self): + test = TEST_VECT[0] + encoder = MoneroPolyseedMnemonicEncoder(MoneroPolyseedLanguages.ENGLISH) + secret = binascii.unhexlify(test["entropy"]) + + mnemonic_monero = encoder.EncodeWithData(secret, test["birthday"], 0, MoneroPolyseedCoins.MONERO) + mnemonic_aeon = encoder.EncodeWithData(secret, test["birthday"], 0, MoneroPolyseedCoins.AEON) + + self.assertNotEqual(mnemonic_monero.ToStr(), mnemonic_aeon.ToStr()) + + # Each should decode correctly with its own coin + self.assertEqual(MoneroPolyseedMnemonicDecoder(MoneroPolyseedCoins.MONERO).Decode(mnemonic_monero), secret) + self.assertEqual(MoneroPolyseedMnemonicDecoder(MoneroPolyseedCoins.AEON).Decode(mnemonic_aeon), secret) + + # Cross-coin should fail + self.assertRaises(MnemonicChecksumError, MoneroPolyseedMnemonicDecoder(MoneroPolyseedCoins.MONERO).Decode, mnemonic_aeon) + + # Test encoding/decoding with feature flags + def test_encode_decode_with_features(self): + test = TEST_VECT[0] + encoder = MoneroPolyseedMnemonicEncoder(test["lang"]) + mnemonic = encoder.EncodeWithData( + binascii.unhexlify(test["entropy"]), + test["birthday"], + 1, + test["coin"], + ) + self.assertEqual(mnemonic.WordsCount(), 16) + + decoder = MoneroPolyseedMnemonicDecoder(test["coin"]) + data = decoder.DecodeWithData(mnemonic) + self.assertEqual(data.secret, binascii.unhexlify(test["entropy"])) + self.assertEqual(data.birthday_timestamp, test["birthday"]) + self.assertEqual(data.features, 1) + self.assertEqual(data.user_features, 1) + + # Test decoded data properties + def test_decoded_data_properties(self): + data = MoneroPolyseedDecodedData( + secret=b"\x00" * 19, + birthday=1, + features=0, + checksum=0, + ) + self.assertFalse(data.is_encrypted) + self.assertEqual(data.user_features, 0) + self.assertEqual(data.birthday_timestamp, 1635768000 + 2629746) + + # With encrypted flag + data_enc = MoneroPolyseedDecodedData( + secret=b"\x00" * 19, + birthday=1, + features=16, # ENCRYPTED_MASK + checksum=0, + ) + self.assertTrue(data_enc.is_encrypted) + + # Test encryption/decryption roundtrip + def test_encryption_roundtrip(self): + test = TEST_VECT[0] + password = TEST_VECT_ENCRYPT_DECRYPT[0]["password"] + + # Decode mnemonic to get original data + decoder = MoneroPolyseedMnemonicDecoder(test["coin"], test["lang"]) + original_data = decoder.DecodeWithData(test["mnemonic"]) + + # Encrypt + encrypted = MoneroPolyseedMnemonicEncrypter.Crypt(original_data, password) + self.assertTrue(encrypted.is_encrypted) + self.assertNotEqual(encrypted.secret, original_data.secret) + + # Decrypt + decrypted = MoneroPolyseedMnemonicEncrypter.Crypt(encrypted, password) + self.assertFalse(decrypted.is_encrypted) + self.assertEqual(decrypted.secret, original_data.secret) + self.assertEqual(decrypted.birthday, original_data.birthday) + self.assertEqual(decrypted.features, original_data.features) + + # Test encrypted encode/decode roundtrip + def test_encrypted_encode_decode_roundtrip(self): + test = TEST_VECT[0] + password = TEST_VECT_ENCRYPT_DECRYPT[0]["password"] + + # Decode mnemonic to get original data + decoder = MoneroPolyseedMnemonicDecoder(test["coin"], test["lang"]) + original_data = decoder.DecodeWithData(test["mnemonic"]) + + # Encrypt + encrypted = MoneroPolyseedMnemonicEncrypter.Crypt(original_data, password) + + # Encode encrypted data + encoder = MoneroPolyseedMnemonicEncoder(test["lang"]) + mnemonic = encoder.EncodeData(encrypted, test["coin"]) + + # Decode + decoded = decoder.DecodeWithData(mnemonic) + self.assertTrue(decoded.is_encrypted) + + # Decrypt + decrypted = MoneroPolyseedMnemonicEncrypter.Crypt(decoded, password) + self.assertFalse(decrypted.is_encrypted) + self.assertEqual(decrypted.secret, original_data.secret) + + # Test GF(2048) arithmetic + def test_gf_arithmetic(self): + # ElemMul2: below 1024 is simple doubling + self.assertEqual(MoneroPolyseedGf.ElemMul2(0), 0) + self.assertEqual(MoneroPolyseedGf.ElemMul2(1), 2) + self.assertEqual(MoneroPolyseedGf.ElemMul2(512), 1024) + # At and above 1024: uses lookup table + self.assertEqual(MoneroPolyseedGf.ElemMul2(1024), 5) + self.assertEqual(MoneroPolyseedGf.ElemMul2(1025), 7) + + # PolyCheck: valid polynomial should pass, corrupted should fail + data = MoneroPolyseedDecodedData( + secret=binascii.unhexlify(TEST_VECT[0]["entropy"]), + birthday=TEST_VECT[0]["birthday"], + features=TEST_VECT[0]["features"], + checksum=0, + ) + coeffs = MoneroPolyseedMnemonicUtils.DataToPoly(data) + MoneroPolyseedGf.PolyEncode(coeffs) + self.assertTrue(MoneroPolyseedGf.PolyCheck(coeffs)) + coeffs[5] ^= 1 + self.assertFalse(MoneroPolyseedGf.PolyCheck(coeffs)) + + # Test DataToPoly -> PolyToData roundtrip + def test_data_to_poly_roundtrip(self): + test = TEST_VECT[0] + birthday = MoneroPolyseedMnemonicUtils.BirthdayEncode(test["birthday"]) + + data = MoneroPolyseedDecodedData( + secret=binascii.unhexlify(test["entropy"]), + birthday=birthday, + features=test["features"], + checksum=0, + ) + coeffs = MoneroPolyseedMnemonicUtils.DataToPoly(data) + MoneroPolyseedGf.PolyEncode(coeffs) + recovered = MoneroPolyseedMnemonicUtils.PolyToData(coeffs) + + self.assertEqual(recovered.secret, data.secret) + self.assertEqual(recovered.birthday, data.birthday) + self.assertEqual(recovered.features, data.features) + + # Test birthday encode/decode + def test_birthday_encode_decode(self): + test_birthday = TEST_VECT[0]["birthday"] + test_birthday_encoded = MoneroPolyseedMnemonicUtils.BirthdayEncode(test_birthday) + + # Known timestamps + self.assertEqual(MoneroPolyseedMnemonicUtils.BirthdayEncode(1638446400), 1) + self.assertGreater(test_birthday_encoded, 0) + # Before epoch + self.assertEqual(MoneroPolyseedMnemonicUtils.BirthdayEncode(0), 0) + self.assertEqual(MoneroPolyseedMnemonicUtils.BirthdayEncode(1000000000), 0) + + # Decode and verify it roundtrips approximately + for birthday, original_time in [(1, 1638446400), (test_birthday_encoded, test_birthday)]: + decoded = MoneroPolyseedMnemonicUtils.BirthdayDecode(birthday) + self.assertLessEqual(decoded, original_time) + self.assertGreater(decoded + 2630000, original_time) diff --git a/tests/ton/mnemonic/test_ton_mnemonic.py b/tests/ton/mnemonic/test_ton_mnemonic.py index 5d4d3a36..cca9345a 100644 --- a/tests/ton/mnemonic/test_ton_mnemonic.py +++ b/tests/ton/mnemonic/test_ton_mnemonic.py @@ -33,6 +33,7 @@ ) +# Verified with ton-crypto wallet: https://github.com/ton-org/ton-crypto TEST_VECT = [ { "mnemonic": "ask fossil tragic dune session prize own bundle element shift pony trouble hamster topple mammal estate strike impulse post moment club hard step lamp",