Coverage for silkaj/auth.py: 49%

77 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-17 17:03 +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} \ 

63 with an authentication file corresponding \n\ 

64to following pubkey `{pubkey_cksum}`?" 

65 click.confirm(message, abort=True) 

66 key.save_seedhex_file(auth_file) 

67 print( 

68 f"Authentication file '{auth_file}' generated and stored for public key: {pubkey_cksum}", 

69 ) 

70 

71 

72@click.pass_context 

73def auth_by_auth_file(ctx: click.Context) -> SigningKey: 

74 """ 

75 Uses an authentication file to generate the key 

76 Authfile can either be: 

77 * A seed in hexadecimal encoding 

78 * PubSec format with public and private key in base58 encoding 

79 """ 

80 authfile = ctx.obj["AUTH_FILE_PATH"] 

81 filetxt = authfile.read_text(encoding="utf-8") 

82 

83 # two regural expressions for the PubSec format 

84 regex_pubkey = re.compile(PUBSEC_PUBKEY_PATTERN, re.MULTILINE) 

85 regex_signkey = re.compile(PUBSEC_SIGNKEY_PATTERN, re.MULTILINE) 

86 

87 # Seed hexadecimal format 

88 if re.search(re.compile(SEED_HEX_PATTERN), filetxt): 

89 return SigningKey.from_seedhex_file(authfile) 

90 # PubSec format 

91 if re.search(regex_pubkey, filetxt) and re.search(regex_signkey, filetxt): 

92 return SigningKey.from_pubsec_file(authfile) 

93 sys.exit("Error: the format of the file is invalid") 

94 

95 

96def auth_by_seed() -> SigningKey: 

97 seedhex = click.prompt("Please enter your seed on hex format", hide_input=True) 

98 try: 

99 return SigningKey.from_seedhex(seedhex) 

100 # To be fixed upstream in DuniterPy 

101 except SigningKeyException as error: 

102 print(error) 

103 sys.exit(FAILURE_EXIT_STATUS) 

104 

105 

106@click.pass_context 

107def auth_by_scrypt(ctx: click.Context) -> SigningKey: 

108 salt = click.prompt( 

109 "Please enter your Scrypt Salt (Secret identifier)", 

110 hide_input=True, 

111 default="", 

112 ) 

113 password = click.prompt( 

114 "Please enter your Scrypt password (masked)", 

115 hide_input=True, 

116 default="", 

117 ) 

118 

119 if ctx.obj["AUTH_SCRYPT_PARAMS"]: 

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

121 

122 if n.isnumeric() and r.isnumeric() and p.isnumeric(): 

123 n, r, p = int(n), int(r), int(p) 

124 if n <= 0 or n > 65536 or r <= 0 or r > 512 or p <= 0 or p > 32: 

125 sys.exit("Error: the values of Scrypt parameters are not good") 

126 scrypt_params = ScryptParams(n, r, p) 

127 else: 

128 sys.exit("one of n, r or p is not a number") 

129 else: 

130 scrypt_params = None 

131 

132 try: 

133 return SigningKey.from_credentials(salt, password, scrypt_params) 

134 except ValueError as error: 

135 print(error) 

136 sys.exit(FAILURE_EXIT_STATUS) 

137 

138 

139def auth_by_wif() -> SigningKey: 

140 wif_hex = click.prompt( 

141 "Enter your WIF or Encrypted WIF address (masked)", 

142 hide_input=True, 

143 ) 

144 password = click.prompt( 

145 "(Leave empty in case WIF format) Enter the Encrypted WIF password (masked)", 

146 hide_input=True, 

147 ) 

148 try: 

149 return SigningKey.from_wif_or_ewif_hex(wif_hex, password) 

150 # To be fixed upstream in DuniterPy 

151 except SigningKeyException as error: 

152 print(error) 

153 sys.exit(FAILURE_EXIT_STATUS)