diff --git a/duniterpy/key/signing_key.py b/duniterpy/key/signing_key.py index 45696723d0eb68cc88f65e7f97f737abee8e9b83..87da804e7090069419ab9cab27b4214b105d7f53 100644 --- a/duniterpy/key/signing_key.py +++ b/duniterpy/key/signing_key.py @@ -17,12 +17,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. import base64 import re +import struct +from os import PathLike from typing import Optional, Union, TypeVar, Type import libnacl.sign import pyaes from libnacl.utils import load_key -from hashlib import scrypt +from hashlib import scrypt, sha256 + +from pyaes import AESModeOfOperationECB from .scrypt_params import ScryptParams from .base58 import Base58Encoder @@ -31,8 +35,25 @@ from ..tools import ( xor_bytes, convert_seedhex_to_seed, convert_seed_to_seedhex, + dubp_mnemonic_to_seed, ) +DEWIF_CURRENCY_CODE_NONE = 0x00000000 +DEWIF_CURRENCY_CODE_G1 = 0x00000001 +DEWIF_CURRENCY_CODE_G1_TEST = 0x10000001 + + +def chunkstring(data: bytes, length: int): + """ + Return a tuple of chunks sized at length from the data bytes + + :param data: Data to split + :param length: Size of chunks + :return: + """ + return (data[0 + i : length + i] for i in range(0, len(data), length)) + + # required to type hint cls in classmethod SigningKeyType = TypeVar("SigningKeyType", bound="SigningKey") @@ -509,3 +530,13 @@ Data: {data}""".format( seed = bytes(base64.b64decode(secret)[0:32]) return cls(seed) + + @classmethod + def from_dubp_mnemonic(cls, mnemonic: str): + """ + Generate key pair instance from a DUBP mnemonic passphrase (128 bits, twelve words) + + :param mnemonic: DUBP mnemonic passphrase + :return: + """ + return cls(dubp_mnemonic_to_seed(mnemonic)) diff --git a/duniterpy/tools.py b/duniterpy/tools.py index a613e642defd524876c65534bfd12529b44e5504..3e594d19ff4e1c5cbd7dfc313e817284c3bfd5e8 100644 --- a/duniterpy/tools.py +++ b/duniterpy/tools.py @@ -16,9 +16,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. """ import uuid +from hashlib import sha256, scrypt from typing import Union from libnacl.encode import hex_decode, hex_encode +from duniterpy.key.scrypt_params import ScryptParams + def ensure_bytes(data: Union[str, bytes]) -> bytes: """ @@ -87,3 +90,18 @@ def get_ws2p_challenge() -> str: :rtype str: """ return str(uuid.uuid4()) + str(uuid.uuid4()) + + +def dubp_mnemonic_to_seed(mnemonic: str) -> bytes: + """ + Return a seed from a 128 bits mnemonic (twelve words) + + :param mnemonic: 128 bits mnemonic + :return: + """ + password = mnemonic.encode("utf-8") # type: bytes + salt = sha256(b"dubp" + password).digest() # type: bytes + seed = scrypt( + password=password, salt=salt, n=4096, r=16, p=1, dklen=32 + ) # type: bytes + return seed diff --git a/tests/key/test_signing_key.py b/tests/key/test_signing_key.py index 1bd9a4ae02935d36c14efd464a512cd49c62cee7..3ddf8b2fcd8304f3f3e9cf073c978a4bf7ed9fe2 100644 --- a/tests/key/test_signing_key.py +++ b/tests/key/test_signing_key.py @@ -14,7 +14,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. """ - +import base64 import os from duniterpy.key import VerifyingKey, SigningKey, PublicKey @@ -144,3 +144,16 @@ class TestSigningKey(unittest.TestCase): sign_key_load.vk.hex(), "d27f4cb2bfadbaf45b61714b896d4639ab90db035aee746611cdd342bdaa8996", ) + + def test_dubp_mnemonic(self): + mnemonic = ( + "tongue cute mail fossil great frozen same social weasel impact brush kind" + ) + + keypair = SigningKey.from_dubp_mnemonic(mnemonic) + + self.assertEqual( + base64.b64encode(keypair.seed).decode("utf-8"), + "qGdvpbP9lJe7ZG4ZUSyu33KFeAEs/KkshAp9gEI4ReY=", + ) + self.assertEqual(keypair.pubkey, "732SSfuwjB7jkt9th1zerGhphs6nknaCBCTozxUcPWPU") diff --git a/tests/test_tools.py b/tests/test_tools.py new file mode 100644 index 0000000000000000000000000000000000000000..0ce1c14a3e2cf1ff2024ff1b697a4b3a353fcbee --- /dev/null +++ b/tests/test_tools.py @@ -0,0 +1,21 @@ +import base64 +import unittest + +from duniterpy import tools + + +class TestTools(unittest.TestCase): + def test_dubp_mnemonic_to_seed(self): + """ + https://git.duniter.org/documents/rfcs/blob/dubp-mnemonic/rfc/0014_Dubp_Mnemonic.md + + :return: + """ + mnemonic = ( + "tongue cute mail fossil great frozen same social weasel impact brush kind" + ) + seed = tools.dubp_mnemonic_to_seed(mnemonic) + self.assertEqual( + "qGdvpbP9lJe7ZG4ZUSyu33KFeAEs/KkshAp9gEI4ReY=", + base64.b64encode(seed).decode("utf-8"), + )