7495b213d512315c4e72be5bda1a5cdd7e9030a0
Marco Ricci Import initial project files

Marco Ricci authored 6 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 5 months ago

4) 
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

5) """Work-alike of vault(1) – a deterministic, stateless password manager"""  # noqa: RUF002
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

6) 
7) from __future__ import annotations
8) 
Marco Ricci Support textual passphrases...

Marco Ricci authored 4 months ago

9) import base64
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

10) import collections
11) import hashlib
12) import math
Marco Ricci Support textual passphrases...

Marco Ricci authored 4 months ago

13) import unicodedata
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

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

Marco Ricci authored 3 months ago

15) from typing_extensions import assert_type
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

16) 
17) import sequin
18) import ssh_agent_client
19) 
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

20) __author__ = 'Marco Ricci <m@the13thletter.info>'
Marco Ricci Release 0.1.2

Marco Ricci authored 3 months ago

21) __version__ = '0.1.2'
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

22) 
Marco Ricci Remove __about__.py files,...

Marco Ricci authored 5 months ago

23) 
Marco Ricci Support textual passphrases...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

26)     def __init__(self):
27)         super().__init__('text string has ambiguous byte representation')
28) 
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

29) 
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

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']
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

43) _CHARSETS['all'] = (
44)     _CHARSETS['alphanum'] + _CHARSETS['space'] + _CHARSETS['symbol']
45) )
46) 
Marco Ricci Support textual passphrases...

Marco Ricci authored 4 months ago

47) 
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

64) 
65)     [vault]: https://getvau.lt
66)     [ALGORITHM]: https://blog.jcoglan.com/2012/07/16/designing-vaults-generator-algorithm/
67) 
68)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

69) 
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

70)     _UUID = b'e87eb0f4-34cb-46b9-93ad-766c5ab063e7'
71)     """A tag used by vault in the bit stream generation."""
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

72)     _CHARSETS = _CHARSETS
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

73)     """
74)         Known character sets from which to draw passphrase characters.
75)         Relies on a certain, fixed order for their definition and their
76)         contents.
77) 
78)     """
79) 
80)     def __init__(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

81)         self,
82)         *,
83)         phrase: bytes | bytearray | str = b'',
84)         length: int = 20,
85)         repeat: int = 0,
86)         lower: int | None = None,
87)         upper: int | None = None,
88)         number: int | None = None,
89)         space: int | None = None,
90)         dash: int | None = None,
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

91)         symbol: int | None = None,
92)     ) -> None:
93)         """Initialize the Vault object.
94) 
95)         Args:
96)             phrase:
97)                 The master passphrase from which to derive the service
Marco Ricci Support textual passphrases...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

100)             length:
101)                 Desired passphrase length.
102)             repeat:
103)                 The maximum number of immediate character repetitions
104)                 allowed in the passphrase.  Disabled if set to 0.
105)             lower:
Marco Ricci Support textual passphrases...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

107)                 positive, include this many lowercase characters
108)                 somewhere in the passphrase.  If 0, avoid lowercase
109)                 characters altogether.
110)             upper:
Marco Ricci Support textual passphrases...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

112)             number:
113)                 Same as `lower`, but for ASCII digits.
114)             space:
115)                 Same as `lower`, but for the space character.
116)             dash:
117)                 Same as `lower`, but for the hyphen-minus and underscore
118)                 characters.
119)             symbol:
120)                 Same as `lower`, but for all other hitherto unlisted
121)                 ASCII printable characters (except backquote).
122) 
Marco Ricci Support textual passphrases...

Marco Ricci authored 4 months ago

123)         Raises:
124)             AmbiguousByteRepresentationError:
125)                 The phrase is a text string with differing NFC- and
126)                 NFD-normalized UTF-8 byte representations.
127) 
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

130)         self._length = length
131)         self._repeat = repeat
132)         self._allowed = bytearray(self._CHARSETS['all'])
133)         self._required: list[bytes] = []
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

134) 
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

135)         def subtract_or_require(
136)             count: int | None, characters: bytes | bytearray
137)         ) -> None:
138)             if not isinstance(count, int):
139)                 return
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

141)                 self._allowed = self._subtract(characters, self._allowed)
142)             else:
143)                 for _ in range(count):
144)                     self._required.append(characters)
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

145) 
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

146)         subtract_or_require(lower, self._CHARSETS['lower'])
147)         subtract_or_require(upper, self._CHARSETS['upper'])
148)         subtract_or_require(number, self._CHARSETS['number'])
149)         subtract_or_require(space, self._CHARSETS['space'])
150)         subtract_or_require(dash, self._CHARSETS['dash'])
151)         subtract_or_require(symbol, self._CHARSETS['symbol'])
Marco Ricci Fix numerous argument type...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

158)         for _ in range(len(self._required), self._length):
159)             self._required.append(bytes(self._allowed))
160) 
Marco Ricci Expose some functionality f...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

180) 
181)         """
182)         factors: list[int] = []
Marco Ricci Expose some functionality f...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

187)         factors.sort()
188)         return math.fsum(math.log2(f) for f in factors)
189) 
190)     def _estimate_sufficient_hash_length(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

191)         self,
192)         safety_factor: float = 2.0,
Marco Ricci Expose some functionality f...

Marco Ricci authored 4 months ago

193)     ) -> int:
194)         """Estimate the sufficient hash length, given the current settings.
195) 
196)         Using the entropy (via `_entropy`) and a safety factor, give an
197)         initial estimate of the length to use for `create_hash` such
198)         that using a `Sequin` with this hash will not exhaust it during
199)         passphrase generation.
200) 
201)         Args:
202)             safety_factor: The safety factor.  Must be at least 1.
203) 
204)         Returns:
205)             The estimated sufficient hash length.
206) 
207)         Warning:
208)             This is a heuristic, not an exact computation; it may
209)             underestimate the true necessary hash length.  It is
210)             intended as a starting point for searching for a sufficient
211)             hash length, usually by doubling the hash length each time
212)             it does not yet prove so.
213) 
214)         """
215)         try:
216)             safety_factor = float(safety_factor)
217)         except TypeError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

226) 
Marco Ricci Support textual passphrases...

Marco Ricci authored 4 months ago

227)     @staticmethod
228)     def _get_binary_string(s: bytes | bytearray | str, /) -> bytes:
229)         """Convert the input string to a read-only, binary string.
230) 
231)         If it is a text string, then test for an unambiguous UTF-8
232)         representation, otherwise abort.  (That is, check whether the
233)         NFC and NFD forms of the string coincide.)
234) 
235)         Args:
236)             s: The string to (check and) convert.
237) 
238)         Returns:
239)             A read-only, binary copy of the string.
240) 
241)         Raises:
242)             AmbiguousByteRepresentationError:
243)                 The text string has differing NFC- and NFD-normalized
244)                 UTF-8 byte representations.
245) 
246)         """
247)         if isinstance(s, str):
248)             norm = unicodedata.normalize
249)             if norm('NFC', s) != norm('NFD', s):
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

251)             return s.encode('UTF-8')
252)         return bytes(s)
253) 
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

254)     @classmethod
255)     def create_hash(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

256)         cls,
257)         phrase: bytes | bytearray | str,
258)         service: bytes | bytearray,
259)         *,
260)         length: int = 32,
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

262)         r"""Create a pseudorandom byte stream from phrase and service.
263) 
264)         Create a pseudorandom byte stream from `phrase` and `service` by
265)         feeding them into the key-derivation function PBKDF2
266)         (8 iterations, using SHA-1).
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

267) 
268)         Args:
Marco Ricci Use neutral arguments in `V...

Marco Ricci authored 5 months ago

269)             phrase:
270)                 A master passphrase, or sometimes an SSH signature.
271)                 Used as the key for PBKDF2, the underlying cryptographic
272)                 primitive.
Marco Ricci Support textual passphrases...

Marco Ricci authored 4 months ago

273) 
274)                 If a text string, then the byte representation must be
275)                 unique.
Marco Ricci Use neutral arguments in `V...

Marco Ricci authored 5 months ago

276)             service:
277)                 A vault service name.  Will be suffixed with
278)                 `Vault._UUID`, and then used as the salt value for
279)                 PBKDF2.
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

280)             length:
281)                 The length of the byte stream to generate.
282) 
283)         Returns:
284)             A pseudorandom byte string of length `length`.
285) 
Marco Ricci Support textual passphrases...

Marco Ricci authored 4 months ago

286)         Raises:
287)             AmbiguousByteRepresentationError:
288)                 The phrase is a text string with differing NFC- and
289)                 NFD-normalized UTF-8 byte representations.
290) 
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

291)         Note:
292)             Shorter values returned from this method (with the same key
293)             and message) are prefixes of longer values returned from
294)             this method.  (This property is inherited from the
295)             underlying PBKDF2 function.)  It is thus safe (if slow) to
296)             call this method with the same input with ever-increasing
297)             target lengths.
298) 
Marco Ricci Use neutral arguments in `V...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

301)             >>> phrase = bytes.fromhex('''
302)             ... 00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39
303)             ... 00 00 00 40
304)             ... f0 98 19 80 6c 1a 97 d5 26 03 6e cc e3 65 8f 86
305)             ... 66 07 13 19 13 09 21 33 33 f9 e4 36 53 1d af fd
306)             ... 0d 08 1f ec f8 73 9b 8c 5f 55 39 16 7c 53 54 2c
307)             ... 1e 52 bb 30 ed 7f 89 e2 2f 69 51 55 d8 9e a6 02
308)             ... ''')
309)             >>> Vault.create_hash(phrase, b'some_service', length=4)
310)             b'M\xb1<S'
311)             >>> Vault.create_hash(phrase, b'some_service', length=16)
312)             b'M\xb1<S\x827E\xd1M\xaf\xf8~\xc8n\x10\xcc'
313)             >>> Vault.create_hash(phrase, b'NOSUCHSERVICE', length=16)
314)             b'\x1c\xc3\x9c\xd9\xb6\x1a\x99CS\x07\xc41\xf4\x85#s'
315) 
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

319)         salt = bytes(service) + cls._UUID
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

320)         return hashlib.pbkdf2_hmac(
321)             hash_name='sha1',
322)             password=phrase,
323)             salt=salt,
324)             iterations=8,
325)             dklen=length,
326)         )
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

327) 
328)     def generate(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

329)         self,
330)         service_name: str | bytes | bytearray,
331)         /,
332)         *,
Marco Ricci Support textual passphrases...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

336) 
337)         Args:
338)             service_name:
339)                 The service name.
340)             phrase:
341)                 If given, override the passphrase given during
342)                 construction.
343) 
Marco Ricci Support textual passphrases...

Marco Ricci authored 4 months ago

344)                 If a text string, then the byte representation must be
345)                 unique.
346) 
347)         Returns:
348)             The service passphrase.
349) 
350)         Raises:
351)             AmbiguousByteRepresentationError:
352)                 The phrase is a text string with differing NFC- and
353)                 NFD-normalized UTF-8 byte representations.
354) 
Marco Ricci Add unit tests, both new an...

Marco Ricci authored 5 months ago

355)         Examples:
356)             >>> phrase = b'She cells C shells bye the sea shoars'
357)             >>> # Using default options in constructor.
358)             >>> Vault(phrase=phrase).generate(b'google')
359)             b': 4TVH#5:aZl8LueOT\\{'
360)             >>> # Also possible:
361)             >>> Vault().generate(b'google', phrase=phrase)
362)             b': 4TVH#5:aZl8LueOT\\{'
363) 
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

367)         # Ensure the phrase is a bytes object.  Needed later for safe
368)         # concatenation.
369)         if isinstance(service_name, str):
370)             service_name = service_name.encode('utf-8')
371)         elif not isinstance(service_name, bytes):
372)             service_name = bytes(service_name)
373)         assert_type(service_name, bytes)
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

374)         if not phrase:
375)             phrase = self._phrase
Marco Ricci Support textual passphrases...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

377)         # Repeat the passphrase generation with ever-increasing hash
378)         # lengths, until the passphrase can be formed without exhausting
379)         # the sequin.  See the guarantee in the create_hash method for
380)         # why this works.
381)         while True:
382)             try:
383)                 required = self._required[:]
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

384)                 seq = sequin.Sequin(
385)                     self.create_hash(
386)                         phrase=phrase, service=service_name, length=hash_length
387)                     )
388)                 )
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

389)                 result = bytearray()
390)                 while len(result) < self._length:
391)                     pos = seq.generate(len(required))
392)                     charset = required.pop(pos)
393)                     # Determine if an unlucky choice right now might
394)                     # violate the restriction on repeated characters.
395)                     # That is, check if the current partial passphrase
396)                     # ends with r - 1 copies of the same character
397)                     # (where r is the repeat limit that must not be
398)                     # reached), and if so, remove this same character
399)                     # from the current character's allowed set.
Marco Ricci Fix repeated character dete...

Marco Ricci authored 5 months ago

400)                     if self._repeat and result:
401)                         bad_suffix = bytes(result[-1:]) * (self._repeat - 1)
402)                         if result.endswith(bad_suffix):
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

403)                             charset = self._subtract(
404)                                 bytes(result[-1:]), charset
405)                             )
Marco Ricci Fix numerous argument type...

Marco Ricci authored 5 months ago

406)                     pos = seq.generate(len(charset))
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

409)                 hash_length *= 2
410)             else:
Marco Ricci Fix numerous argument type...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

412) 
Marco Ricci Expose some functionality f...

Marco Ricci authored 4 months ago

413)     @staticmethod
414)     def _is_suitable_ssh_key(key: bytes | bytearray, /) -> bool:
415)         """Check whether the key is suitable for passphrase derivation.
416) 
417)         Currently, this only checks whether signatures with this key
418)         type are deterministic.
419) 
420)         Args:
421)             key: SSH public key to check.
422) 
423)         Returns:
424)             True if and only if the key is suitable for use in deriving
425)             a passphrase deterministically.
426) 
427)         """
428)         deterministic_signature_types = {
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

429)             'ssh-ed25519': lambda k: k.startswith(
430)                 b'\x00\x00\x00\x0bssh-ed25519'
431)             ),
432)             'ssh-ed448': lambda k: k.startswith(b'\x00\x00\x00\x09ssh-ed448'),
433)             'ssh-rsa': lambda k: k.startswith(b'\x00\x00\x00\x07ssh-rsa'),
Marco Ricci Expose some functionality f...

Marco Ricci authored 4 months ago

434)         }
435)         return any(v(key) for v in deterministic_signature_types.values())
436) 
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

437)     @classmethod
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

438)     def phrase_from_key(cls, key: bytes | bytearray, /) -> bytes:
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

439)         """Obtain the master passphrase from a configured SSH key.
440) 
441)         vault allows the usage of certain SSH keys to derive a master
442)         passphrase, by signing the vault UUID with the SSH key.  The key
443)         type must ensure that signatures are deterministic.
444) 
445)         Args:
446)             key: The (public) SSH key to use for signing.
447) 
448)         Returns:
Marco Ricci Fix passphrase-from-SSH-sig...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

451) 
452)         Raises:
453)             ValueError:
454)                 The SSH key is principally unsuitable for this use case.
455)                 Usually this means that the signature is not
456)                 deterministic.
457) 
Marco Ricci Add example for `Vault.phra...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

479)             True
480) 
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

482)         if not cls._is_suitable_ssh_key(key):
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

483)             msg = (
484)                 'unsuitable SSH key: bad key, or '
485)                 'signature not deterministic'
486)             )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

487)             raise ValueError(msg)
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

493) 
Marco Ricci Fix character set subtracti...

Marco Ricci authored 5 months ago

494)     @staticmethod
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

495)     def _subtract(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

498)     ) -> bytearray:
499)         """Remove the characters in charset from allowed.
500) 
501)         This preserves the relative order of characters in `allowed`.
502) 
503)         Args:
Marco Ricci Fix character set subtracti...

Marco Ricci authored 5 months ago

504)             charset:
505)                 Characters to remove.  Must not contain duplicate
506)                 characters.
507)             allowed:
508)                 Character set to remove the other characters from.  Must
509)                 not contain duplicate characters.
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

510) 
511)         Returns:
Marco Ricci Fix character set subtracti...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

513) 
514)         Raises:
Marco Ricci Fix character set subtracti...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

517) 
518)         """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

519)         allowed = (
520)             allowed if isinstance(allowed, bytearray) else bytearray(allowed)
521)         )
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

522)         assert_type(allowed, bytearray)
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

528)         for c in charset:
529)             try:
530)                 pos = allowed.index(c)
Marco Ricci Fix numerous argument type...

Marco Ricci authored 5 months ago

531)             except ValueError:
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

532)                 pass
533)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

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