Coverage for silkaj/auth.py: 49%
77 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-20 12:29 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-20 12:29 +0000
1# Copyright 2016-2025 Maël Azimi <m.a@moul.re>
2#
3# Silkaj is free software: you can redistribute it and/or modify
4# it under the terms of the GNU Affero General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7#
8# Silkaj is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU Affero General Public License for more details.
12#
13# You should have received a copy of the GNU Affero General Public License
14# along with Silkaj. If not, see <https://www.gnu.org/licenses/>.
16import re
17import sys
18from pathlib import Path
20import rich_click as click
21from duniterpy.key.scrypt_params import ScryptParams
22from duniterpy.key.signing_key import SigningKey, SigningKeyException
24from silkaj.constants import FAILURE_EXIT_STATUS, PUBKEY_PATTERN
25from silkaj.public_key import gen_pubkey_checksum
27SEED_HEX_PATTERN = "^[0-9a-fA-F]{64}$"
28PUBSEC_PUBKEY_PATTERN = f"pub: ({PUBKEY_PATTERN})"
29PUBSEC_SIGNKEY_PATTERN = "sec: ([1-9A-HJ-NP-Za-km-z]{87,90})"
32@click.pass_context
33def auth_method(ctx: click.Context) -> SigningKey:
34 if ctx.obj.get("AUTH_SEED"):
35 return auth_by_seed()
36 if ctx.obj.get("AUTH_FILE_PATH"):
37 return auth_by_auth_file()
38 if ctx.obj.get("AUTH_WIF"):
39 return auth_by_wif()
40 return auth_by_scrypt()
43@click.pass_context
44def has_auth_method(ctx: click.Context) -> bool:
45 return (
46 ("AUTH_SCRYPT" in ctx.obj and ctx.obj["AUTH_SCRYPT"])
47 or ("AUTH_FILE_PATH" in ctx.obj and ctx.obj["AUTH_FILE_PATH"])
48 or ("AUTH_SEED" in ctx.obj and ctx.obj["AUTH_SEED"])
49 or ("AUTH_WIF" in ctx.obj and ctx.obj["AUTH_WIF"])
50 )
53@click.command("authentication", help="Generate authentication file")
54@click.argument(
55 "auth_file",
56 type=click.Path(dir_okay=False, writable=True, path_type=Path),
57)
58def generate_auth_file(auth_file: Path) -> None:
59 key = auth_method()
60 pubkey_cksum = gen_pubkey_checksum(key.pubkey)
61 if auth_file.is_file():
62 message = f"Would you like to erase {auth_file} with an authentication file corresponding \n\
63to following pubkey `{pubkey_cksum}`?"
64 click.confirm(message, abort=True)
65 key.save_seedhex_file(auth_file)
66 print(
67 f"Authentication file '{auth_file}' generated and stored for public key: {pubkey_cksum}",
68 )
71@click.pass_context
72def auth_by_auth_file(ctx: click.Context) -> SigningKey:
73 """
74 Uses an authentication file to generate the key
75 Authfile can either be:
76 * A seed in hexadecimal encoding
77 * PubSec format with public and private key in base58 encoding
78 """
79 authfile = ctx.obj["AUTH_FILE_PATH"]
80 filetxt = authfile.read_text(encoding="utf-8")
82 # two regural expressions for the PubSec format
83 regex_pubkey = re.compile(PUBSEC_PUBKEY_PATTERN, re.MULTILINE)
84 regex_signkey = re.compile(PUBSEC_SIGNKEY_PATTERN, re.MULTILINE)
86 # Seed hexadecimal format
87 if re.search(re.compile(SEED_HEX_PATTERN), filetxt):
88 return SigningKey.from_seedhex_file(authfile)
89 # PubSec format
90 if re.search(regex_pubkey, filetxt) and re.search(regex_signkey, filetxt):
91 return SigningKey.from_pubsec_file(authfile)
92 sys.exit("Error: the format of the file is invalid")
95def auth_by_seed() -> SigningKey:
96 seedhex = click.prompt("Please enter your seed on hex format", hide_input=True)
97 try:
98 return SigningKey.from_seedhex(seedhex)
99 # To be fixed upstream in DuniterPy
100 except SigningKeyException as error:
101 print(error)
102 sys.exit(FAILURE_EXIT_STATUS)
105@click.pass_context
106def auth_by_scrypt(ctx: click.Context) -> SigningKey:
107 salt = click.prompt(
108 "Please enter your Scrypt Salt (Secret identifier)",
109 hide_input=True,
110 default="",
111 )
112 password = click.prompt(
113 "Please enter your Scrypt password (masked)",
114 hide_input=True,
115 default="",
116 )
118 if ctx.obj["AUTH_SCRYPT_PARAMS"]:
119 n, r, p = ctx.obj["AUTH_SCRYPT_PARAMS"].split(",")
121 if n.isnumeric() and r.isnumeric() and p.isnumeric():
122 n, r, p = int(n), int(r), int(p)
123 if n <= 0 or n > 65536 or r <= 0 or r > 512 or p <= 0 or p > 32:
124 sys.exit("Error: the values of Scrypt parameters are not good")
125 scrypt_params = ScryptParams(n, r, p)
126 else:
127 sys.exit("one of n, r or p is not a number")
128 else:
129 scrypt_params = None
131 try:
132 return SigningKey.from_credentials(salt, password, scrypt_params)
133 except ValueError as error:
134 print(error)
135 sys.exit(FAILURE_EXIT_STATUS)
138def auth_by_wif() -> SigningKey:
139 wif_hex = click.prompt(
140 "Enter your WIF or Encrypted WIF address (masked)",
141 hide_input=True,
142 )
143 password = click.prompt(
144 "(Leave empty in case WIF format) Enter the Encrypted WIF password (masked)",
145 hide_input=True,
146 )
147 try:
148 return SigningKey.from_wif_or_ewif_hex(wif_hex, password)
149 # To be fixed upstream in DuniterPy
150 except SigningKeyException as error:
151 print(error)
152 sys.exit(FAILURE_EXIT_STATUS)