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