Mise à jour effectuée, merci de nous signaler tout dysfonctionnement ! | Upgrade done, please let us know about any dysfunction!

signing_key.py 12.2 KB
Newer Older
inso's avatar
inso committed
1
2
3
4
5
"""
duniter public and private keys

@author: inso
"""
6
7
from re import compile, MULTILINE, search
from typing import Optional, Union, TypeVar, Type
inso's avatar
inso committed
8
9

import libnacl.sign
10
import pyaes
11
from libnacl.utils import load_key
inso's avatar
inso committed
12
from pylibscrypt import scrypt
13

inso's avatar
inso committed
14
from .base58 import Base58Encoder
15
from ..helpers import ensure_bytes, xor_bytes
inso's avatar
inso committed
16
17
18
19

SEED_LENGTH = 32  # Length of the key
crypto_sign_BYTES = 64

Vincent Texier's avatar
Vincent Texier committed
20

inso's avatar
inso committed
21
class ScryptParams:
22
    def __init__(self, n: int, r: int, p: int) -> None:
Vincent Texier's avatar
Vincent Texier committed
23
24
25
        """
        Init a ScryptParams instance with crypto parameters

26
27
28
        :param n: scrypt param N
        :param r: scrypt param r
        :param p: scrypt param p
Vincent Texier's avatar
Vincent Texier committed
29
        """
30
        self.N = n
inso's avatar
inso committed
31
32
        self.r = r
        self.p = p
inso's avatar
inso committed
33

Vincent Texier's avatar
Vincent Texier committed
34

35
36
37
38
# required to type hint cls in classmethod
SigningKeyType = TypeVar('SigningKeyType', bound='SigningKey')


inso's avatar
inso committed
39
class SigningKey(libnacl.sign.Signer):
40
41
42
43
44
45

    def __init__(self, seed: bytes) -> None:
        """
        Init pubkey property

        :param str seed: Hexadecimal seed string
Vincent Texier's avatar
Vincent Texier committed
46
        """
47
48
49
50
51
52
53
54
        super().__init__(seed)
        self.pubkey = Base58Encoder.encode(self.vk)

    @classmethod
    def from_credentials(cls: Type[SigningKeyType], salt: Union[str, bytes], password: Union[str, bytes],
                         scrypt_params: Optional[ScryptParams] = None) -> SigningKeyType:
        """
        Create a SigningKey object from credentials
Vincent Texier's avatar
Vincent Texier committed
55

56
57
58
        :param salt: Secret salt passphrase credential
        :param password: Secret password credential
        :param scrypt_params: ScryptParams instance
Vincent Texier's avatar
Vincent Texier committed
59
60
61
62
        """
        if scrypt_params is None:
            scrypt_params = ScryptParams(4096, 16, 1)

inso's avatar
inso committed
63
64
        salt = ensure_bytes(salt)
        password = ensure_bytes(password)
inso's avatar
inso committed
65
        seed = scrypt(password, salt,
inso's avatar
inso committed
66
67
                      scrypt_params.N, scrypt_params.r, scrypt_params.p,
                      SEED_LENGTH)
inso's avatar
inso committed
68

69
        return cls(seed)
70

71
72
73
74
75
76
77
78
    def save_private_key(self, path: str) -> None:
        """
        Save authentication file

        :param path: Authentication file path
        """
        self.save(path)

79
    @staticmethod
80
81
82
83
84
85
86
87
88
89
90
    def from_private_key(path: str) -> SigningKeyType:
        """
        Read authentication file
        Add public key attribute

        :param path: Authentication file path
        """
        key = load_key(path)
        key.pubkey = Base58Encoder.encode(key.vk)
        return key

91
92
93
94
95
96
97
98
99
100
101
    def decrypt_seal(self, message: bytes) -> str:
        """
        Decrypt message with a curve25519 version of the ed25519 key pair

        :param message: Encrypted message

        :return:
        """
        curve25519_public_key = libnacl.crypto_sign_ed25519_pk_to_curve25519(self.vk)
        curve25519_secret_key = libnacl.crypto_sign_ed25519_sk_to_curve25519(self.sk)
        return libnacl.crypto_box_seal_open(message, curve25519_public_key, curve25519_secret_key).decode('utf-8')
102

103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
    @classmethod
    def from_pubsec_file(cls: Type[SigningKeyType], path: str) -> SigningKeyType:
        """
        Return SigningKey instance from Duniter WIF file

        :param path: Path to WIF file
        """
        with open(path, 'r') as fh:
            pubsec_content = fh.read()

        # line patterns
        regex_pubkey = compile("pub: ([1-9A-HJ-NP-Za-km-z]+)", MULTILINE)
        regex_signkey = compile("sec: ([1-9A-HJ-NP-Za-km-z]+)", MULTILINE)

        # check public key field
        match = search(regex_pubkey, pubsec_content)
        if not match:
            raise Exception('Error: Bad format PubSec v1 file, missing public key')

        # check signkey field
        match = search(regex_signkey, pubsec_content)
        if not match:
            raise Exception('Error: Bad format PubSec v1 file, missing sec key')

        # capture signkey
        signkey_hex = match.groups()[0]

        # extract seed from signkey
        seed = bytes(Base58Encoder.decode(signkey_hex)[0:32])

        return cls(seed)

    def save_pubsec_file(self, path: str) -> None:
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
        """
        Save a Duniter PubSec file (PubSec) v1

        :param path: Path to file
        """
        # version
        version = 1

        # base58 encode keys
        base58_signing_key = Base58Encoder.encode(self.sk)
        base58_public_key = self.pubkey

        # save file
        with open(path, 'w') as fh:
            fh.write(
                """Type: PubSec
152
153
154
Version: {version}
pub: {pubkey}
sec: {signkey}""".format(version=version, pubkey=base58_public_key, signkey=base58_signing_key)
155
            )
156

Vincent Texier's avatar
Vincent Texier committed
157
    @staticmethod
Vincent Texier's avatar
Vincent Texier committed
158
    def from_wif_or_ewif_file(path: str, password: Optional[str] = None) -> SigningKeyType:
Moul's avatar
Moul committed
159
160
161
162
        """
        Return SigningKey instance from Duniter WIF or EWIF file

        :param path: Path to WIF of EWIF file
Vincent Texier's avatar
Vincent Texier committed
163
        :param password: Password needed for EWIF file
Moul's avatar
Moul committed
164
165
166
167
168
169
170
171
172
173
174
175
176
177
        """
        with open(path, 'r') as fh:
            wif_content = fh.read()

        # check data field
        regex = compile('Data: ([1-9A-HJ-NP-Za-km-z]+)', MULTILINE)
        match = search(regex, wif_content)
        if not match:
            raise Exception('Error: Bad format WIF or EWIF v1 file')

        # capture hexa wif key
        wif_hex = match.groups()[0]
        return SigningKey.from_wif_or_ewif_hex(wif_hex, password)

Vincent Texier's avatar
Vincent Texier committed
178
    @staticmethod
Vincent Texier's avatar
Vincent Texier committed
179
    def from_wif_or_ewif_hex(wif_hex: str, password: Optional[str] = None) -> SigningKeyType:
Moul's avatar
Moul committed
180
181
182
183
184
185
186
187
188
189
190
191
        """
        Return SigningKey instance from Duniter WIF or EWIF in hexadecimal format

        :param wif_hex: WIF or EWIF string in hexadecimal format
        :param password: Password of EWIF encrypted seed
        """
        wif_bytes = Base58Encoder.decode(wif_hex)

        fi = wif_bytes[0:1]

        if fi == b"\x01":
            return SigningKey.from_wif_hex(wif_hex)
Vincent Texier's avatar
Vincent Texier committed
192
        elif fi == b"\x02" and password is not None:
Moul's avatar
Moul committed
193
            return SigningKey.from_ewif_hex(wif_hex, password)
Moul's avatar
Moul committed
194
195
196
        else:
            raise Exception("Error: Bad format: not WIF nor EWIF")

Vincent Texier's avatar
Vincent Texier committed
197
    @staticmethod
Moul's avatar
Moul committed
198
    def from_wif_file(path: str) -> SigningKeyType:
199
        """
200
        Return SigningKey instance from Duniter WIF file
201
202
203
204
205
206

        :param path: Path to WIF file
        """
        with open(path, 'r') as fh:
            wif_content = fh.read()

207
        # check data field
208
209
210
211
212
        regex = compile('Data: ([1-9A-HJ-NP-Za-km-z]+)', MULTILINE)
        match = search(regex, wif_content)
        if not match:
            raise Exception('Error: Bad format WIF v1 file')

213
        # capture hexa wif key
214
        wif_hex = match.groups()[0]
Moul's avatar
Moul committed
215
216
217
218
219
220
221
222
223
        return SigningKey.from_wif_hex(wif_hex)

    @classmethod
    def from_wif_hex(cls: Type[SigningKeyType], wif_hex: str) -> SigningKeyType:
        """
        Return SigningKey instance from Duniter WIF in hexadecimal format

        :param wif_hex: WIF string in hexadecimal format
        """
224
225
226
227
        wif_bytes = Base58Encoder.decode(wif_hex)
        if len(wif_bytes) != 35:
            raise Exception("Error: the size of WIF is invalid")

228
        # extract data
229
230
231
232
233
        checksum_from_wif = wif_bytes[-2:]
        fi = wif_bytes[0:1]
        seed = wif_bytes[1:-2]
        seed_fi = wif_bytes[0:-2]

234
        # check WIF format flag
235
        if fi != b"\x01":
236
            raise Exception("Error: bad format version, not WIF")
237
238
239
240
241
242
243
244

        # checksum control
        checksum = libnacl.crypto_hash_sha256(libnacl.crypto_hash_sha256(seed_fi))[0:2]
        if checksum_from_wif != checksum:
            raise Exception("Error: bad checksum of the WIF")

        return cls(seed)

245
    def save_wif_file(self, path: str) -> None:
246
        """
247
        Save a Wallet Import Format file (WIF) v1
248
249
250

        :param path: Path to file
        """
251
        # version
252
253
        version = 1

254
255
        # add format to seed (1=WIF,2=EWIF)
        seed_fi = b"\x01" + self.seed
256
257
258
259
260
261

        # calculate checksum
        sha256_v1 = libnacl.crypto_hash_sha256(seed_fi)
        sha256_v2 = libnacl.crypto_hash_sha256(sha256_v1)
        checksum = sha256_v2[0:2]

262
        # base58 encode key and checksum
263
264
265
266
267
268
        wif_key = Base58Encoder.encode(seed_fi + checksum)

        with open(path, 'w') as fh:
            fh.write(
                """Type: WIF
Version: {version}
269
270
271
Data: {data}""".format(version=version, data=wif_key)
            )

Vincent Texier's avatar
Vincent Texier committed
272
    @staticmethod
Moul's avatar
Moul committed
273
    def from_ewif_file(path: str, password: str) -> SigningKeyType:
274
275
276
        """
        Return SigningKey instance from Duniter EWIF file

Moul's avatar
Moul committed
277
        :param path: Path to EWIF file
278
279
280
281
282
        :param password: Password of the encrypted seed
        """
        with open(path, 'r') as fh:
            wif_content = fh.read()

283
        # check data field
284
285
286
287
288
        regex = compile('Data: ([1-9A-HJ-NP-Za-km-z]+)', MULTILINE)
        match = search(regex, wif_content)
        if not match:
            raise Exception('Error: Bad format EWIF v1 file')

289
        # capture ewif key
290
        ewif_hex = match.groups()[0]
Moul's avatar
Moul committed
291
292
293
294
295
296
297
298
299
300
        return SigningKey.from_ewif_hex(ewif_hex, password)

    @classmethod
    def from_ewif_hex(cls: Type[SigningKeyType], ewif_hex: str, password: str) -> SigningKeyType:
        """
        Return SigningKey instance from Duniter EWIF in hexadecimal format

        :param ewif_hex: EWIF string in hexadecimal format
        :param password: Password of the encrypted seed
        """
301
302
303
304
        ewif_bytes = Base58Encoder.decode(ewif_hex)
        if len(ewif_bytes) != 39:
            raise Exception("Error: the size of EWIF is invalid")

305
        # extract data
306
307
308
309
310
311
312
        fi = ewif_bytes[0:1]
        checksum_from_ewif = ewif_bytes[-2:]
        ewif_no_checksum = ewif_bytes[0:-2]
        salt = ewif_bytes[1:5]
        encryptedhalf1 = ewif_bytes[5:21]
        encryptedhalf2 = ewif_bytes[21:37]

313
        # check format flag
314
        if fi != b"\x02":
315
            raise Exception("Error: bad format version, not EWIF")
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354

        # checksum control
        checksum = libnacl.crypto_hash_sha256(libnacl.crypto_hash_sha256(ewif_no_checksum))[0:2]
        if checksum_from_ewif != checksum:
            raise Exception("Error: bad checksum of the EWIF")

        # SCRYPT
        password_bytes = password.encode("utf-8")
        scrypt_seed = scrypt(password_bytes, salt, 16384, 8, 8, 64)
        derivedhalf1 = scrypt_seed[0:32]
        derivedhalf2 = scrypt_seed[32:64]

        # AES
        aes = pyaes.AESModeOfOperationECB(derivedhalf2)
        decryptedhalf1 = aes.decrypt(encryptedhalf1)
        decryptedhalf2 = aes.decrypt(encryptedhalf2)

        # XOR
        seed1 = xor_bytes(decryptedhalf1, derivedhalf1[0:16])
        seed2 = xor_bytes(decryptedhalf2, derivedhalf1[16:32])
        seed = bytes(seed1 + seed2)

        # Password Control
        signer = SigningKey(seed)
        salt_from_seed = libnacl.crypto_hash_sha256(
            libnacl.crypto_hash_sha256(
                Base58Encoder.decode(signer.pubkey)))[0:4]
        if salt_from_seed != salt:
            raise Exception("Error: bad Password of EWIF address")

        return cls(seed)

    def save_ewif_file(self, path: str, password: str) -> None:
        """
        Save an Encrypted Wallet Import Format file (WIF v2)

        :param path: Path to file
        :param password:
        """
355
        # version
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
        version = 1

        # add version to seed
        salt = libnacl.crypto_hash_sha256(
            libnacl.crypto_hash_sha256(
                Base58Encoder.decode(self.pubkey)))[0:4]

        # SCRYPT
        password_bytes = password.encode("utf-8")
        scrypt_seed = scrypt(password_bytes, salt, 16384, 8, 8, 64)
        derivedhalf1 = scrypt_seed[0:32]
        derivedhalf2 = scrypt_seed[32:64]

        # XOR
        seed1_xor_derivedhalf1_1 = bytes(xor_bytes(self.seed[0:16], derivedhalf1[0:16]))
        seed2_xor_derivedhalf1_2 = bytes(xor_bytes(self.seed[16:32], derivedhalf1[16:32]))

        # AES
        aes = pyaes.AESModeOfOperationECB(derivedhalf2)
        encryptedhalf1 = aes.encrypt(seed1_xor_derivedhalf1_1)
        encryptedhalf2 = aes.encrypt(seed2_xor_derivedhalf1_2)

        # add format to final seed (1=WIF,2=EWIF)
        seed_bytes = b'\x02' + salt + encryptedhalf1 + encryptedhalf2

        # calculate checksum
        sha256_v1 = libnacl.crypto_hash_sha256(seed_bytes)
        sha256_v2 = libnacl.crypto_hash_sha256(sha256_v1)
        checksum = sha256_v2[0:2]

        # B58 encode final key string
        ewif_key = Base58Encoder.encode(seed_bytes + checksum)

        # save file
        with open(path, 'w') as fh:
            fh.write(
                """Type: EWIF
Version: {version}
Data: {data}""".format(version=version, data=ewif_key)
395
            )