Coverage for silkaj/auth.py: 49%

77 statements  

« prev     ^ index     » next       coverage.py v7.6.3, created at 2024-10-18 04:28 +0000

1# Copyright 2016-2024 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/>. 

15 

16import re 

17import sys 

18from pathlib import Path 

19 

20import rich_click as click 

21from duniterpy.key.scrypt_params import ScryptParams 

22from duniterpy.key.signing_key import SigningKey, SigningKeyException 

23 

24from silkaj.constants import FAILURE_EXIT_STATUS, PUBKEY_PATTERN 

25from silkaj.public_key import gen_pubkey_checksum 

26 

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})" 

30 

31 

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() 

41 

42 

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 ) 

51 

52 

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 ) 

69 

70 

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") 

81 

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) 

85 

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") 

93 

94 

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) 

103 

104 

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 ) 

117 

118 if ctx.obj["AUTH_SCRYPT_PARAMS"]: 

119 n, r, p = ctx.obj["AUTH_SCRYPT_PARAMS"].split(",") 

120 

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 

130 

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) 

136 

137 

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)