https://stackoverflow.com/questions/77962998/how-do-i-read-store-a-password-in-a-possibly-locked-gnome-keyring-manager-from-t/77962999#77962999
I do not want gnome-keyring-daemon prompting to unlock a keyring on some remote X session, I do not want it to prompt unlocking of a keyring at all.
If there is an unlocked keyring (e.g. an X console session) then I want to use that keyring session, but if not, then I don't want to unlock an existing keyring as that could interfere with the available credentials of that X session, instead I want to run a private instance of gnome-keyring-daemon just long enough to get the secret, without interfering with any existing clients of any existing gnome-keyring-daemon (e.g. seahorse, chrome, evolution, ssh-agent -- I don't want suddenly asking for ssh keys because something called gnome-keyring-daemon --replace).
And I want to do it from the command line with without perl and python tools.
The solution has multiple steps:
- Without prompting to unlock, check if there is an unlocked keyring-manager that secret-tool can use
- If not, create an isolated dbus environment in which you run and unlock a gnome-keyring-daemon which you can then use with secret-tool.
Is a keyring already available?
There are various solutions proposed to query whether or not an unlocked keyring is available. Some of this will start gnome-keyring-daemon if it is not running.
The method I settled on is:
busctl --timeout=10 --user get-property \
org.freedesktop.secrets /org/freedesktop/secrets/collection/login \
org.freedesktop.Secret.Collection Locked
There are 4 results:
- failure to get result
- get "locked"
- get "unlocked"
- get other result
I process it like this:
is-unlocked() {
local locked
if locked=$(busctl --timeout=10 --user get-property org.freedesktop.secrets \
/org/freedesktop/secrets/collection/login org.freedesktop.Secret.Collection Locked
) && read _ locked _ <<<"$locked"
then case "${locked,,}" in
true) return 1;;
false) return 0;;
*) return 2;; # try to start one
esac
else if type -p gnome-keyring-daemon 2>/dev/null
then return 2
else return 3
fi
fi
}
If the function returns 3 then there is nothing to be done, if it returns 0 then it is usable, otherwise a private gnome-keyring-daemon is required.
Launching a private keyring
Most of what I've read in all the various answers about launching gnome-keyring-daemon, damage the relationship with existing clients, causing them to fail. Evolution can't connect any more until it is fully restarted (and that takes a bit of doing), seahorse can't change any secrets (although it has cached what it has), ssh-agent doesn't have the passphrase for your keys any more and asks you...
If we want a private unlocked gnome-keyring-daemon we have to use either --login
or --unlock
and feed the password on stdin, but many people also suggest --replace
which is misleading because without other precautions that aren't mentioned, it closes the existing daemon and ruins all the client sessions.
We need to run our private unlocked gnome-keyring-daemon in a private dbus session to avoid other interferences that will happen even if we don't use --replace
You'd think that dbus-run-session
or bus-launch --exit-with-session
would work, but you'd be wrong, they fight over the contents of XDG_RUNTIME_DIR where all the control sockets are kept, and whether or not this matters partly depends on whether you've ssh'd in or not and whether or not the keyring is locked.
Extensive experiments lead me to conclude that dbus-run-session is ideal provided that XDG_RUNTIME_DIR is also private.
Getting a password using a private gome-keyring-daemon might work something like this:
echo -n "$keyring_password" | \
XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR}/sub/$$ dbus-run-session -- \
bash --noprofile --norc -c \
'eval export $(gnome-keyring-daemon --replace --unlock ) && is-unlocked "$@"' keyring-manager "$@"
but it's not enough. You need to mkdir
and chmod
the temporary XDG_RUNTIME_DIR
(and cleanup). You also need a lot of flow control so that you can separate your stderr of the different processes.
Also you don't want to be doing: echo -n "$keyring_password"
because in bash set -x
mode you will leak the password to stderr (or ${BASH_XTRACEFD}
).
I use something like this:
with-private-keyring-manager() {
export -f is-unlocked # so it is easily available inside the private session
mkdir -p "${XDG_RUNTIME_DIR}/sub/$$" &&
chmod 700 "${XDG_RUNTIME_DIR}/sub/$$" &&
{ tr -d $'\n' <<<"$keyring_password" | \
XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR}/sub/$$ dbus-run-session -- \
bash --noprofile --norc -c \
'exec >/dev/fd/4 && eval export $(gnome-keyring-daemon --replace --unlock ) && { is-unlocked <&- || exit 253 ; } <&3 && "$@"' scx-keyring-manager "$@"
} 3<&0 2>&${BASH_XTRACEFD:-2} 4>&1 1>&2
set -- $?
rm -fr "${XDG_RUNTIME_DIR}/sub/$$"
return $1 # saved exit code
}
What I'm trying to do here is preserve the original stdin, stdout, stderr of the calling context to the passed command and args in $@
, while still feeding the password on stdin, and ensuring that stderr of dbus-run-session and stdout of anything in there not taint the process stdout.
The missing pieces
You can call get-secret
and put-secret
with the typical arguments that secret-tool
has
put-secret() {
with-keyring secret-tool store "$@"
}
get-secret() {
with-keyring secret-tool lookup "$@"
}
with-keyring() {
if is-unlocked
then "$@"
else get-keyring-password && with-private-keyring-manager "$@"
fi
}
get-keyring-password() {
read -r -s -p $"$prompt: " keyring_password && test ${#keyring_password} != 0
}
examples
If you run these in a terminal as part of the X console login, they will generally work without a password. If you lock the keyring with seahorse then you will need to provide the password. Or, if you just ssh into a machine that doesn't have a keyring running you will need to provide a password.
put-secret --label "Test secret" purpose test <<<"It's a secret"
get-secret purpose test
When running the tests, check the existence of gnome-keyring-daemon processes before and after. Typically any that were running before should continue undisturbed.
Sometimes when running the tests, if you are on the X console and you locked your keyring with seahorse, you will be spontaneously prompted to unlock it for the benefit of existing keyring clients which are nothing to do with what you are testing here
Tips on passwords in the shell
You don't really want to store passwords in the shell because you'll leak them by having them on as command line parameters of externally invoked processes, accidentally have the in environment variables which will leak to external processes and will be visible if set -x
is active (and for protection against that, use things like test ${#password} = 0
instead of test -z "${password}"
and instead of echo "$password" |
have <<<"$password"
and instead of echo -n "$password" |
have tr -d '\n' <<<"$password |
You probably would rather use dialog
to pipe the password straight into keyring manager:
The full script
To make it easy to cut-n-paste the full script is here.
#! /bin/bash
is-unlocked() {
local locked
if locked=$(busctl --timeout=10 --user get-property org.freedesktop.secrets \
/org/freedesktop/secrets/collection/login org.freedesktop.Secret.Collection Locked
) && read _ locked _ <<<"$locked"
then case "${locked,,}" in
true) return 1;;
false) return 0;;
*) return 2;; # try to start one
esac
else if type -p gnome-keyring-daemon 2>/dev/null
then return 2
else return 3
fi
fi
}
with-private-keyring-manager() {
export -f is-unlocked # so it is easily available inside the private session
mkdir -p "${XDG_RUNTIME_DIR}/sub/$$" &&
chmod 700 "${XDG_RUNTIME_DIR}/sub/$$" &&
{ tr -d $'\n' <<<"$keyring_password" | \
XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR}/sub/$$ dbus-run-session -- \
bash --noprofile --norc -c \
'exec >/dev/fd/4 && eval export $(gnome-keyring-daemon --replace --unlock ) && { is-unlocked <&- || exit 253 ; } <&3 && "$@"' scx-keyring-manager "$@"
} 3<&0 2>&${BASH_XTRACEFD:-2} 4>&1 1>&2
set -- $?
rm -fr "${XDG_RUNTIME_DIR}/sub/$$"
return $1 # saved exit code
}
put-secret() {
with-keyring secret-tool store "$@"
}
get-secret() {
with-keyring secret-tool lookup "$@"
}
with-keyring() {
if is-unlocked
then "$@"
else get-keyring-password && with-private-keyring-manager "$@"
fi
}
get-keyring-password() {
read -r -s -p $"$prompt: " keyring_password && test ${#keyring_password} != 0
}
put-secret --label "Test secret" purpose test <<<"It's a secret"
get-secret purpose test
No comments:
Post a Comment