diff --git a/duniterpy/key/signing_key.py b/duniterpy/key/signing_key.py index 87da804e7090069419ab9cab27b4214b105d7f53..156eb3b8aa9af060d833bf9de262308aaf97280c 100644 --- a/duniterpy/key/signing_key.py +++ b/duniterpy/key/signing_key.py @@ -36,23 +36,12 @@ from ..tools import ( convert_seedhex_to_seed, convert_seed_to_seedhex, dubp_mnemonic_to_seed, + chunkstring, ) -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)) - +DEWIF_CURRENCY_CODE_NONE = "00000000" +DEWIF_CURRENCY_CODE_G1 = "00000001" +DEWIF_CURRENCY_CODE_G1_TEST = "10000001" # required to type hint cls in classmethod SigningKeyType = TypeVar("SigningKeyType", bound="SigningKey") @@ -540,3 +529,77 @@ Data: {data}""".format( :return: """ return cls(dubp_mnemonic_to_seed(mnemonic)) + + @classmethod + def from_dewif_file( + cls, path: Union[str, PathLike], password: str + ) -> SigningKeyType: + """ + Load a DEWIF encrypted file using the password to decrypt + Only version 1 supported + + :param path: Path of the file + :param password: Password to decrypt the file + :return: + """ + with open(path, "r") as file_handler: + base64_content = file_handler.read() + + data = base64.b64decode(base64_content) + version = struct.unpack(">i", data[0:4])[0] + if version != 1: + raise Exception(f"Version {version} not supported") + # skip currency code + encrypted_data = data[8:] + + aes_key = scrypt( + password=password.encode("utf-8"), + salt=sha256(f"dewif{password}".encode("utf-8")).digest(), + n=4096, + r=16, + p=1, + dklen=32, + ) + aes = AESModeOfOperationECB(aes_key) + data = b"".join(map(aes.decrypt, chunkstring(encrypted_data, 16))) + + seed = data[:32] + public_key = data[32:64] + signing_key = cls(seed) + assert signing_key.vk == public_key + + return signing_key + + def save_dewif_v1_file( + self, + path: Union[str, PathLike], + password: str, + currency: str = DEWIF_CURRENCY_CODE_G1, + ) -> None: + """ + Save the instance seed in an encrypted DEWIF V1 file + Use the password to encrypt data + + :param path: Path of the file to save + :param password: Password to encrypt data + :param currency: Currency code (default=DEWIF_CURRENCY_CODE_G1) + :return: + """ + aes_key = scrypt( + password=password.encode("utf-8"), + salt=sha256(f"dewif{password}".encode("utf-8")).digest(), + n=4096, + r=16, + p=1, + dklen=32, + ) + header = struct.pack(">i", 1) + bytes.fromhex(currency) + data = self.seed + self.vk + + aes = AESModeOfOperationECB(aes_key) + encrypted_data = b"".join(map(aes.encrypt, chunkstring(data, 16))) + + base64_data = base64.b64encode(header + encrypted_data) + + with open(path, "w") as file_handler: + file_handler.write(base64_data.decode("utf-8")) diff --git a/duniterpy/tools.py b/duniterpy/tools.py index 3e594d19ff4e1c5cbd7dfc313e817284c3bfd5e8..2e2db3926bd0a1809cf75e2743a9e551c28c4cae 100644 --- a/duniterpy/tools.py +++ b/duniterpy/tools.py @@ -20,8 +20,6 @@ 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: """ @@ -105,3 +103,14 @@ def dubp_mnemonic_to_seed(mnemonic: str) -> bytes: password=password, salt=salt, n=4096, r=16, p=1, dklen=32 ) # type: bytes return seed + + +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)) diff --git a/tests/key/test_signing_key.py b/tests/key/test_signing_key.py index 3ddf8b2fcd8304f3f3e9cf073c978a4bf7ed9fe2..1cf9c2e99da975b8e03cb696c1284c533c8015f3 100644 --- a/tests/key/test_signing_key.py +++ b/tests/key/test_signing_key.py @@ -16,11 +16,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. """ import base64 import os +from hashlib import scrypt +from pathlib import Path from duniterpy.key import VerifyingKey, SigningKey, PublicKey from duniterpy.key.scrypt_params import ScryptParams import unittest +from duniterpy.key.signing_key import DEWIF_CURRENCY_CODE_G1_TEST + TEST_FILE_PATH = "/tmp/test_file.txt" @@ -157,3 +161,32 @@ class TestSigningKey(unittest.TestCase): "qGdvpbP9lJe7ZG4ZUSyu33KFeAEs/KkshAp9gEI4ReY=", ) self.assertEqual(keypair.pubkey, "732SSfuwjB7jkt9th1zerGhphs6nknaCBCTozxUcPWPU") + + def test_dewif_v1_save_and_load(self): + path = "/tmp/test.dewif" + password = "toto titi tata" + scrypt_params = ScryptParams() + seed = scrypt( + password=b"user password", + salt=b"user salt", + n=scrypt_params.N, # 4096 + r=scrypt_params.r, # 16 + p=scrypt_params.p, # 1 + dklen=scrypt_params.seed_length, + ) + signing_key = SigningKey(seed) + signing_key.save_dewif_v1_file(path, password, DEWIF_CURRENCY_CODE_G1_TEST) + + with open(path, "r") as file_handler: + b64_content = file_handler.read() + self.assertEqual( + "AAAAARAAAAGfFDAs+jVZYkfhBlHZZ2fEQIvBqnG16g5+02cY18wSOjW0cUg2JV3SUTJYN2CrbQeRDwGazWnzSFBphchMmiL0", + b64_content, + ) + + signing_key_loaded = SigningKey.from_dewif_file(path, password) + + self.assertEqual(signing_key_loaded.seed, signing_key.seed) + + if Path(path).exists(): + Path(path).unlink()