Skip to content

derivepassphrase._internals.cli_helpers

Helper functions for the derivepassphrase command-line.

Warning

Non-public module (implementation detail), provided for didactical and educational purposes only. Subject to change without notice, including removal.

LOCK_SIZE module-attribute

LOCK_SIZE = 4096

The size of the record to lock at the beginning of the file, for locking implementations that lock byte ranges instead of whole files.

While POSIX specifies that fcntl locks shall support a size of zero to denote “any conceivable file size”, the locking system available in msvcrt does not support this, and requires an explicit size.

ConfigurationMutex

ConfigurationMutex()

A mutual exclusion context manager for configuration edits.

See configuration_mutex.

lock instance-attribute

lock: Callable[[], None] = lock_func

A function to lock the mutex exclusively.

This implementation uses a file descriptor of a well-known file, which is opened before locking and closed after unlocking (and on error when locking). On Windows, we use msvcrt.locking, on other systems, we use fcntl.flock.

Note

This is a normal Python function, not a method.

Warning

You really should not have to change this. If you absolutely must, then it is your responsibility to ensure that lock and unlock are still compatible.

unlock instance-attribute

unlock: Callable[[], None] = unlock_func

A function to unlock the mutex.

This implementation uses a file descriptor of a well-known file, which is opened before locking and closed after unlocking (and on error when locking). It will fail if the file descriptor is unavailable. On Windows, we use msvcrt.locking, on other systems, we use fcntl.flock.

Note

This is a normal Python function, not a method.

Warning

You really should not have to change this. If you absolutely must, then it is your responsibility to ensure that lock and unlock are still compatible.

write_lock_fileobj instance-attribute

write_lock_fileobj: BinaryIO | None = None

The file object, if currently locked by this context manager.

write_lock_file instance-attribute

write_lock_file: Path = config_filename('write lock')

The filename to lock.

write_lock_condition instance-attribute

write_lock_condition: Condition = Condition(Lock())

The lock protecting access to the file object.

__enter__

__enter__() -> Self

Enter the context, locking the configuration file.

__exit__

__exit__(
    exc_type: type[BaseException] | None,
    exc_value: BaseException | None,
    exc_tb: TracebackType | None,
) -> Literal[False]

Exit the context, releasing the lock on the configuration file.

ORIGIN

Bases: Enum

The origin of a setting, if not from the user configuration file.

Attributes:

Name Type Description
INTERACTIVE Label

interactive input

INTERACTIVE class-attribute instance-attribute

INTERACTIVE = SETTINGS_ORIGIN_INTERACTIVE

shell_complete_path

shell_complete_path(
    ctx: Context, parameter: Parameter, value: str
) -> list[str | CompletionItem]

Request standard path completion for the path argument.

is_completable_item

is_completable_item(obj: object) -> bool

Return whether the item is completable on the command-line.

The item is completable if and only if it contains no ASCII control characters (U+0000 through U+001F, and U+007F).

shell_complete_service

shell_complete_service(
    ctx: Context, parameter: Parameter, value: str
) -> list[str | CompletionItem]

Return known vault service names as completion items.

Service names are looked up in the vault configuration file. All errors will be suppressed. Additionally, any service names deemed not completable as per is_completable_item will be silently skipped.

configuration_mutex

configuration_mutex() -> (
    AbstractContextManager[AbstractContextManager]
)

Enter a mutually exclusive context for configuration writes.

Within this context, no other cooperating instance of derivepassphrase will attempt to write to its configuration directory. We achieve this by locking a specific temporary file (whose name depends on the location of the configuration directory) for the duration of the context.

Returns:

Type Description
AbstractContextManager[AbstractContextManager]

A reusable but not reentrant context manager, ensuring mutual exclusion (while within its context) with all other derivepassphrase instances using the same configuration directory.

Upon entering the context, the context manager returns itself.

Locking specifics

The directory for the lock file is determined via get_tempdir. The lock filename is derivepassphrase-lock-<hash>.txt, where <hash> is computed as follows. First, canonicalize the path to the configuration directory with pathlib.Path.resolve. Then encode the result as per the filesystem encoding (os.fsencode), and hash it with SHA256. Finally, convert the result to standard base32 and use the first twelve characters, in lowercase, as <hash>.

We use msvcrt.locking on Windows platforms (sys.platform == "win32") and fcntl.flock on all others. All locks are exclusive locks. If the locking system requires a byte range, we lock the first LOCK_SIZE bytes. For maximum portability between locking implementations, we first open the lock file for writing, which is sometimes necessary to lock a file exclusively. Thus locking will fail if we lack permission to write to an already-existing lockfile.

get_tempdir

get_tempdir() -> Path

Return a suitable temporary directory.

We implement the same algorithm as tempfile.gettempdir, except that we default to the derivepassphrase configuration directory instead of the current directory if no other choice is suitable, and that we return pathlib.Path objects directly.

config_filename

config_filename(
    subsystem: str | None = "old settings.json",
) -> Path

Return the filename of the configuration file for the subsystem.

The (implicit default) file is currently named settings.json, located within the configuration directory as determined by the DERIVEPASSPHRASE_PATH environment variable, or by click.get_app_dir in POSIX mode. Depending on the requested subsystem, this will usually be a different file within that directory.

Parameters:

Name Type Description Default
subsystem str | None

Name of the configuration subsystem whose configuration filename to return. If not given, return the old filename from before the subcommand migration. If None, return the configuration directory instead.

'old settings.json'

Raises:

Type Description
AssertionError

An unknown subsystem was passed.

Deprecated

Since v0.2.0: The implicit default subsystem and the old configuration filename are deprecated, and will be removed in v1.0. The subsystem will be mandatory to specify.

load_config

load_config() -> VaultConfig

Load a vault(1)-compatible config from the application directory.

The filename is obtained via config_filename. This must be an unencrypted JSON file.

Returns:

Type Description
VaultConfig

The vault settings. See _types.VaultConfig for details.

Raises:

Type Description
OSError

There was an OS error accessing the file.

ValueError

The data loaded from the file is not a vault(1)-compatible config.

migrate_and_load_old_config

migrate_and_load_old_config() -> (
    tuple[VaultConfig, OSError | None]
)

Load and migrate a vault(1)-compatible config.

The (old) filename is obtained via config_filename. This must be an unencrypted JSON file. After loading, the file is migrated to the new standard filename.

Returns:

Type Description
tuple[VaultConfig, OSError | None]

The vault settings, and an optional exception encountered during migration. See _types.VaultConfig for details on the former.

Raises:

Type Description
OSError

There was an OS error accessing the old file.

ValueError

The data loaded from the file is not a vault(1)-compatible config.

save_config

save_config(config: VaultConfig) -> None

Save a vault(1)-compatible config to the application directory.

The filename is obtained via config_filename. The config will be stored as an unencrypted JSON file.

Parameters:

Name Type Description Default
config VaultConfig

vault configuration to save.

required

Raises:

Type Description
OSError

There was an OS error accessing or writing the file.

ValueError

The data cannot be stored as a vault(1)-compatible config.

load_user_config

load_user_config() -> dict[str, Any]

Load the user config from the application directory.

The filename is obtained via config_filename.

Returns:

Type Description
dict[str, Any]

The user configuration, as a nested dict.

Raises:

Type Description
OSError

There was an OS error accessing the file.

ValueError

The data loaded from the file is not a valid configuration file.

get_suitable_ssh_keys

get_suitable_ssh_keys(
    conn: SSHAgentClient | socket | None = None,
) -> Iterator[SSHKeyCommentPair]

Yield all SSH keys suitable for passphrase derivation.

Suitable SSH keys are queried from the running SSH agent (see ssh_agent.SSHAgentClient.list_keys).

Parameters:

Name Type Description Default
conn SSHAgentClient | socket | None

An optional connection hint to the SSH agent. See ssh_agent.SSHAgentClient.ensure_agent_subcontext.

None

Yields:

Type Description
SSHKeyCommentPair

Every SSH key from the SSH agent that is suitable for passphrase derivation.

Raises:

Type Description
KeyError

conn was None, and the SSH_AUTH_SOCK environment variable was not found.

NotImplementedError

conn was None, and this Python does not support socket.AF_UNIX, so the SSH agent client cannot be automatically set up.

OSError

conn was a socket or None, and there was an error setting up a socket connection to the agent.

LookupError

No keys usable for passphrase derivation are loaded into the SSH agent.

RuntimeError

There was an error communicating with the SSH agent.

SSHAgentFailedError

The agent failed to supply a list of loaded keys.

prompt_for_selection

prompt_for_selection(
    items: Sequence[str | bytes],
    heading: str = "Possible choices:",
    single_choice_prompt: str = "Confirm this choice?",
    ctx: Context | None = None,
) -> int

Prompt user for a choice among the given items.

Print the heading, if any, then present the items to the user. If there are multiple items, prompt the user for a selection, validate the choice, then return the list index of the selected item. If there is only a single item, request confirmation for that item instead, and return the correct index.

Parameters:

Name Type Description Default
items Sequence[str | bytes]

The list of items to choose from.

required
heading str

A heading for the list of items, to print immediately before. Defaults to a reasonable standard heading. If explicitly empty, print no heading.

'Possible choices:'
single_choice_prompt str

The confirmation prompt if there is only a single possible choice. Defaults to a reasonable standard prompt.

'Confirm this choice?'
ctx Context | None

An optional click context, from which output device properties and color preferences will be queried.

None

Returns:

Type Description
int

An index into the items sequence, indicating the user’s selection.

Raises:

Type Description
IndexError

The user made an invalid or empty selection, or requested an abort.

select_ssh_key

select_ssh_key(
    conn: SSHAgentClient | socket | None = None,
    /,
    *,
    ctx: Context | None = None,
) -> bytes | bytearray

Interactively select an SSH key for passphrase derivation.

Suitable SSH keys are queried from the running SSH agent (see ssh_agent.SSHAgentClient.list_keys), then the user is prompted interactively (see click.prompt) for a selection.

Parameters:

Name Type Description Default
conn SSHAgentClient | socket | None

An optional connection hint to the SSH agent. See ssh_agent.SSHAgentClient.ensure_agent_subcontext.

None
ctx Context | None

An click context, queried for output device properties and color preferences when issuing the prompt.

None

Returns:

Type Description
bytes | bytearray

The selected SSH key.

Raises:

Type Description
KeyError

conn was None, and the SSH_AUTH_SOCK environment variable was not found.

NotImplementedError

conn was None, and this Python does not support socket.AF_UNIX, so the SSH agent client cannot be automatically set up.

OSError

conn was a socket or None, and there was an error setting up a socket connection to the agent.

IndexError

The user made an invalid or empty selection, or requested an abort.

LookupError

No keys usable for passphrase derivation are loaded into the SSH agent.

RuntimeError

There was an error communicating with the SSH agent.

SSHAgentFailedError

The agent failed to supply a list of loaded keys.

prompt_for_passphrase

prompt_for_passphrase() -> str

Interactively prompt for the passphrase.

Calls click.prompt internally. Moved into a separate function mainly for testing/mocking purposes.

Returns:

Type Description
str

The user input.

toml_key

toml_key(*parts: str) -> str

Return a formatted TOML key, given its parts.

check_for_misleading_passphrase

check_for_misleading_passphrase(
    key: tuple[str, ...] | ORIGIN,
    value: Mapping[str, Any],
    *,
    main_config: dict[str, Any],
    ctx: Context | None = None
) -> None

Check for a misleading passphrase according to user configuration.

Look up the desired Unicode normalization form in the user configuration, and if the passphrase is not normalized according to this form, issue a warning to the user.

Parameters:

Name Type Description Default
key tuple[str, ...] | ORIGIN

A vault configuration key or an origin of the value/configuration section, e.g. ORIGIN.INTERACTIVE, or ("global",), or ("services", "foo").

required
value Mapping[str, Any]

The vault configuration section maybe containing a passphrase to vet.

required
main_config dict[str, Any]

The parsed main user configuration.

required
ctx Context | None

The click context. This is necessary to pass output options set on the context to the logging machinery.

None

Raises:

Type Description
AssertionError

The main user configuration is invalid.

default_error_callback

default_error_callback(
    message: Any, /, *_args: Any, **_kwargs: Any
) -> NoReturn

Calls sys.exit on its first argument, ignoring the rest.

key_to_phrase

key_to_phrase(
    key: str | Buffer,
    /,
    *,
    error_callback: Callable[
        ..., NoReturn
    ] = default_error_callback,
) -> bytes

Return the equivalent master passphrase, or abort.

This wrapper around vault.Vault.phrase_from_key emits user-facing error messages if no equivalent master passphrase can be obtained from the key, because this is the first point of contact with the SSH agent.

print_config_as_sh_script

print_config_as_sh_script(
    config: VaultConfig,
    /,
    *,
    outfile: TextIO,
    prog_name_list: Sequence[str],
) -> None

Print the given vault configuration as a sh(1) script.

This implements the --export-as=sh option of derivepassphrase vault.

Parameters:

Name Type Description Default
config VaultConfig

The configuration to serialize.

required
outfile TextIO

A file object to write the output to.

required
prog_name_list Sequence[str]

A list of (subcommand) names for the command emitting this output, e.g. ["derivepassphrase", "vault"].

required