Marco Ricci commited on 2025-01-29 15:07:21
Zeige 2 geänderte Dateien mit 88 Einfügungen und 27 Löschungen.
Any method that accepts `bytes` and `bytearray` now also accepts arbitrary Buffer-type classes such as `memoryview` and `array.array`. The return values (mostly `bytes`, sometimes `bytearray`) remain unchanged.
| ... | ... |
@@ -22,6 +22,8 @@ if TYPE_CHECKING: |
| 22 | 22 |
import socket |
| 23 | 23 |
from collections.abc import Callable |
| 24 | 24 |
|
| 25 |
+ from typing_extensions import Buffer |
|
| 26 |
+ |
|
| 25 | 27 |
__author__ = 'Marco Ricci <software@the13thletter.info>' |
| 26 | 28 |
|
| 27 | 29 |
|
| ... | ... |
@@ -104,7 +106,7 @@ class Vault: |
| 104 | 106 |
def __init__( # noqa: PLR0913 |
| 105 | 107 |
self, |
| 106 | 108 |
*, |
| 107 |
- phrase: bytes | bytearray | str = b'', |
|
| 109 |
+ phrase: Buffer | str = b'', |
|
| 108 | 110 |
length: int = 20, |
| 109 | 111 |
repeat: int = 0, |
| 110 | 112 |
lower: int | None = None, |
| ... | ... |
@@ -257,7 +259,7 @@ class Vault: |
| 257 | 259 |
return math.ceil(safety_factor * entropy_bound / 8) |
| 258 | 260 |
|
| 259 | 261 |
@staticmethod |
| 260 |
- def _get_binary_string(s: bytes | bytearray | str, /) -> bytes: |
|
| 262 |
+ def _get_binary_string(s: Buffer | str, /) -> bytes: |
|
| 261 | 263 |
"""Convert the input string to a read-only, binary string. |
| 262 | 264 |
|
| 263 | 265 |
If it is a text string, return the string's UTF-8 |
| ... | ... |
@@ -277,8 +279,8 @@ class Vault: |
| 277 | 279 |
@classmethod |
| 278 | 280 |
def create_hash( |
| 279 | 281 |
cls, |
| 280 |
- phrase: bytes | bytearray | str, |
|
| 281 |
- service: bytes | bytearray | str, |
|
| 282 |
+ phrase: Buffer | str, |
|
| 283 |
+ service: Buffer | str, |
|
| 282 | 284 |
*, |
| 283 | 285 |
length: int = 32, |
| 284 | 286 |
) -> bytes: |
| ... | ... |
@@ -332,7 +334,7 @@ class Vault: |
| 332 | 334 |
|
| 333 | 335 |
""" |
| 334 | 336 |
phrase = cls._get_binary_string(phrase) |
| 335 |
- assert not isinstance(phrase, str) |
|
| 337 |
+ assert isinstance(phrase, bytes) |
|
| 336 | 338 |
salt = cls._get_binary_string(service) + cls._UUID |
| 337 | 339 |
return hashlib.pbkdf2_hmac( |
| 338 | 340 |
hash_name='sha1', |
| ... | ... |
@@ -344,10 +346,10 @@ class Vault: |
| 344 | 346 |
|
| 345 | 347 |
def generate( |
| 346 | 348 |
self, |
| 347 |
- service_name: bytes | bytearray | str, |
|
| 349 |
+ service_name: Buffer | str, |
|
| 348 | 350 |
/, |
| 349 | 351 |
*, |
| 350 |
- phrase: bytes | bytearray | str = b'', |
|
| 352 |
+ phrase: Buffer | str = b'', |
|
| 351 | 353 |
) -> bytes: |
| 352 | 354 |
r"""Generate a service passphrase. |
| 353 | 355 |
|
| ... | ... |
@@ -452,7 +454,7 @@ class Vault: |
| 452 | 454 |
|
| 453 | 455 |
@staticmethod |
| 454 | 456 |
def is_suitable_ssh_key( |
| 455 |
- key: bytes | bytearray, |
|
| 457 |
+ key: Buffer, |
|
| 456 | 458 |
/, |
| 457 | 459 |
*, |
| 458 | 460 |
client: ssh_agent.SSHAgentClient | None = None, |
| ... | ... |
@@ -477,6 +479,7 @@ class Vault: |
| 477 | 479 |
restricted to the indicated SSH agent). |
| 478 | 480 |
|
| 479 | 481 |
""" |
| 482 |
+ key = bytes(key) |
|
| 480 | 483 |
TestFunc: TypeAlias = 'Callable[[bytes | bytearray], bool]' |
| 481 | 484 |
deterministic_signature_types: dict[str, TestFunc] |
| 482 | 485 |
deterministic_signature_types = {
|
| ... | ... |
@@ -515,7 +518,7 @@ class Vault: |
| 515 | 518 |
@classmethod |
| 516 | 519 |
def phrase_from_key( |
| 517 | 520 |
cls, |
| 518 |
- key: bytes | bytearray, |
|
| 521 |
+ key: Buffer, |
|
| 519 | 522 |
/, |
| 520 | 523 |
*, |
| 521 | 524 |
conn: ssh_agent.SSHAgentClient | socket.socket | None = None, |
| ... | ... |
@@ -593,8 +596,8 @@ class Vault: |
| 593 | 596 |
@classmethod |
| 594 | 597 |
def phrases_are_interchangable( |
| 595 | 598 |
cls, |
| 596 |
- phrase1: bytes | bytearray, |
|
| 597 |
- phrase2: bytes | bytearray, |
|
| 599 |
+ phrase1: Buffer, |
|
| 600 |
+ phrase2: Buffer, |
|
| 598 | 601 |
/, |
| 599 | 602 |
) -> bool: |
| 600 | 603 |
"""Return true if the passphrases are interchangable to Vault. |
| ... | ... |
@@ -640,7 +643,7 @@ class Vault: |
| 640 | 643 |
@classmethod |
| 641 | 644 |
def _phrase_to_hmac_key( |
| 642 | 645 |
cls, |
| 643 |
- phrase: bytes | bytearray | str, |
|
| 646 |
+ phrase: Buffer | str, |
|
| 644 | 647 |
/, |
| 645 | 648 |
) -> bytes: |
| 646 | 649 |
r"""Return the HMAC key belonging to a passphrase. |
| ... | ... |
@@ -669,8 +672,8 @@ class Vault: |
| 669 | 672 |
|
| 670 | 673 |
@staticmethod |
| 671 | 674 |
def _subtract( |
| 672 |
- charset: bytes | bytearray, |
|
| 673 |
- allowed: bytes | bytearray, |
|
| 675 |
+ charset: Buffer, |
|
| 676 |
+ allowed: Buffer, |
|
| 674 | 677 |
) -> bytearray: |
| 675 | 678 |
"""Remove the characters in charset from allowed. |
| 676 | 679 |
|
| ... | ... |
@@ -696,6 +699,8 @@ class Vault: |
| 696 | 699 |
allowed if isinstance(allowed, bytearray) else bytearray(allowed) |
| 697 | 700 |
) |
| 698 | 701 |
assert_type(allowed, bytearray) |
| 702 |
+ charset = memoryview(charset).toreadonly().cast('c')
|
|
| 703 |
+ assert_type(charset, 'memoryview[bytes]') |
|
| 699 | 704 |
msg_dup_characters = 'duplicate characters in set' |
| 700 | 705 |
if len(frozenset(allowed)) != len(allowed): |
| 701 | 706 |
raise ValueError(msg_dup_characters) |
| ... | ... |
@@ -6,6 +6,7 @@ |
| 6 | 6 |
|
| 7 | 7 |
from __future__ import annotations |
| 8 | 8 |
|
| 9 |
+import array |
|
| 9 | 10 |
import hashlib |
| 10 | 11 |
import math |
| 11 | 12 |
from typing import TYPE_CHECKING |
| ... | ... |
@@ -19,15 +20,17 @@ import tests |
| 19 | 20 |
from derivepassphrase import vault |
| 20 | 21 |
|
| 21 | 22 |
if TYPE_CHECKING: |
| 22 |
- from collections.abc import Iterator |
|
| 23 |
+ from collections.abc import Callable, Iterator |
|
| 24 |
+ |
|
| 25 |
+ from typing_extensions import Buffer |
|
| 23 | 26 |
|
| 24 | 27 |
BLOCK_SIZE = hashlib.sha1().block_size |
| 25 | 28 |
DIGEST_SIZE = hashlib.sha1().digest_size |
| 26 | 29 |
|
| 27 | 30 |
|
| 28 | 31 |
def phrases_are_interchangable( |
| 29 |
- phrase1: bytes | bytearray | str, |
|
| 30 |
- phrase2: bytes | bytearray | str, |
|
| 32 |
+ phrase1: Buffer | str, |
|
| 33 |
+ phrase2: Buffer | str, |
|
| 31 | 34 |
/, |
| 32 | 35 |
) -> bool: |
| 33 | 36 |
"""Work-alike of [`vault.Vault.phrases_are_interchangable`][]. |
| ... | ... |
@@ -358,6 +361,12 @@ class TestVault: |
| 358 | 361 |
b'google' |
| 359 | 362 |
) == vault.Vault(phrase=self.phrase).generate(bytearray(b'google')) |
| 360 | 363 |
|
| 364 |
+ def test_202c_reproducibility_and_buffer_like_service_name(self) -> None: |
|
| 365 |
+ """Deriving a passphrase works equally for memory views.""" |
|
| 366 |
+ assert vault.Vault(phrase=self.phrase).generate( |
|
| 367 |
+ b'google' |
|
| 368 |
+ ) == vault.Vault(phrase=self.phrase).generate(memoryview(b'google')) |
|
| 369 |
+ |
|
| 361 | 370 |
@hypothesis.given( |
| 362 | 371 |
phrase=strategies.text( |
| 363 | 372 |
strategies.characters(min_codepoint=32, max_codepoint=126), |
| ... | ... |
@@ -370,18 +379,65 @@ class TestVault: |
| 370 | 379 |
max_size=32, |
| 371 | 380 |
), |
| 372 | 381 |
) |
| 373 |
- def test_202c_reproducibility_and_binary_service_name( |
|
| 382 |
+ def test_203a_reproducibility_and_binary_phrases( |
|
| 374 | 383 |
self, |
| 375 | 384 |
phrase: str, |
| 376 | 385 |
service: str, |
| 377 | 386 |
) -> None: |
| 378 |
- """Deriving a passphrase works equally for byte arrays/strings.""" |
|
| 379 |
- assert vault.Vault(phrase=phrase).generate(service) == vault.Vault( |
|
| 380 |
- phrase=phrase |
|
| 381 |
- ).generate(service.encode('utf-8'))
|
|
| 382 |
- assert vault.Vault(phrase=phrase).generate(service) == vault.Vault( |
|
| 383 |
- phrase=phrase |
|
| 384 |
- ).generate(bytearray(service.encode('utf-8')))
|
|
| 387 |
+ """Binary and text master passphrases generate the same passphrases.""" |
|
| 388 |
+ buffer_types: dict[str, Callable[..., Buffer]] = {
|
|
| 389 |
+ 'bytes': bytes, |
|
| 390 |
+ 'bytearray': bytearray, |
|
| 391 |
+ 'memoryview': memoryview, |
|
| 392 |
+ 'array.array': lambda data: array.array('B', data),
|
|
| 393 |
+ } |
|
| 394 |
+ for type_name, buffer_type in buffer_types.items(): |
|
| 395 |
+ str_phrase = phrase |
|
| 396 |
+ bytes_phrase = phrase.encode('utf-8')
|
|
| 397 |
+ assert vault.Vault(phrase=str_phrase).generate( |
|
| 398 |
+ service |
|
| 399 |
+ ) == vault.Vault(phrase=buffer_type(bytes_phrase)).generate( |
|
| 400 |
+ service |
|
| 401 |
+ ), ( |
|
| 402 |
+ f'{str_phrase!r} and {type_name}({bytes_phrase!r}) '
|
|
| 403 |
+ 'master passphrases generate different passphrases' |
|
| 404 |
+ ) |
|
| 405 |
+ |
|
| 406 |
+ @hypothesis.given( |
|
| 407 |
+ phrase=strategies.text( |
|
| 408 |
+ strategies.characters(min_codepoint=32, max_codepoint=126), |
|
| 409 |
+ min_size=1, |
|
| 410 |
+ max_size=32, |
|
| 411 |
+ ), |
|
| 412 |
+ service=strategies.text( |
|
| 413 |
+ strategies.characters(min_codepoint=32, max_codepoint=126), |
|
| 414 |
+ min_size=1, |
|
| 415 |
+ max_size=32, |
|
| 416 |
+ ), |
|
| 417 |
+ ) |
|
| 418 |
+ def test_203b_reproducibility_and_binary_service_name( |
|
| 419 |
+ self, |
|
| 420 |
+ phrase: str, |
|
| 421 |
+ service: str, |
|
| 422 |
+ ) -> None: |
|
| 423 |
+ """Binary and text service names generate the same passphrases.""" |
|
| 424 |
+ buffer_types: dict[str, Callable[..., Buffer]] = {
|
|
| 425 |
+ 'bytes': bytes, |
|
| 426 |
+ 'bytearray': bytearray, |
|
| 427 |
+ 'memoryview': memoryview, |
|
| 428 |
+ 'array.array': lambda data: array.array('B', data),
|
|
| 429 |
+ } |
|
| 430 |
+ for type_name, buffer_type in buffer_types.items(): |
|
| 431 |
+ str_service = service |
|
| 432 |
+ bytes_service = service.encode('utf-8')
|
|
| 433 |
+ assert vault.Vault(phrase=phrase).generate( |
|
| 434 |
+ str_service |
|
| 435 |
+ ) == vault.Vault(phrase=phrase).generate( |
|
| 436 |
+ buffer_type(bytes_service) |
|
| 437 |
+ ), ( |
|
| 438 |
+ f'{str_service!r} and {type_name}({bytes_service!r}) '
|
|
| 439 |
+ 'service name generate different passphrases' |
|
| 440 |
+ ) |
|
| 385 | 441 |
|
| 386 | 442 |
@hypothesis.given( |
| 387 | 443 |
phrase=strategies.text( |
| ... | ... |
@@ -396,7 +452,7 @@ class TestVault: |
| 396 | 452 |
unique=True, |
| 397 | 453 |
), |
| 398 | 454 |
) |
| 399 |
- def test_203a_service_name_dependence( |
|
| 455 |
+ def test_204a_service_name_dependence( |
|
| 400 | 456 |
self, |
| 401 | 457 |
phrase: str, |
| 402 | 458 |
services: list[bytes], |
| ... | ... |
@@ -421,7 +477,7 @@ class TestVault: |
| 421 | 477 |
unique=True, |
| 422 | 478 |
), |
| 423 | 479 |
) |
| 424 |
- def test_203b_service_name_dependence_with_config( |
|
| 480 |
+ def test_204b_service_name_dependence_with_config( |
|
| 425 | 481 |
self, |
| 426 | 482 |
phrase: str, |
| 427 | 483 |
config: dict[str, int], |
| 428 | 484 |