From e92a661db75cbcd95a74b02e3b363ed6d9e33524 Mon Sep 17 00:00:00 2001 From: Vincent Texier <vit@free.fr> Date: Fri, 19 Mar 2021 19:52:39 +0100 Subject: [PATCH] [feat] #151 Add DEWIF file format support for wallets See RFC0013 of the Duniter Project --- duniterpy/key/signing_key.py | 72 +++++++++++++++++++++++++++++++++++ tests/key/test_signing_key.py | 33 ++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/duniterpy/key/signing_key.py b/duniterpy/key/signing_key.py index 2b66d618..6853b5a8 100644 --- a/duniterpy/key/signing_key.py +++ b/duniterpy/key/signing_key.py @@ -553,3 +553,75 @@ Data: {data}""".format( dklen=scrypt_params.seed_length, # 32 ) # type: bytes return cls(_seed) + + @classmethod + def from_dewif_file( + cls, path: Union[str, PathLike], password: str + ) -> SigningKeyType: + """ + Load a DEWIF encrypted file using the password to decrypt + + Add dewif_version and dewif_currency properties to the instance + + :param path: Path of the file + :param password: Password to decrypt the file + :return: + """ + scrypt_params = ScryptParams() + aes_key = scrypt( + password=password.encode("utf-8"), + salt=sha256(f"dewif{password}".encode("utf-8")).digest(), + n=scrypt_params.N, # 4096 + r=scrypt_params.r, # 16 + p=scrypt_params.p, # 1 + dklen=scrypt_params.seed_length, # 32 + ) + aes = AESModeOfOperationECB(aes_key) + + with open(path, "rb") as file_handler: + file_handler.seek(8) + # header = file_handler.read(8) + # version, currency = struct.unpack("ii", header) + encrypted_data = file_handler.read() + + data = b"".join(map(aes.decrypt, chunkstring(encrypted_data, 16))) + + seed = data[:32] + public_key = data[32:] + 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: int = 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=tikka.domain.dewif.DEWIF_CURRENCY_CODE_G1) + :return: + """ + scrypt_params = ScryptParams() + aes_key = scrypt( + password=password.encode("utf-8"), + salt=sha256(f"dewif{password}".encode("utf-8")).digest(), + n=scrypt_params.N, # 4096 + r=scrypt_params.r, # 16 + p=scrypt_params.p, # 1 + dklen=scrypt_params.seed_length, # 32 + ) + header = struct.pack(">ii", 1, currency) + data = self.seed + self.vk + + aes = AESModeOfOperationECB(aes_key) + encrypted_data = b"".join(map(aes.encrypt, chunkstring(data, 16))) + + with open(path, "wb") as file_handler: + file_handler.write(header + encrypted_data) diff --git a/tests/key/test_signing_key.py b/tests/key/test_signing_key.py index 3ddf8b2f..25d1d505 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, "rb") as file_handler: + b64_content = base64.b64encode(file_handler.read()).decode("utf-8") + self.assertEqual( + b64_content, + "AAAAARAAAAGfFDAs+jVZYkfhBlHZZ2fEQIvBqnG16g5+02cY18wSOjW0cUg2JV3SUTJYN2CrbQeRDwGazWnzSFBphchMmiL0", + ) + + 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() -- GitLab