2
2
import struct
3
3
from base64 import b64decode
4
4
from contextlib import suppress
5
+ from typing import Optional
5
6
from urllib .parse import quote
6
7
from urllib .request import urlopen
7
8
11
12
from covert .elliptic import egcreate , egreveal
12
13
13
14
14
- def derive_symkey (nonce , local , remote ):
15
- assert local .sk , f"Missing secret key for { local = } "
16
- shared = sodium .crypto_scalarmult (local .sk , remote .pk )
17
- return sodium .crypto_hash_sha512 (bytes (nonce ) + shared )[:32 ]
18
-
19
-
20
15
class Key :
21
16
22
17
def __init__ (self , * , keystr = "" , comment = "" , sk = None , pk = None , edsk = None , edpk = None , pkhash = None ):
@@ -106,7 +101,13 @@ def _validate(self):
106
101
sodium .crypto_box_open (ciphertext , nonce , self .pk , self .sk )
107
102
108
103
109
- def read_pk_file (keystr ):
104
+ def derive_symkey (nonce , local : Key , remote : Key ) -> bytes :
105
+ assert local .sk , f"Missing secret key for { local = } "
106
+ shared = sodium .crypto_scalarmult (local .sk , remote .pk )
107
+ return sodium .crypto_hash_sha512 (bytes (nonce ) + shared )[:32 ]
108
+
109
+
110
+ def read_pk_file (keystr : str ) -> list [Key ]:
110
111
ghuser = None
111
112
if keystr .startswith ("github:" ):
112
113
ghuser = keystr [7 :]
@@ -135,13 +136,13 @@ def read_pk_file(keystr):
135
136
return keys
136
137
137
138
138
- def read_sk_any (keystr ) :
139
+ def read_sk_any (keystr : str ) -> list [ Key ] :
139
140
with suppress (ValueError ):
140
- return decode_sk (keystr )
141
+ return [ decode_sk (keystr )]
141
142
return read_sk_file (keystr )
142
143
143
144
144
- def read_sk_file (keystr ) :
145
+ def read_sk_file (keystr : str ) -> list [ Key ] :
145
146
if not os .path .isfile (keystr ):
146
147
raise ValueError (f"Secret key file { keystr } not found" )
147
148
with open (keystr , "rb" ) as f :
@@ -163,16 +164,16 @@ def read_sk_file(keystr):
163
164
return keys
164
165
165
166
166
- def decode_pk (keystr ) :
167
+ def decode_pk (keystr : str ) -> Key :
167
168
# Age keys use Bech32 encoding
168
169
if keystr .startswith ("age1" ):
169
170
return decode_age_pk (keystr )
170
171
# Try Base64 encoded formats
171
172
try :
172
173
token , comment = keystr , ''
173
174
if keystr .startswith ('ssh-ed25519 ' ):
174
- t , token , * comment = keystr .split (' ' , 2 )
175
- comment = comment [0 ] if comment else 'ssh'
175
+ t , token , * cmt = keystr .split (' ' , 2 )
176
+ comment = cmt [0 ] if cmt else 'ssh'
176
177
keybytes = b64decode (token , validate = True )
177
178
ssh = keybytes .startswith (b"\x00 \x00 \x00 \x0b ssh-ed25519\x00 \x00 \x00 " )
178
179
minisign = len (keybytes ) == 42 and keybytes .startswith (b'Ed' )
@@ -188,7 +189,7 @@ def decode_pk(keystr):
188
189
raise ValueError (f"Unrecognized key { keystr } " )
189
190
190
191
191
- def decode_sk (keystr ) :
192
+ def decode_sk (keystr : str ) -> Key :
192
193
# Age secret keys in Bech32 encoding
193
194
if keystr .lower ().startswith ("age-secret-key-" ):
194
195
return decode_age_sk (keystr )
@@ -198,48 +199,49 @@ def decode_sk(keystr):
198
199
# Plain Curve25519 key (WireGuard)
199
200
try :
200
201
keybytes = b64decode (keystr , validate = True )
201
- if len (keybytes ) == 32 :
202
- return Key (sk = keybytes )
202
+ # Must be a clamped scalar
203
+ if len (keybytes ) == 32 and keybytes [0 ] & 8 == 0 and keybytes [31 ] & 0xC0 == 0x40 :
204
+ return Key (keystr = keystr , sk = keybytes , comment = "wg" )
203
205
except ValueError :
204
206
pass
205
- raise ValueError (f"Unable to parse private key { keystr !r} " )
207
+ raise ValueError (f"Unable to parse secret key { keystr !r} " )
206
208
207
209
208
- def decode_sk_minisign (keystr , pw = None ):
210
+ def decode_sk_minisign (keystr : str , pw : Optional [ bytes ] = None ) -> Key :
209
211
# None means try without password, then ask
210
212
if pw is None :
211
213
try :
212
214
return decode_sk_minisign (keystr , b'' )
213
215
except ValueError :
214
216
pass
215
- pw = util . encode ( passphrase .ask ('Minisign passkey' )[0 ])
217
+ pw = passphrase .ask ('Minisign passkey' )[0 ]
216
218
return decode_sk_minisign (keystr , pw )
217
219
data = b64decode (keystr )
218
220
fmt , salt , ops , mem , token = struct .unpack ('<6s32sQQ104s' , data )
219
221
if fmt != b'EdScB2' or ops != 1 << 25 or mem != 1 << 30 :
220
222
raise ValueError (f'Not a (supported) Minisign secret key { fmt = } ' )
221
- out = sodium .crypto_pwhash_scryptsalsa208sha256_ll (pw , salt , n = 1 << 20 , r = 8 , p = 1 , maxmem = float ( 'inf' ) , dklen = 104 )
223
+ out = sodium .crypto_pwhash_scryptsalsa208sha256_ll (pw , salt , n = 1 << 20 , r = 8 , p = 1 , maxmem = 1 << 31 , dklen = 104 )
222
224
token = util .xor (out , token )
223
225
keyid , edsk , edpk , csum = struct .unpack ('8s32s32s32s' , token )
224
226
b2state = sodium .crypto_generichash_blake2b_init ()
225
227
sodium .crypto_generichash .generichash_blake2b_update (b2state , fmt [:2 ] + keyid + edsk + edpk )
226
228
csum2 = sodium .crypto_generichash .generichash_blake2b_final (b2state )
227
229
if csum != csum2 :
228
230
raise ValueError ('Unable to decrypt Minisign secret key' )
229
- return Key (edsk = edsk , edpk = edpk )
231
+ return Key (edsk = edsk , edpk = edpk , comment = "ms" )
230
232
231
233
232
- def decode_age_pk (keystr ) :
234
+ def decode_age_pk (keystr : str ) -> Key :
233
235
return Key (keystr = keystr , comment = "age" , pk = bech .decode ("age" , keystr .lower ()))
234
236
235
237
236
- def encode_age_pk (key ) :
238
+ def encode_age_pk (key : Key ) -> str :
237
239
return bech .encode ("age" , key .pk )
238
240
239
241
240
- def decode_age_sk (keystr ) :
242
+ def decode_age_sk (keystr : str ) -> Key :
241
243
return Key (keystr = keystr , comment = "age" , sk = bech .decode ("age-secret-key-" , keystr .lower ()))
242
244
243
245
244
- def encode_age_sk (key ) :
246
+ def encode_age_sk (key : Key ) -> str :
245
247
return bech .encode ("age-secret-key-" , key .sk ).upper ()
0 commit comments