a101d1f604a3ad6de242989f7a4887b78ab012a1
Marco Ricci Import initial project files

Marco Ricci authored 4 months ago

1) # SPDX-FileCopyrightText: 2024 Marco Ricci <m@the13thletter.info>
2) #
3) # SPDX-License-Identifier: MIT
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

4) 
5) """Work-alike of vault(1) – a deterministic, stateless password manager
6) 
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

7) """  # noqa: RUF002
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

8) 
9) from __future__ import annotations
10) 
Marco Ricci Support textual passphrases...

Marco Ricci authored 2 months ago

11) import base64
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

12) import collections
13) import hashlib
14) import math
Marco Ricci Support textual passphrases...

Marco Ricci authored 2 months ago

15) import unicodedata
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

16) 
Marco Ricci Support Python 3.10 and PyP...

Marco Ricci authored 2 months ago

17) from typing_extensions import assert_type
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

18) 
19) import sequin
20) import ssh_agent_client
21) 
Marco Ricci Remove __about__.py files,...

Marco Ricci authored 3 months ago

22) __author__ = "Marco Ricci <m@the13thletter.info>"
Marco Ricci Release 0.1.1

Marco Ricci authored 2 months ago

23) __version__ = "0.1.1"
Marco Ricci Remove __about__.py files,...

Marco Ricci authored 3 months ago

24) 
Marco Ricci Support textual passphrases...

Marco Ricci authored 2 months ago

25) class AmbiguousByteRepresentationError(ValueError):
26)     """The object has an ambiguous byte representation."""
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

27)     def __init__(self):
28)         super().__init__('text string has ambiguous byte representation')
29) 
30) _CHARSETS = collections.OrderedDict([
31)     ('lower', b'abcdefghijklmnopqrstuvwxyz'),
32)     ('upper', b'ABCDEFGHIJKLMNOPQRSTUVWXYZ'),
33)     ('alpha', b''),  # Placeholder.
34)     ('number', b'0123456789'),
35)     ('alphanum', b''),  # Placeholder.
36)     ('space', b' '),
37)     ('dash', b'-_'),
38)     ('symbol', b'!"#$%&\'()*+,./:;<=>?@[\\]^{|}~-_'),
39)     ('all', b''),  # Placeholder.
40) ])
41) _CHARSETS['alpha'] = _CHARSETS['lower'] + _CHARSETS['upper']
42) _CHARSETS['alphanum'] = _CHARSETS['alpha'] + _CHARSETS['number']
43) _CHARSETS['all'] = (_CHARSETS['alphanum'] + _CHARSETS['space']
44)                     + _CHARSETS['symbol'])
Marco Ricci Support textual passphrases...

Marco Ricci authored 2 months ago

45) 
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

46) class Vault:
47)     """A work-alike of James Coglan's vault.
48) 
49)     Store settings for generating (actually: deriving) passphrases for
50)     named services, with various constraints, given only a master
51)     passphrase.  Also, actually generate the passphrase.  The derivation
52)     is deterministic and non-secret; only the master passphrase need be
53)     kept secret.  The implementation is compatible with [vault][].
54) 
55)     [James Coglan explains the passphrase derivation algorithm in great
56)     detail][ALGORITHM] in his blog post on said topic: A principally
57)     infinite bit stream is obtained by running a key-derivation function
58)     on the master passphrase and the service name, then this bit stream
Marco Ricci Fix documentation link to `...

Marco Ricci authored 3 months ago

59)     is fed into a [Sequin][sequin.Sequin] to generate random numbers in
60)     the correct range, and finally these random numbers select
61)     passphrase characters until the desired length is reached.
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

62) 
63)     [vault]: https://getvau.lt
64)     [ALGORITHM]: https://blog.jcoglan.com/2012/07/16/designing-vaults-generator-algorithm/
65) 
66)     """
67)     _UUID = b'e87eb0f4-34cb-46b9-93ad-766c5ab063e7'
68)     """A tag used by vault in the bit stream generation."""
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

69)     _CHARSETS = _CHARSETS
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

70)     """
71)         Known character sets from which to draw passphrase characters.
72)         Relies on a certain, fixed order for their definition and their
73)         contents.
74) 
75)     """
76) 
77)     def __init__(
Marco Ricci Support textual passphrases...

Marco Ricci authored 2 months ago

78)         self, *, phrase: bytes | bytearray | str = b'',
79)         length: int = 20, repeat: int = 0, lower: int | None = None,
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

80)         upper: int | None = None, number: int | None = None,
81)         space: int | None = None, dash: int | None = None,
82)         symbol: int | None = None,
83)     ) -> None:
84)         """Initialize the Vault object.
85) 
86)         Args:
87)             phrase:
88)                 The master passphrase from which to derive the service
Marco Ricci Support textual passphrases...

Marco Ricci authored 2 months ago

89)                 passphrases.  If a text string, then the byte
90)                 representation must be unique.
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

91)             length:
92)                 Desired passphrase length.
93)             repeat:
94)                 The maximum number of immediate character repetitions
95)                 allowed in the passphrase.  Disabled if set to 0.
96)             lower:
Marco Ricci Support textual passphrases...

Marco Ricci authored 2 months ago

97)                 Optional constraint on ASCII lowercase characters.  If
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

98)                 positive, include this many lowercase characters
99)                 somewhere in the passphrase.  If 0, avoid lowercase
100)                 characters altogether.
101)             upper:
Marco Ricci Support textual passphrases...

Marco Ricci authored 2 months ago

102)                 Same as `lower`, but for ASCII uppercase characters.
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

103)             number:
104)                 Same as `lower`, but for ASCII digits.
105)             space:
106)                 Same as `lower`, but for the space character.
107)             dash:
108)                 Same as `lower`, but for the hyphen-minus and underscore
109)                 characters.
110)             symbol:
111)                 Same as `lower`, but for all other hitherto unlisted
112)                 ASCII printable characters (except backquote).
113) 
Marco Ricci Support textual passphrases...

Marco Ricci authored 2 months ago

114)         Raises:
115)             AmbiguousByteRepresentationError:
116)                 The phrase is a text string with differing NFC- and
117)                 NFD-normalized UTF-8 byte representations.
118) 
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

119)         """
Marco Ricci Support textual passphrases...

Marco Ricci authored 2 months ago

120)         self._phrase = self._get_binary_string(phrase)
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

121)         self._length = length
122)         self._repeat = repeat
123)         self._allowed = bytearray(self._CHARSETS['all'])
124)         self._required: list[bytes] = []
125)         def subtract_or_require(
126)             count: int | None, characters: bytes | bytearray
127)         ) -> None:
128)             if not isinstance(count, int):
129)                 return
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

130)             if count <= 0:
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

131)                 self._allowed = self._subtract(characters, self._allowed)
132)             else:
133)                 for _ in range(count):
134)                     self._required.append(characters)
135)         subtract_or_require(lower, self._CHARSETS['lower'])
136)         subtract_or_require(upper, self._CHARSETS['upper'])
137)         subtract_or_require(number, self._CHARSETS['number'])
138)         subtract_or_require(space, self._CHARSETS['space'])
139)         subtract_or_require(dash, self._CHARSETS['dash'])
140)         subtract_or_require(symbol, self._CHARSETS['symbol'])
Marco Ricci Fix numerous argument type...

Marco Ricci authored 3 months ago

141)         if len(self._required) > self._length:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

142)             msg = 'requested passphrase length too short'
143)             raise ValueError(msg)
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

144)         if not self._allowed:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

145)             msg = 'no allowed characters left'
146)             raise ValueError(msg)
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

147)         for _ in range(len(self._required), self._length):
148)             self._required.append(bytes(self._allowed))
149) 
Marco Ricci Expose some functionality f...

Marco Ricci authored 2 months ago

150)     def _entropy(self) -> float:
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

151)         """Estimate the passphrase entropy, given the current settings.
152) 
153)         The entropy is the base 2 logarithm of the amount of
Marco Ricci Expose some functionality f...

Marco Ricci authored 2 months ago

154)         possibilities.  We operate directly on the logarithms, and use
155)         sorting and [`math.fsum`][] to keep high accuracy.
156) 
157)         Note:
158)             We actually overestimate the entropy here because of poor
159)             handling of character repetitions.  In the extreme, assuming
160)             that only one character were allowed, then because there is
161)             only one possible string of each given length, the entropy
162)             of that string `s` is always be zero.  However, we calculate
163)             the entropy as `math.log2(math.factorial(len(s)))`, i.e. we
164)             assume the characters at the respective string position are
165)             distinguishable from each other.
166) 
167)         Returns:
168)             A valid (and somewhat close) upper bound to the entropy.
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

169) 
170)         """
171)         factors: list[int] = []
Marco Ricci Expose some functionality f...

Marco Ricci authored 2 months ago

172)         if not self._required or any(not x for x in self._required):
173)             return float('-inf')
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

174)         for i, charset in enumerate(self._required):
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

175)             factors.extend([i + 1, len(charset)])
Marco Ricci Expose some functionality f...

Marco Ricci authored 2 months ago

176)         factors.sort()
177)         return math.fsum(math.log2(f) for f in factors)
178) 
179)     def _estimate_sufficient_hash_length(
180)         self, safety_factor: float = 2.0,
181)     ) -> int:
182)         """Estimate the sufficient hash length, given the current settings.
183) 
184)         Using the entropy (via `_entropy`) and a safety factor, give an
185)         initial estimate of the length to use for `create_hash` such
186)         that using a `Sequin` with this hash will not exhaust it during
187)         passphrase generation.
188) 
189)         Args:
190)             safety_factor: The safety factor.  Must be at least 1.
191) 
192)         Returns:
193)             The estimated sufficient hash length.
194) 
195)         Warning:
196)             This is a heuristic, not an exact computation; it may
197)             underestimate the true necessary hash length.  It is
198)             intended as a starting point for searching for a sufficient
199)             hash length, usually by doubling the hash length each time
200)             it does not yet prove so.
201) 
202)         """
203)         try:
204)             safety_factor = float(safety_factor)
205)         except TypeError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

206)             msg = f'invalid safety factor: not a float: {safety_factor!r}'
207)             raise TypeError(msg) from e
Marco Ricci Expose some functionality f...

Marco Ricci authored 2 months ago

208)         if not math.isfinite(safety_factor) or safety_factor < 1.0:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

209)             msg = f'invalid safety factor {safety_factor!r}'
210)             raise ValueError(msg)
Marco Ricci Expose some functionality f...

Marco Ricci authored 2 months ago

211)         # Ensure the bound is strictly positive.
212)         entropy_bound = max(1, self._entropy())
213)         return int(math.ceil(safety_factor * entropy_bound / 8))
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

214) 
Marco Ricci Support textual passphrases...

Marco Ricci authored 2 months ago

215)     @staticmethod
216)     def _get_binary_string(s: bytes | bytearray | str, /) -> bytes:
217)         """Convert the input string to a read-only, binary string.
218) 
219)         If it is a text string, then test for an unambiguous UTF-8
220)         representation, otherwise abort.  (That is, check whether the
221)         NFC and NFD forms of the string coincide.)
222) 
223)         Args:
224)             s: The string to (check and) convert.
225) 
226)         Returns:
227)             A read-only, binary copy of the string.
228) 
229)         Raises:
230)             AmbiguousByteRepresentationError:
231)                 The text string has differing NFC- and NFD-normalized
232)                 UTF-8 byte representations.
233) 
234)         """
235)         if isinstance(s, str):
236)             norm = unicodedata.normalize
237)             if norm('NFC', s) != norm('NFD', s):
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

238)                 raise AmbiguousByteRepresentationError
Marco Ricci Support textual passphrases...

Marco Ricci authored 2 months ago

239)             return s.encode('UTF-8')
240)         return bytes(s)
241) 
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

242)     @classmethod
243)     def create_hash(
Marco Ricci Support textual passphrases...

Marco Ricci authored 2 months ago

244)         cls, phrase: bytes | bytearray | str,
245)         service: bytes | bytearray, *, length: int = 32,
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

246)     ) -> bytes:
Marco Ricci Use neutral arguments in `V...

Marco Ricci authored 3 months ago

247)         r"""Create a pseudorandom byte stream from phrase and service.
248) 
249)         Create a pseudorandom byte stream from `phrase` and `service` by
250)         feeding them into the key-derivation function PBKDF2
251)         (8 iterations, using SHA-1).
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

252) 
253)         Args:
Marco Ricci Use neutral arguments in `V...

Marco Ricci authored 3 months ago

254)             phrase:
255)                 A master passphrase, or sometimes an SSH signature.
256)                 Used as the key for PBKDF2, the underlying cryptographic
257)                 primitive.
Marco Ricci Support textual passphrases...

Marco Ricci authored 2 months ago

258) 
259)                 If a text string, then the byte representation must be
260)                 unique.
Marco Ricci Use neutral arguments in `V...

Marco Ricci authored 3 months ago

261)             service:
262)                 A vault service name.  Will be suffixed with
263)                 `Vault._UUID`, and then used as the salt value for
264)                 PBKDF2.
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

265)             length:
266)                 The length of the byte stream to generate.
267) 
268)         Returns:
269)             A pseudorandom byte string of length `length`.
270) 
Marco Ricci Support textual passphrases...

Marco Ricci authored 2 months ago

271)         Raises:
272)             AmbiguousByteRepresentationError:
273)                 The phrase is a text string with differing NFC- and
274)                 NFD-normalized UTF-8 byte representations.
275) 
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

276)         Note:
277)             Shorter values returned from this method (with the same key
278)             and message) are prefixes of longer values returned from
279)             this method.  (This property is inherited from the
280)             underlying PBKDF2 function.)  It is thus safe (if slow) to
281)             call this method with the same input with ever-increasing
282)             target lengths.
283) 
Marco Ricci Use neutral arguments in `V...

Marco Ricci authored 3 months ago

284)         Examples:
Marco Ricci Fix passphrase-from-SSH-sig...

Marco Ricci authored 2 months ago

285)             >>> # See also Vault.phrase_from_key examples.
Marco Ricci Use neutral arguments in `V...

Marco Ricci authored 3 months ago

286)             >>> phrase = bytes.fromhex('''
287)             ... 00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39
288)             ... 00 00 00 40
289)             ... f0 98 19 80 6c 1a 97 d5 26 03 6e cc e3 65 8f 86
290)             ... 66 07 13 19 13 09 21 33 33 f9 e4 36 53 1d af fd
291)             ... 0d 08 1f ec f8 73 9b 8c 5f 55 39 16 7c 53 54 2c
292)             ... 1e 52 bb 30 ed 7f 89 e2 2f 69 51 55 d8 9e a6 02
293)             ... ''')
294)             >>> Vault.create_hash(phrase, b'some_service', length=4)
295)             b'M\xb1<S'
296)             >>> Vault.create_hash(phrase, b'some_service', length=16)
297)             b'M\xb1<S\x827E\xd1M\xaf\xf8~\xc8n\x10\xcc'
298)             >>> Vault.create_hash(phrase, b'NOSUCHSERVICE', length=16)
299)             b'\x1c\xc3\x9c\xd9\xb6\x1a\x99CS\x07\xc41\xf4\x85#s'
300) 
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

301)         """
Marco Ricci Support textual passphrases...

Marco Ricci authored 2 months ago

302)         phrase = cls._get_binary_string(phrase)
303)         assert not isinstance(phrase, str)
Marco Ricci Use neutral arguments in `V...

Marco Ricci authored 3 months ago

304)         salt = bytes(service) + cls._UUID
305)         return hashlib.pbkdf2_hmac(hash_name='sha1', password=phrase,
306)                                    salt=salt, iterations=8, dklen=length)
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

307) 
308)     def generate(
Marco Ricci Fix numerous argument type...

Marco Ricci authored 3 months ago

309)         self, service_name: str | bytes | bytearray, /, *,
Marco Ricci Support textual passphrases...

Marco Ricci authored 2 months ago

310)         phrase: bytes | bytearray | str = b'',
Marco Ricci Fix numerous argument type...

Marco Ricci authored 3 months ago

311)     ) -> bytes:
312)         r"""Generate a service passphrase.
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

313) 
314)         Args:
315)             service_name:
316)                 The service name.
317)             phrase:
318)                 If given, override the passphrase given during
319)                 construction.
320) 
Marco Ricci Support textual passphrases...

Marco Ricci authored 2 months ago

321)                 If a text string, then the byte representation must be
322)                 unique.
323) 
324)         Returns:
325)             The service passphrase.
326) 
327)         Raises:
328)             AmbiguousByteRepresentationError:
329)                 The phrase is a text string with differing NFC- and
330)                 NFD-normalized UTF-8 byte representations.
331) 
Marco Ricci Add unit tests, both new an...

Marco Ricci authored 3 months ago

332)         Examples:
333)             >>> phrase = b'She cells C shells bye the sea shoars'
334)             >>> # Using default options in constructor.
335)             >>> Vault(phrase=phrase).generate(b'google')
336)             b': 4TVH#5:aZl8LueOT\\{'
337)             >>> # Also possible:
338)             >>> Vault().generate(b'google', phrase=phrase)
339)             b': 4TVH#5:aZl8LueOT\\{'
340) 
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

341)         """
Marco Ricci Expose some functionality f...

Marco Ricci authored 2 months ago

342)         hash_length = self._estimate_sufficient_hash_length()
343)         assert hash_length >= 1
Marco Ricci Fix numerous argument type...

Marco Ricci authored 3 months ago

344)         # Ensure the phrase is a bytes object.  Needed later for safe
345)         # concatenation.
346)         if isinstance(service_name, str):
347)             service_name = service_name.encode('utf-8')
348)         elif not isinstance(service_name, bytes):
349)             service_name = bytes(service_name)
350)         assert_type(service_name, bytes)
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

351)         if not phrase:
352)             phrase = self._phrase
Marco Ricci Support textual passphrases...

Marco Ricci authored 2 months ago

353)         phrase = self._get_binary_string(phrase)
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

354)         # Repeat the passphrase generation with ever-increasing hash
355)         # lengths, until the passphrase can be formed without exhausting
356)         # the sequin.  See the guarantee in the create_hash method for
357)         # why this works.
358)         while True:
359)             try:
360)                 required = self._required[:]
361)                 seq = sequin.Sequin(self.create_hash(
Marco Ricci Use neutral arguments in `V...

Marco Ricci authored 3 months ago

362)                     phrase=phrase, service=service_name, length=hash_length))
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

363)                 result = bytearray()
364)                 while len(result) < self._length:
365)                     pos = seq.generate(len(required))
366)                     charset = required.pop(pos)
367)                     # Determine if an unlucky choice right now might
368)                     # violate the restriction on repeated characters.
369)                     # That is, check if the current partial passphrase
370)                     # ends with r - 1 copies of the same character
371)                     # (where r is the repeat limit that must not be
372)                     # reached), and if so, remove this same character
373)                     # from the current character's allowed set.
Marco Ricci Fix repeated character dete...

Marco Ricci authored 3 months ago

374)                     if self._repeat and result:
375)                         bad_suffix = bytes(result[-1:]) * (self._repeat - 1)
376)                         if result.endswith(bad_suffix):
377)                             charset = self._subtract(bytes(result[-1:]),
378)                                                      charset)
Marco Ricci Fix numerous argument type...

Marco Ricci authored 3 months ago

379)                     pos = seq.generate(len(charset))
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

380)                     result.extend(charset[pos:pos + 1])
Marco Ricci Rename SequinExhaustedExcep...

Marco Ricci authored 2 months ago

381)             except sequin.SequinExhaustedError:
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

382)                 hash_length *= 2
383)             else:
Marco Ricci Fix numerous argument type...

Marco Ricci authored 3 months ago

384)                 return bytes(result)
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

385) 
Marco Ricci Expose some functionality f...

Marco Ricci authored 2 months ago

386)     @staticmethod
387)     def _is_suitable_ssh_key(key: bytes | bytearray, /) -> bool:
388)         """Check whether the key is suitable for passphrase derivation.
389) 
390)         Currently, this only checks whether signatures with this key
391)         type are deterministic.
392) 
393)         Args:
394)             key: SSH public key to check.
395) 
396)         Returns:
397)             True if and only if the key is suitable for use in deriving
398)             a passphrase deterministically.
399) 
400)         """
401)         deterministic_signature_types = {
402)             'ssh-ed25519':
403)                 lambda k: k.startswith(b'\x00\x00\x00\x0bssh-ed25519'),
404)             'ssh-ed448':
405)                 lambda k: k.startswith(b'\x00\x00\x00\x09ssh-ed448'),
406)             'ssh-rsa':
407)                 lambda k: k.startswith(b'\x00\x00\x00\x07ssh-rsa'),
408)         }
409)         return any(v(key) for v in deterministic_signature_types.values())
410) 
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

411)     @classmethod
Marco Ricci Fix passphrase-from-SSH-sig...

Marco Ricci authored 2 months ago

412)     def phrase_from_key(
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

413)         cls, key: bytes | bytearray, /
Marco Ricci Fix passphrase-from-SSH-sig...

Marco Ricci authored 2 months ago

414)     ) -> bytes:
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

415)         """Obtain the master passphrase from a configured SSH key.
416) 
417)         vault allows the usage of certain SSH keys to derive a master
418)         passphrase, by signing the vault UUID with the SSH key.  The key
419)         type must ensure that signatures are deterministic.
420) 
421)         Args:
422)             key: The (public) SSH key to use for signing.
423) 
424)         Returns:
Marco Ricci Fix passphrase-from-SSH-sig...

Marco Ricci authored 2 months ago

425)             The signature of the vault UUID under this key, unframed but
426)             encoded in base64.
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

427) 
428)         Raises:
429)             ValueError:
430)                 The SSH key is principally unsuitable for this use case.
431)                 Usually this means that the signature is not
432)                 deterministic.
433) 
Marco Ricci Add example for `Vault.phra...

Marco Ricci authored 3 months ago

434)         Examples:
Marco Ricci Fix passphrase-from-SSH-sig...

Marco Ricci authored 2 months ago

435)             >>> import base64
436)             >>> # Actual Ed25519 test public key.
Marco Ricci Add example for `Vault.phra...

Marco Ricci authored 3 months ago

437)             >>> public_key = bytes.fromhex('''
438)             ... 00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39
439)             ... 00 00 00 20
440)             ... 81 78 81 68 26 d6 02 48 5f 0f ff 32 48 6f e4 c1
441)             ... 30 89 dc 1c 6a 45 06 09 e9 09 0f fb c2 12 69 76
442)             ... ''')
Marco Ricci Fix passphrase-from-SSH-sig...

Marco Ricci authored 2 months ago

443)             >>> expected_sig_raw = bytes.fromhex('''
Marco Ricci Add example for `Vault.phra...

Marco Ricci authored 3 months ago

444)             ... 00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39
445)             ... 00 00 00 40
446)             ... f0 98 19 80 6c 1a 97 d5 26 03 6e cc e3 65 8f 86
447)             ... 66 07 13 19 13 09 21 33 33 f9 e4 36 53 1d af fd
448)             ... 0d 08 1f ec f8 73 9b 8c 5f 55 39 16 7c 53 54 2c
449)             ... 1e 52 bb 30 ed 7f 89 e2 2f 69 51 55 d8 9e a6 02
450)             ... ''')
Marco Ricci Fix passphrase-from-SSH-sig...

Marco Ricci authored 2 months ago

451)             >>> # Raw Ed25519 signatures are 64 bytes long.
452)             >>> signature_blob = expected_sig_raw[-64:]
453)             >>> phrase = base64.standard_b64encode(signature_blob)
454)             >>> Vault.phrase_from_key(phrase) == expected  # doctest:+SKIP
Marco Ricci Add example for `Vault.phra...

Marco Ricci authored 3 months ago

455)             True
456) 
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

457)         """
Marco Ricci Expose some functionality f...

Marco Ricci authored 2 months ago

458)         if not cls._is_suitable_ssh_key(key):
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

459)             msg = ('unsuitable SSH key: bad key, or '
460)                    'signature not deterministic')
461)             raise ValueError(msg)
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

462)         with ssh_agent_client.SSHAgentClient() as client:
Marco Ricci Fix passphrase-from-SSH-sig...

Marco Ricci authored 2 months ago

463)             raw_sig = client.sign(key, cls._UUID)
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

464)         _keytype, trailer = client.unstring_prefix(raw_sig)
Marco Ricci Fix passphrase-from-SSH-sig...

Marco Ricci authored 2 months ago

465)         signature_blob = client.unstring(trailer)
466)         return bytes(base64.standard_b64encode(signature_blob))
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

467) 
Marco Ricci Fix character set subtracti...

Marco Ricci authored 3 months ago

468)     @staticmethod
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

469)     def _subtract(
Marco Ricci Fix character set subtracti...

Marco Ricci authored 3 months ago

470)         charset: bytes | bytearray, allowed: bytes | bytearray,
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

471)     ) -> bytearray:
472)         """Remove the characters in charset from allowed.
473) 
474)         This preserves the relative order of characters in `allowed`.
475) 
476)         Args:
Marco Ricci Fix character set subtracti...

Marco Ricci authored 3 months ago

477)             charset:
478)                 Characters to remove.  Must not contain duplicate
479)                 characters.
480)             allowed:
481)                 Character set to remove the other characters from.  Must
482)                 not contain duplicate characters.
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

483) 
484)         Returns:
Marco Ricci Fix character set subtracti...

Marco Ricci authored 3 months ago

485)             The pruned "allowed" character set.
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

486) 
487)         Raises:
Marco Ricci Fix character set subtracti...

Marco Ricci authored 3 months ago

488)             ValueError:
489)                 `allowed` or `charset` contained duplicate characters.
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

490) 
491)         """
492)         allowed = (allowed if isinstance(allowed, bytearray)
493)                    else bytearray(allowed))
494)         assert_type(allowed, bytearray)
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

495)         msg_dup_characters = 'duplicate characters in set'
Marco Ricci Fix character set subtracti...

Marco Ricci authored 3 months ago

496)         if len(frozenset(allowed)) != len(allowed):
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

497)             raise ValueError(msg_dup_characters)
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

498)         if len(frozenset(charset)) != len(charset):
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

499)             raise ValueError(msg_dup_characters)
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

500)         for c in charset:
501)             try:
502)                 pos = allowed.index(c)
Marco Ricci Fix numerous argument type...

Marco Ricci authored 3 months ago

503)             except ValueError:
Marco Ricci Add prototype implementation

Marco Ricci authored 4 months ago

504)                 pass
505)             else:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

506)                 allowed[pos:pos + 1] = []