age-plugin-yubikey
age-plugin-yubikey copied to clipboard
Decrypting with multiple identities
Hello,
Thanks for this plugin!
My scenario is:
- I am encrypting a secret with 2 recipients (returned by
age-plugin-yubikey --list-all
) - I would like it to be decryptable by either one of the identities (returned by
age-plugin-yubikey --identity
)
For this, I am using https://github.com/ryantm/agenix and I realized it does the following:
age --decrypt -i <identity-1> -i <identity-2> FILE
The problem is:
If I have the yubikey with <identity-1>
plugged in, it works, but if I have the one with <identity-2>
it does not.
Executing the above command manually brings up a popup to choose between skipping the yubikey or inserting it and trying it again, so I got this error:
age: warning: could not read value for age-plugin-yubikey: standard input is not a terminal, and /dev/tty is not available: open /dev/tty: no such device or address
age: error: yubikey plugin: Could not open YubiKey with serial <serial>
If I change the order to (still: yubikey with <identity-2>
is plugged in), it works:
age --decrypt -i <identity-2> -i <identity-1> FILE
Could this be handled to make the decryption successful if any of the provided identities are ok? Am I missing something?
The underlying issue is the interactive prompt generated when the plugin fails to find the YubiKey for the identity it is attempting to use.
i.e.
Please insert YubiKey with serial xxxxxxxx (press [1] for "YubiKey is plugged in" or [2] for "Skip this YubiKey")
I would have voiced my desire for this prompt to be made optional, with the alternative being automatically skip an indentoity if the YubiKey it is associated to is not present.
However, I think doing so would cause another issue whereby the PIN would be required for each decryption (at least for all but the first key in the identity file). I imagine the issue arises from an attempt to access a key that is not present closing the existing session on the present key (see also).
The UX issue you are encountering is a side-effect of a UX decision made in the age clients.
In the distant past, some age clients (naming no names but rage
, my client) would take the -i
flags, coalesce them, and then for plugin identities call the plugin once with all identities. Then the plugin could make decisions such as "you've given me identities for two Yubikeys, and I can see one of them is already plugged in, so let's try that one first".
However, this had a significant UX problem in that the order in which identities were tried was non-deterministic, and could not be predicted by the user. So back in 2021 we decided to make -i
flags deterministic during decryption (https://github.com/str4d/rage/issues/236). After this change, -i
flags are tried in left-to-right order, and no coalescing is performed.
What this means is, when you call:
age --decrypt -i <identity-1> -i <identity-2> FILE
what you are telling age
is "always try to decrypt with identity-1
first, and if that can't be used, only then try with identity-2
". age-plugin-yubikey
only sees one identity at a time, and because plugin invocations are ephemeral, it has no idea that it might get called a second time, so it asks the user to plug in the Yubikey for identity-1
. That is the best thing the plugin can do in that moment, and is a correct thing to do: the user expressed their preference to try identity-1
before identity-2
by way of ordering the -i
flags.
What this fundamentally boils down to is two conflicting user desires:
- 1️⃣ Deterministic handling of identity order, so users can directly express that
identity-1
sitting in their desk drawer should be tried beforeidentity-2
sitting in their safe. - 2️⃣ Dynamically adapting to the environment, so that
identity-2
gets tried because the user happened to have taken it out of the safe and plugged it into their computer, beforeidentity-1
sitting in their desk drawer.
Now, there might be UX tricks we could pull to mitigate this somewhat. But as long as age clients treat -i
flags as deterministic during decryption (preferentially handling case 1️⃣ above), there are limits on what age plugins can do here.
Now, there might be UX tricks we could pull to mitigate this somewhat.
One such UX trick would be to define an "OR" identity. That is, in addition to the current AGE-PLUGIN-YUBIKEY-
identity format, support a format that internally can store multiple Yubikey stubs, effectively re-introducing the coalescing that rage
used to have, but done explicitly by the user. It gets a bit weird in that it would need to be an alternative AGE-PLUGIN-YUBIKEY-
encoding (as age clients determine which plugin to use via the name in the identity), which would cause errors in old plugin versions (but that's probably fine, as users couldn't generate the new encoding without having updated the plugin on at least one machine), but I think that overall it could maybe be made to work.
The workflow would be something like:
- User uses
age-plugin-yubikey
to OR-concatenateidentity-1
andidentity-2
intoidentity-1-or-2
.- The new encoding would probably also include metadata about which order to try the YubiKeys depending on what combinations are plugged in vs not. Maybe also caching in the OR identity flags for "this identity requires a PIN always, vs this other identity only requires a PIN once"? The user mental model could become quite complex here, so we probably want to keep it simple.
- User calls
age --decrypt -i identity-1-or-2 -i identity-3
. -
age
callsage-plugin-yubikey
withidentity-1-or-2
. -
age-plugin-yubikey
sees the OR, looks to see if either key is plugged in, and then tries the YubiKeys in the order encoded in the identity. - If neither key is available, then
age
callsage-plugin-yubikey
withidentity-3
(and the plugin behaves like currently).
Having sketched out the above, I'm not sure about whether this added UX and implementation complexity is worth the flexibility it brings. Thoughts @FiloSottile?
I'd prefer this as well, as I have one Yubikey always plugged in my PC at home, and another one on my keychain which I use on my laptop. I'd like to use try the one on my keychain first, if that one isn't plugged in, it'd be great if it could automatically check the other Yubikey.
@Tiebe a plain reading of your use case would suggest it can be handled with the existing client and plugin UX, by expressing your preferences thusly:
# Encryption on both PC and laptop (and anywhere else) encrypts to
# both recipients, so you can decrypt on either.
$ age -r RECIPIENT_PC -r RECIPIENT_LAPTOP ...
# Decryption on PC
# No need to include IDENTITY_LAPTOP, because IDENTITY_PC is always
# plugged in and therefore always available to use.
$ age -d -i IDENTITY_PC ...
# Decryption on laptop
# No need to include IDENTITY_PC, because it is always plugged into
# your PC, and therefore never plugged into your laptop. You'll be
# prompted to plug in the keychain Yubikey if it isn't already.
$ age -d -i IDENTITY_LAPTOP ...
However, I think what you intended to say is (emphasis added):
I'd like to on my PC try the one on my keychain first, if that one isn't plugged in, it'd be great if it could automatically check the other Yubikey.
which is a somewhat unusual use case: if there's a Yubikey always plugged in, why decrypt with the keychain Yubikey first? (The answer is likely "the file is only encrypted to the keychain Yubikey", which per above can be solved by always encrypting to both, but let's assume that isn't done here.)
If you explicitly use age -i IDENTITY_LAPTOP -i IDENTITY_PC
to encode this preference, then you're telling age
to always try IDENTITY_LAPTOP
before trying IDENTITY_PC
. age
itself has no knowledge that IDENTITY_PC
is plugged it, so it does what you told it to do, and tries IDENTITY_LAPTOP
first, causing age-plugin-yubikey
to ask you for your keyring Yubikey, or tell it to skip. It's the "tell it to skip" that is the UX automation hurdle here, for which we do not have a good solution to.
Couldn't age/rage call age-plugin-yubikey --identity
first when it encounters an identity that's using the yubikey plugin and only use those identities that are available? There would be no need for an OR identity but maybe another CLI flag to toggle this behavior?