Skip to content
Snippets Groups Projects
Commit 16602532 authored by Vincent Texier's avatar Vincent Texier
Browse files

[feat] #151 Add DEWIF v1 file format support for wallets

parent 36e1dcae
No related branches found
No related tags found
1 merge request!125Mnemonic dewif
Pipeline #11400 passed
...@@ -36,23 +36,12 @@ from ..tools import ( ...@@ -36,23 +36,12 @@ from ..tools import (
convert_seedhex_to_seed, convert_seedhex_to_seed,
convert_seed_to_seedhex, convert_seed_to_seedhex,
dubp_mnemonic_to_seed, dubp_mnemonic_to_seed,
chunkstring,
) )
DEWIF_CURRENCY_CODE_NONE = 0x00000000 DEWIF_CURRENCY_CODE_NONE = "00000000"
DEWIF_CURRENCY_CODE_G1 = 0x00000001 DEWIF_CURRENCY_CODE_G1 = "00000001"
DEWIF_CURRENCY_CODE_G1_TEST = 0x10000001 DEWIF_CURRENCY_CODE_G1_TEST = "10000001"
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 # required to type hint cls in classmethod
SigningKeyType = TypeVar("SigningKeyType", bound="SigningKey") SigningKeyType = TypeVar("SigningKeyType", bound="SigningKey")
...@@ -540,3 +529,77 @@ Data: {data}""".format( ...@@ -540,3 +529,77 @@ Data: {data}""".format(
:return: :return:
""" """
return cls(dubp_mnemonic_to_seed(mnemonic)) 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"))
...@@ -20,8 +20,6 @@ from hashlib import sha256, scrypt ...@@ -20,8 +20,6 @@ from hashlib import sha256, scrypt
from typing import Union from typing import Union
from libnacl.encode import hex_decode, hex_encode from libnacl.encode import hex_decode, hex_encode
from duniterpy.key.scrypt_params import ScryptParams
def ensure_bytes(data: Union[str, bytes]) -> bytes: def ensure_bytes(data: Union[str, bytes]) -> bytes:
""" """
...@@ -105,3 +103,14 @@ def dubp_mnemonic_to_seed(mnemonic: str) -> 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 password=password, salt=salt, n=4096, r=16, p=1, dklen=32
) # type: bytes ) # type: bytes
return seed 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))
...@@ -16,11 +16,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. ...@@ -16,11 +16,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
""" """
import base64 import base64
import os import os
from hashlib import scrypt
from pathlib import Path
from duniterpy.key import VerifyingKey, SigningKey, PublicKey from duniterpy.key import VerifyingKey, SigningKey, PublicKey
from duniterpy.key.scrypt_params import ScryptParams from duniterpy.key.scrypt_params import ScryptParams
import unittest import unittest
from duniterpy.key.signing_key import DEWIF_CURRENCY_CODE_G1_TEST
TEST_FILE_PATH = "/tmp/test_file.txt" TEST_FILE_PATH = "/tmp/test_file.txt"
...@@ -157,3 +161,32 @@ class TestSigningKey(unittest.TestCase): ...@@ -157,3 +161,32 @@ class TestSigningKey(unittest.TestCase):
"qGdvpbP9lJe7ZG4ZUSyu33KFeAEs/KkshAp9gEI4ReY=", "qGdvpbP9lJe7ZG4ZUSyu33KFeAEs/KkshAp9gEI4ReY=",
) )
self.assertEqual(keypair.pubkey, "732SSfuwjB7jkt9th1zerGhphs6nknaCBCTozxUcPWPU") 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()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment