56ea02d5de542487633a1444cc55ccfb2269a498
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) 
5) """Work-alike of vault(1) – a deterministic, stateless password manager
6) 
7) """
8) 
9) from __future__ import annotations
10) 
Marco Ricci Support textual passphrases...

Marco Ricci authored 4 months ago

11) import base64
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

15) import unicodedata
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

16) import warnings
17) 
18) from typing import assert_type, reveal_type
19) 
20) import sequin
21) import ssh_agent_client
22) 
Marco Ricci Remove __about__.py files,...

Marco Ricci authored 5 months ago

23) __author__ = "Marco Ricci <m@the13thletter.info>"
24) __version__ = "0.1.0"
25) 
Marco Ricci Support textual passphrases...

Marco Ricci authored 4 months ago

26) class AmbiguousByteRepresentationError(ValueError):
27)     """The object has an ambiguous byte representation."""
28) 
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

29) class Vault:
30)     """A work-alike of James Coglan's vault.
31) 
32)     Store settings for generating (actually: deriving) passphrases for
33)     named services, with various constraints, given only a master
34)     passphrase.  Also, actually generate the passphrase.  The derivation
35)     is deterministic and non-secret; only the master passphrase need be
36)     kept secret.  The implementation is compatible with [vault][].
37) 
38)     [James Coglan explains the passphrase derivation algorithm in great
39)     detail][ALGORITHM] in his blog post on said topic: A principally
40)     infinite bit stream is obtained by running a key-derivation function
41)     on the master passphrase and the service name, then this bit stream
Marco Ricci Fix documentation link to `...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

45) 
46)     [vault]: https://getvau.lt
47)     [ALGORITHM]: https://blog.jcoglan.com/2012/07/16/designing-vaults-generator-algorithm/
48) 
49)     """
50)     _UUID = b'e87eb0f4-34cb-46b9-93ad-766c5ab063e7'
51)     """A tag used by vault in the bit stream generation."""
52)     _CHARSETS: collections.OrderedDict[str, bytes]
53)     """
54)         Known character sets from which to draw passphrase characters.
55)         Relies on a certain, fixed order for their definition and their
56)         contents.
57) 
58)     """
59)     _CHARSETS = collections.OrderedDict([
60)         ('lower', b'abcdefghijklmnopqrstuvwxyz'),
61)         ('upper', b'ABCDEFGHIJKLMNOPQRSTUVWXYZ'),
62)         ('alpha', b''),  # Placeholder.
63)         ('number', b'0123456789'),
64)         ('alphanum', b''),  # Placeholder.
65)         ('space', b' '),
66)         ('dash', b'-_'),
67)         ('symbol', b'!"#$%&\'()*+,./:;<=>?@[\\]^{|}~-_'),
68)         ('all', b''),  # Placeholder.
69)     ])
70)     _CHARSETS['alpha'] = _CHARSETS['lower'] + _CHARSETS['upper']
71)     _CHARSETS['alphanum'] = _CHARSETS['alpha'] + _CHARSETS['number']
72)     _CHARSETS['all'] = (_CHARSETS['alphanum'] + _CHARSETS['space']
73)                         + _CHARSETS['symbol'])
74) 
75)     def __init__(
Marco Ricci Support textual passphrases...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

119)         self._length = length
120)         self._repeat = repeat
121)         self._allowed = bytearray(self._CHARSETS['all'])
122)         self._required: list[bytes] = []
123)         def subtract_or_require(
124)             count: int | None, characters: bytes | bytearray
125)         ) -> None:
126)             if not isinstance(count, int):
127)                 return
128)             elif count <= 0:
129)                 self._allowed = self._subtract(characters, self._allowed)
130)             else:
131)                 for _ in range(count):
132)                     self._required.append(characters)
133)         subtract_or_require(lower, self._CHARSETS['lower'])
134)         subtract_or_require(upper, self._CHARSETS['upper'])
135)         subtract_or_require(number, self._CHARSETS['number'])
136)         subtract_or_require(space, self._CHARSETS['space'])
137)         subtract_or_require(dash, self._CHARSETS['dash'])
138)         subtract_or_require(symbol, self._CHARSETS['symbol'])
Marco Ricci Fix numerous argument type...

Marco Ricci authored 5 months ago

139)         if len(self._required) > self._length:
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

140)             raise ValueError('requested passphrase length too short')
141)         if not self._allowed:
142)             raise ValueError('no allowed characters left')
143)         for _ in range(len(self._required), self._length):
144)             self._required.append(bytes(self._allowed))
145) 
Marco Ricci Expose some functionality f...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

165) 
166)         """
167)         factors: list[int] = []
Marco Ricci Expose some functionality f...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

170)         for i, charset in enumerate(self._required):
171)             factors.append(i + 1)
172)             factors.append(len(charset))
Marco Ricci Expose some functionality f...

Marco Ricci authored 4 months ago

173)         factors.sort()
174)         return math.fsum(math.log2(f) for f in factors)
175) 
176)     def _estimate_sufficient_hash_length(
177)         self, safety_factor: float = 2.0,
178)     ) -> int:
179)         """Estimate the sufficient hash length, given the current settings.
180) 
181)         Using the entropy (via `_entropy`) and a safety factor, give an
182)         initial estimate of the length to use for `create_hash` such
183)         that using a `Sequin` with this hash will not exhaust it during
184)         passphrase generation.
185) 
186)         Args:
187)             safety_factor: The safety factor.  Must be at least 1.
188) 
189)         Returns:
190)             The estimated sufficient hash length.
191) 
192)         Warning:
193)             This is a heuristic, not an exact computation; it may
194)             underestimate the true necessary hash length.  It is
195)             intended as a starting point for searching for a sufficient
196)             hash length, usually by doubling the hash length each time
197)             it does not yet prove so.
198) 
199)         """
200)         try:
201)             safety_factor = float(safety_factor)
202)         except TypeError as e:
203)             raise TypeError(f'invalid safety factor: not a float: '
204)                             f'{safety_factor!r}') from e
205)         if not math.isfinite(safety_factor) or safety_factor < 1.0:
206)             raise ValueError(f'invalid safety factor {safety_factor!r}')
207)         # Ensure the bound is strictly positive.
208)         entropy_bound = max(1, self._entropy())
209)         return int(math.ceil(safety_factor * entropy_bound / 8))
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

210) 
Marco Ricci Support textual passphrases...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

239)     @classmethod
240)     def create_hash(
Marco Ricci Support textual passphrases...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

249) 
250)         Args:
Marco Ricci Use neutral arguments in `V...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

255) 
256)                 If a text string, then the byte representation must be
257)                 unique.
Marco Ricci Use neutral arguments in `V...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

304) 
305)     def generate(
Marco Ricci Fix numerous argument type...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

348)         if not phrase:
349)             phrase = self._phrase
Marco Ricci Support textual passphrases...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

376)                     pos = seq.generate(len(charset))
377)                     result.extend(charset[pos:pos+1])
Marco Ricci Rename SequinExhaustedExcep...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

379)                 hash_length *= 2
380)             else:
Marco Ricci Fix numerous argument type...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

382) 
Marco Ricci Expose some functionality f...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

409)     def phrase_from_key(
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

452)             True
453) 
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

455)         if not cls._is_suitable_ssh_key(key):
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

456)             raise ValueError(
457)                 'unsuitable SSH key: bad key, or signature not deterministic')
458)         with ssh_agent_client.SSHAgentClient() as client:
Marco Ricci Fix passphrase-from-SSH-sig...

Marco Ricci authored 4 months ago

459)             raw_sig = client.sign(key, cls._UUID)
460)         keytype, trailer = client.unstring_prefix(raw_sig)
461)         signature_blob = client.unstring(trailer)
462)         return bytes(base64.standard_b64encode(signature_blob))
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

463) 
Marco Ricci Fix character set subtracti...

Marco Ricci authored 5 months ago

464)     @staticmethod
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

467)     ) -> bytearray:
468)         """Remove the characters in charset from allowed.
469) 
470)         This preserves the relative order of characters in `allowed`.
471) 
472)         Args:
Marco Ricci Fix character set subtracti...

Marco Ricci authored 5 months ago

473)             charset:
474)                 Characters to remove.  Must not contain duplicate
475)                 characters.
476)             allowed:
477)                 Character set to remove the other characters from.  Must
478)                 not contain duplicate characters.
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

479) 
480)         Returns:
Marco Ricci Fix character set subtracti...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

482) 
483)         Raises:
Marco Ricci Fix character set subtracti...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

486) 
487)         """
488)         allowed = (allowed if isinstance(allowed, bytearray)
489)                    else bytearray(allowed))
490)         assert_type(allowed, bytearray)
Marco Ricci Fix character set subtracti...

Marco Ricci authored 5 months ago

491)         if len(frozenset(allowed)) != len(allowed):
492)             raise ValueError('duplicate characters in set')
Marco Ricci Add prototype implementation

Marco Ricci authored 5 months ago

493)         if len(frozenset(charset)) != len(charset):
494)             raise ValueError('duplicate characters in set')
495)         for c in charset:
496)             try:
497)                 pos = allowed.index(c)
Marco Ricci Fix numerous argument type...

Marco Ricci authored 5 months ago

498)             except ValueError: