From dd5d06343a35d6901b69a5429f492ff2e9ea1c17 Mon Sep 17 00:00:00 2001 From: Vincent Texier <vit@free.fr> Date: Fri, 19 Mar 2021 19:48:12 +0100 Subject: [PATCH] [feat] #150 Add DUBP Mnemonic SigningKey feature See the RFC0014 of the Duniter Project https://git.duniter.org/documents/rfcs/blob/dubp-mnemonic/rfc/0014_Dubp_Mnemonic.md --- duniterpy/key/signing_key.py | 33 ++++++++++++++++++++++++++++++++- duniterpy/tools.py | 18 ++++++++++++++++++ tests/key/test_signing_key.py | 15 ++++++++++++++- tests/test_tools.py | 21 +++++++++++++++++++++ 4 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 tests/test_tools.py diff --git a/duniterpy/key/signing_key.py b/duniterpy/key/signing_key.py index 45696723..87da804e 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 a613e642..3e594d19 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 1bd9a4ae..3ddf8b2f 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 00000000..0ce1c14a --- /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"), + ) -- GitLab