NAME

Crypt::MultiKey::InteractiveUnlock - Prompt user for passwords, authenticator PINs, etc

DESCRIPTION

Unlocking a LockMechanism involves a surprising number of special cases:

  • Checking for newly-plugged-in hardware authenticators needs to happen frequently, and might need to interupt password entry, and might need to collect a PIN from the user.

  • If one of the locks has a single tumbler that can't be unlocked, we shouldn't prompt the user for any of the other tumbers in that lock.

  • If one of the locks can be opened without interaction, we should use it immediately without probing hardware authenticators.

  • Multiple PKey objects might refer to the same private key, in which case we should only decrypt one of them and then copy the private key instead of trying to unlock the rest.

Also, this process is implemented for the console, but users might want to apply the same algorithm to a graphical interface. Rather than trying to make one massive customizable function, the unlock sequence is implemented as a class so that its behavior can be re-used and overridden as needed.

CONSTRUCTOR

new

$un= Crypt::MultiKey::InteractiveUnlock->new($target, %attributes);
$un= Crypt::MultiKey::InteractiveUnlock->new(%attributes);

Return a new InteractiveUnlock object. No action is taken until you call "run".

For Coffer and
Vault, the C<locks> are inspected to determine the sets of PKey objects to process.  If the
Coffer or Vault are not already associated with PKey objects (they may only serialize the
fingerprint) you need to specify those with the C<keys> option.


$bool= interactive_unlock($thing_to_unlock, %options);
# where $thing_to_unlock may be a Coffer, Vault, or arrayref of arrayrefs of PKey objects:

ATTRIBUTES

target

An instance of Crypt::MultiKey::LockMechanism or object with ->lock_mechanism attribute. If supplied, "goals" will be derived from the LockMechanism and a successful "run" will call unlock on the LockMechanism.

If not specified, you can supply the "goals" directly and a successful "run" just leaves you with a usable set of PKey objects.

goals

[
  [ $pkey1, $pkey2 ],
  [ $pkey1_fingerprint, $pkey3_fingerprint ],
  [ $pkey2, $pkey3, $pkey4 ],
]

This is an arrayref where each element represents a possible set of PKeys that constitute a successful "unlock". In other words, the goal of this algorithm is to find all the private keys for one of these sets, and then stop. Each goal is represented as an arrayref of PKey objects or PKey fingerprints. You may assign new goals, but do not modify the existing arrayref.

pkeys

This is a set of PKey objects which the algorithm is attempting to obtain private halves for. You can supply this to the constructor as an arrayref or hashref, but it is returned from this attribute accessor as a de-duplicated arrayref. You may add additional PKey objects with "add_pkeys" or exclude existing ones with "exclude_pkey", but modifying the arrayref has no effect.

pkeys_by_fingerprint
@pkeys= $iun->pkeys_by_fingerprint($pkey->fingerprint);

Returns a list of pkeys by their 'fingerprint' attribute.

pkeys_by_public
@pkeys= $iun->pkeys_by_public($pkey->public);

Returns a list of pkeys by their 'public' attribute.

unlocked

This holds the first successfully unlocked element of "goals" (but with each element replaced by a PKey object, if they were fingerprint strings), or undef if none have been unlocked yet. The algorithm is considered complete once this attribute is set.

input_fh

A file handle to use for reading passwords from the user. The default is to open /dev/tty on Unix or CONIN$ on MSWin32.

prompt_fh

A file handle to use for writing prompts and status messages. The default is to open /dev/tty on Unix or CONOUT$ on MSWin32.

prompt_state

The "run" method can be executed iteratively so that the loop doesn't block the rest of your script's execution. To that end, this hashref holds the state of the console if a user happens to be mid-password-entry. Subclasses could also use it to hold state for something other than a console, like window handles etc. When the user is not being prompted for a password, this hashref should be empty.

password_buffer

An instance of Crypt::SecretBuffer that receives passwords.

ssh_agent

An instance of Crypt::MultiKey::SSHAgentClient. It will be created on demand if you don't initialize it.

METHODS

add_pkey

my $added= $iun->add_pkey($PKey_obj, ...);

Add (or re-add) one or more PKey objects for consideration. If the PKey object has the private half available, it triggers a check for whether this key object can provide the solution to one of the "goals". Any time you obtain the private half of a key object, pass it to this method again to check for a solution.

Returns the number of PKey objects which were added. Check the "unlocked" attribute to see if one of them completed a goal. Note that you still need to call "run" to trigger a call to ->target->unlock(@keys) even if this method sets unlocked.

exclude_pkey

$iun->exclude_pkey($PKey_obj, ...);

Flag a PKey object as impossible to obtain the private half via this algorithm. For example, if there is no console available and the PKey requires a password, then "run" can't obtain the private half. Likewise if the PKey is encrypted with FIDO2 but libfido2 wasn't available when Crypt::MultiKey was built, then all FIDO2 keys get excluded.

This removes the PKey object from "pkeys" but not from "pkeys_by_public" or "pkeys_by_fingerprint".

run

my $bool= $iun->run(%options);

Run the interactive unlock algorithm. This begins a console/tty interactive process to call obtain_private on one or more sets of PKey objects, succeeding as soon as one of the sets is fully assembled.

The PKey objects may already have the private halves loaded, in which case some sets may already be complete, and this function returns success immediately. Otherwise, it groups the private-lacking PKey objects by mechanism:

Password

If any PKey can be decrypted by a plain password, the interactive loop will prompt for passwords and then test each remaining password-encrypted key to see if the password can decrypt it.

SSHAgentSignature

If an SSH Agent is available, it will check for any PKey that can be decrypted by a signature from any of the keys in your agent. It will re-check that list once per second, allowing you to add them to your agent on the fly.

YKChalResp

If any PKey requires the YubiKey OTP Chal/Resp protocol, it scans for attached YubiKeys of a matching serial number. If found, it starts a "ykchalresp" in a background thread/process which succeeds as soon as you touch the button on the YubiKey. It re-checks for matching devices every second.

FIDO2

If any PKey requires the FIDO2 protocol, it scans for attached FIDO2 devices of a matching aaguid. If found, it requests an assertion from the device in a background thread/process which succeeds as soon as you touch the button, unless the credentials aren't on that device in which case the device is ignored. If the assertion fails due to lack of a PIN, it prompts for the PIN and tries again. It re-scans for new devices once a second.

In addition to the password prompt, it prints status messages about the remaining number and type of PKeys it is attempting to obtain private halves for.

This function may return false if the user presses ^C or hits enter on a blank line.

VERSION

version 0.000_001

AUTHOR

Michael Conrad <mike@nrdvana.net>

COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Michael Conrad.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.