Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/kdbxtool/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@
db.save()
"""

__version__ = "0.1.0"
__version__ = "0.1.5"

from .database import Database, DatabaseSettings
from .exceptions import (
AuthenticationError,
ChallengeResponseError,
CorruptedDataError,
CredentialError,
CryptoError,
Expand Down Expand Up @@ -62,6 +63,7 @@
create_keyfile_bytes,
parse_keyfile,
)
from .security.challenge_response import ChallengeResponseProvider
from .security.yubikey import (
YubiKeyConfig,
check_slot_configured,
Expand Down Expand Up @@ -95,7 +97,8 @@
"create_keyfile",
"create_keyfile_bytes",
"parse_keyfile",
# YubiKey support
# Challenge-response / YubiKey support
"ChallengeResponseProvider",
"YubiKeyConfig",
"check_slot_configured",
"list_yubikeys",
Expand All @@ -116,6 +119,7 @@
"InvalidKeyFileError",
"MergeError",
"MissingCredentialsError",
"ChallengeResponseError",
"DatabaseError",
"EntryNotFoundError",
"GroupNotFoundError",
Expand Down
153 changes: 63 additions & 90 deletions src/kdbxtool/database.py

Large diffs are not rendered by default.

31 changes: 22 additions & 9 deletions src/kdbxtool/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@
│ ├── InvalidPasswordError
│ ├── InvalidKeyFileError
│ ├── MissingCredentialsError
│ └── YubiKeyError
│ ├── YubiKeyNotFoundError
│ ├── YubiKeySlotError
│ ├── YubiKeyTimeoutError
│ └── YubiKeyNotAvailableError
│ └── ChallengeResponseError
│ └── YubiKeyError
│ ├── YubiKeyNotFoundError
│ ├── YubiKeySlotError
│ ├── YubiKeyTimeoutError
│ └── YubiKeyNotAvailableError
└── DatabaseError
├── EntryNotFoundError
└── GroupNotFoundError
Expand Down Expand Up @@ -197,10 +198,23 @@ def __init__(self) -> None:
super().__init__("At least one credential (password or keyfile) is required")


# --- Challenge-Response Errors ---


class ChallengeResponseError(CredentialError):
"""Error during challenge-response authentication.

Base class for challenge-response related errors. These occur during
hardware-backed authentication operations (e.g., YubiKey, smart cards).

Implementations may raise subclasses with device-specific details.
"""


# --- YubiKey Errors ---


class YubiKeyError(CredentialError):
class YubiKeyError(ChallengeResponseError):
"""Error communicating with YubiKey.

Base class for YubiKey-related errors. These occur during
Expand Down Expand Up @@ -238,10 +252,9 @@ class YubiKeyTimeoutError(YubiKeyError):
was required but not received within the timeout period.
"""

def __init__(self, timeout_seconds: float = 15.0) -> None:
self.timeout_seconds = timeout_seconds
def __init__(self) -> None:
super().__init__(
f"YubiKey operation timed out after {timeout_seconds}s. "
"YubiKey operation timed out. "
"Touch may be required - try again and press the YubiKey button."
)

Expand Down
14 changes: 10 additions & 4 deletions src/kdbxtool/security/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
- Secure memory handling (SecureBytes)
- Cryptographic operations
- Key derivation functions
- YubiKey challenge-response support
- Challenge-response authentication (YubiKey support)

All code in this module should be audited carefully.
"""

from .challenge_response import ChallengeResponseProvider
from .crypto import (
Cipher,
CipherContext,
Expand Down Expand Up @@ -37,7 +38,9 @@
from .memory import SecureBytes
from .yubikey import (
HMAC_SHA1_RESPONSE_SIZE,
YUBIKEY_AVAILABLE,
YUBIKEY_HARDWARE_AVAILABLE,
HardwareYubiKey,
MockYubiKey,
YubiKeyConfig,
check_slot_configured,
compute_challenge_response,
Expand Down Expand Up @@ -69,9 +72,12 @@
"create_keyfile",
"create_keyfile_bytes",
"parse_keyfile",
# YubiKey
# YubiKey / Challenge-Response
"HMAC_SHA1_RESPONSE_SIZE",
"YUBIKEY_AVAILABLE",
"YUBIKEY_HARDWARE_AVAILABLE",
"ChallengeResponseProvider",
"HardwareYubiKey",
"MockYubiKey",
"YubiKeyConfig",
"check_slot_configured",
"compute_challenge_response",
Expand Down
69 changes: 69 additions & 0 deletions src/kdbxtool/security/challenge_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Abstract challenge-response authentication interface.

This module provides the abstract base class for challenge-response
authentication providers. Implementations include:
- HardwareYubiKey: Physical YubiKey hardware via yubikey-manager
- MockYubiKey: Software implementation for testing

The ChallengeResponseProvider ABC allows different hardware tokens
to be used interchangeably for database authentication.

Example:
>>> from kdbxtool.security.yubikey import HardwareYubiKey
>>> provider = HardwareYubiKey(slot=2)
>>> db = Database.open("vault.kdbx", password="secret",
... challenge_response_provider=provider)
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from .memory import SecureBytes


class ChallengeResponseProvider(ABC):
"""Abstract base class for HMAC-SHA1 challenge-response providers.

This interface allows different implementations of challenge-response
authentication to be used interchangeably:
- HardwareYubiKey: Uses physical YubiKey hardware
- MockYubiKey: Software implementation for testing

Implementations must provide challenge_response() which computes an
HMAC-SHA1 response for a given challenge.

Example:
>>> # Using hardware YubiKey
>>> from kdbxtool.security.yubikey import HardwareYubiKey
>>> provider = HardwareYubiKey(slot=2)
>>> db = Database.open("vault.kdbx", password="secret",
... challenge_response_provider=provider)
>>>
>>> # Using mock for testing
>>> from kdbxtool.security.yubikey import MockYubiKey
>>> provider = MockYubiKey.with_zero_secret(slot=1)
>>> db = Database.open_bytes(data, password="test",
... challenge_response_provider=provider)
"""

@abstractmethod
def challenge_response(
self,
challenge: bytes,
) -> SecureBytes:
"""Compute HMAC-SHA1 response for the given challenge.

Args:
challenge: Challenge bytes (e.g., 32-byte KDF salt from database)

Returns:
20-byte HMAC-SHA1 response wrapped in SecureBytes

Raises:
ChallengeResponseError: If the operation fails. Implementations
may raise subclasses with device-specific details.
"""
...
7 changes: 5 additions & 2 deletions src/kdbxtool/security/kdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,18 +179,21 @@ def high_security(
cls,
salt: bytes | None = None,
variant: KdfType = KdfType.ARGON2D,
iterations: int = 10,
) -> Argon2Config:
"""Create configuration for high-security applications.

Use for sensitive data where longer unlock times are acceptable.
Provides stronger protection against brute-force attacks.

Parameters: 256 MiB memory, 10 iterations, 4 parallelism
Parameters: 256 MiB memory, 10 iterations (configurable), 4 parallelism

Args:
salt: Optional salt (32 random bytes generated if not provided)
variant: Argon2 variant (ARGON2D or ARGON2ID). Default is ARGON2D
which provides better GPU resistance for local password databases.
iterations: Number of iterations (default: 10). Higher values increase
security but also unlock time.

Returns:
Argon2Config with high security parameters
Expand All @@ -201,7 +204,7 @@ def high_security(
salt = os.urandom(32)
return cls(
memory_kib=256 * 1024, # 256 MiB
iterations=10,
iterations=iterations,
parallelism=4,
salt=salt,
variant=variant,
Expand Down
Loading