The first machine
After the upgrade, Brave opened normally, but brave://password-manager/passwords showed only the handful of logins I had saved that morning. The profile directory was unchanged, and the database still held all 258 saved logins:
$ sqlite3 /tmp/copy-of-login-data 'select count(*) from logins;'
258
The database was fine; the keyring was not. Before the upgrade, my XFCE session ran gnome-keyring. After the upgrade, nothing started gnome-keyring, and oo7-daemon owned the Secret Service D-Bus name instead:
$ busctl --user list | grep -i secrets
org.freedesktop.secrets 33646 oo7-daemon robert :1.97 user@1000.service
The old keyring file, ~/.local/share/keyrings/Default_keyring.keyring, was still there, but oo7 started its own store in ~/.local/share/keyrings/v1/. When Brave first launched, it asked the Secret Service for its key, got nothing back, generated a new one, and from then on could not decrypt anything encrypted with the old key.
How Chromium-family browsers store passwords on Linux
Brave, Chrome and Chromium share the same code here.
- Saved logins live in an SQLite database,
Login Data, one per profile, for example ~/.config/BraveSoftware/Brave-Browser/Default/Login Data. The password_value column holds the encrypted password.
- Each encrypted value starts with a three-byte version prefix:
v10: encrypted with a fixed, built-in key derived from the string peanuts. This is used when no keyring is available, or when the browser runs with --password-store=basic.
v11: encrypted with a random secret stored in the desktop keyring under the label Brave Safe Storage (or Chrome Safe Storage, Chromium Safe Storage).
- The AES key is derived from that secret with a single round of PBKDF2:
K = \mathrm{PBKDF2\text{-}HMAC\text{-}SHA1}\left(\text{secret},\ \texttt{saltysalt},\ 1,\ 16\ \text{bytes}\right)
and each password is encrypted with AES-128-CBC using an IV of 16 space characters.
4. The browser finds its secret by asking the Secret Service for an item with two attributes: xdg:schema = chrome_libsecret_os_crypt_password_v2 and application = brave. An item that has the right label and application attribute but no xdg:schema attribute is ignored. This detail cost me an hour: a key stored by hand with a plain secret-tool store call never gets used. Current versions also look only in the keyring behind the Secret Service alias default, which matters with oo7 (see the second machine below).
The practical consequence: the database and the keyring secret are two halves of one thing. Back up either one alone and you can't decrypt anything.
Getting the old key back
The old gnome-keyring file is encrypted with the keyring password (normally your login password), and its format is documented in the gnome-keyring sources. With the password, the old Brave Safe Storage secret can be read straight out of Default_keyring.keyring with no daemon running. Putting it back where Brave looks for it is then one command:
secret-tool clear application brave
printf '%s' "$OLD_SECRET" | secret-tool store --label='Brave Safe Storage' \
xdg:schema chrome_libsecret_os_crypt_password_v2 application brave
That brought back 133 of my 243 v11 entries. The rest were encrypted under an even older key that I have not yet located. KeePassXC holds all of them, so I lost nothing that mattered. It would have been far simpler to have saved the key beforehand.
The second machine: oo7 under XFCE, step by step
The second machine runs XFCE with the lightdm login manager. Before upgrading it I saved Brave's and Chromium's passwords with browser-pwsafe (below), copied ~/.local/share/keyrings/, counted how many saved passwords each browser profile could decrypt, and kept both browsers closed after the upgrade until the keyring was sorted out. The versions involved: oo7 0.7.0 (a beta), Brave 1.98.63, Chromium 156.
What the upgrade changes
The upgrade removed gnome-keyring and gnome-keyring-pam and installed oo7-daemon, oo7-portal and pam_oo7. lightdm's PAM file, /usr/lib/pam.d/lightdm, now has:
-auth optional pam_oo7.so
-session optional pam_oo7.so auto_start
The idea: at login, pam_oo7 starts the daemon and hands it your login password, so the keyring opens without a prompt, just as gnome-keyring's PAM module did.
Problem 1: after the first login, no keyring at all
$ busctl --user list | grep org.freedesktop.secrets
$
Nothing answered. pam_oo7 started its helper, oo7-daemon-login, which exited without a word. The user service oo7-daemon.service is installed but not enabled, and there is no D-Bus activation file for org.freedesktop.secrets, so nothing starts the daemon when a program asks for it. With the service enabled, the same helper works (see below):
systemctl --user enable oo7-daemon
Problem 2: started by hand, the old keyrings stay locked
systemctl --user start oo7-daemon starts the daemon without your password. oo7 then cannot convert the old gnome-keyring files to its own format ("Incorrect secret") and lists them as locked. To unlock them it needs a program that asks you for the password, and outside GNOME there is none. The log says:
CLI prompter failed: NameHasNoOwner
Started with the password, it works. The --login option reads the password from standard input:
systemctl --user stop oo7-daemon
/usr/libexec/oo7-daemon --login
The daemon stays in the foreground; type the keyring password and press Enter. It converted ~/.local/share/keyrings/Default_keyring.keyring into ~/.local/share/keyrings/v1/Default_keyring.keyring, left the old file in place with a .migrated marker next to it, and every browser key was there.
Problem 3: Brave still sees nothing
Current Brave and Chromium versions don't search every keyring. They ask the Secret Service for the keyring behind the alias default and look only there. gnome-keyring kept that alias in a small file, ~/.local/share/keyrings/default, which on this machine named Default_keyring. oo7 ignores that file and always maps default to the keyring called login; asking it to change the alias does nothing. This machine also had an older login.keyring, which oo7 could not open, so default pointed at a locked keyring. Brave asked oo7 to unlock it, oo7 had no way to ask me for a password, and Brave gave up:
freedesktop_secret_key_provider.cc:116] Prompt call returned no response.
Starting Brave with --password-store=gnome-libsecret made no difference: no saved passwords. Had that login keyring been open but empty, Brave would most likely have created a new key in it, and this machine would have ended up exactly like the first one.
The fix
- Quit Brave and Chromium, then stop oo7 (press Ctrl+C if it still runs in a terminal from the step above, otherwise):
systemctl --user stop oo7-daemon
- Move the old login keyring out of the way (keep it until everything works):
mv ~/.local/share/keyrings/login.keyring ~/login.keyring.old
- Give the converted keyring the name oo7 uses for
default:
mv ~/.local/share/keyrings/v1/Default_keyring.keyring ~/.local/share/keyrings/v1/login.keyring
- Start oo7 with the password again:
/usr/libexec/oo7-daemon --login
- In another terminal, check that
default is now the login keyring, that it is unlocked, and that it holds exactly one key for the browser:
busctl --user call org.freedesktop.secrets /org/freedesktop/secrets \
org.freedesktop.Secret.Service ReadAlias s default
busctl --user get-property org.freedesktop.secrets \
/org/freedesktop/secrets/collection/login org.freedesktop.Secret.Collection Locked
secret-tool search --all application brave
The answers should be o "/org/freedesktop/secrets/collection/login", b false, and one item labelled Brave Safe Storage.
- Start Brave. All its passwords were back, with the original key.
Problem 4: the keyring password is not the login password
To make this survive a reboot, I enabled the service (Problem 1) and logged in again. oo7 started and pam_oo7 handed it my login password ("Received login secret from helper"), but:
Failed to unlock keyring 'login' with provided secret: Incorrect secret
On this machine the keyring had its own password, not my login password. gnome-keyring under XFCE put up with that; oo7 tells you nothing and leaves the keyring locked, which for Brave is Problem 3 all over again. The cure is to give the keyring the login password. I did it through a D-Bus call that oo7 implements for compatibility with gnome-keyring, ChangeWithMasterPassword, on an interface gnome-keyring named, honestly, org.gnome.keyring.InternalUnsupportedGuiltRiddenInterface. With oo7 running and the keyring unlocked (steps 4 and 5 above), this script asks for both passwords and changes it. It needs python3-dbus:
import getpass, sys, dbus
bus = dbus.SessionBus()
svc = bus.get_object("org.freedesktop.secrets", "/org/freedesktop/secrets")
coll = dbus.ObjectPath("/org/freedesktop/secrets/collection/login")
_, session = svc.OpenSession("plain", dbus.String("", variant_level=1),
dbus_interface="org.freedesktop.Secret.Service")
old = getpass.getpass("Old keyring password: ")
new = getpass.getpass("Login password: ")
if getpass.getpass("Login password again: ") != new or not new:
sys.exit("Passwords differ or empty; nothing changed.")
def s(pw):
return dbus.Struct((session, dbus.ByteArray(b""), dbus.ByteArray(pw.encode()), "text/plain"))
svc.ChangeWithMasterPassword(coll, s(old), s(new),
dbus_interface="org.gnome.keyring.InternalUnsupportedGuiltRiddenInterface")
props = bus.get_object("org.freedesktop.secrets", coll)
print("Changed. login locked =", bool(props.Get("org.freedesktop.Secret.Collection", "Locked",
dbus_interface="org.freedesktop.DBus.Properties")))
Copy ~/.local/share/keyrings/v1/login.keyring somewhere safe before running it. After the next reboot, oo7 unlocked the keyring at login by itself:
INFO oo7_daemon::service: Unlocked keyring 'login' from ".../keyrings/v1/login.keyring"
Brave and Chromium then showed every password, and each profile could decrypt exactly as many saved passwords as before the upgrade. Brave did not create a second key. One warning remains in oo7's log:
Failed to lock memory pages (secrets may be swapped to disk)
The daemon may not lock its memory, so unlocked secrets could end up in swap. Fedora's default swap is zram, which stays in memory; if you added a swap partition or file, encrypt it.
The fix for next time: browser-pwsafe
browser-pwsafe is a short Bash script that saves each profile's Login Data together with the keyring secret into one GPG-encrypted archive, and puts them back. It never decrypts a password itself. It works with Brave, Chrome and Chromium, and with any Secret Service backend: gnome-keyring, KWallet or oo7.
Install
- Install the dependencies. On Fedora:
sudo dnf install libsecret gnupg2 tar
On Debian or Ubuntu:
sudo apt install libsecret-tools gnupg tar
- Save the script below as
~/.local/bin/browser-pwsafe and make it executable:
chmod +x ~/.local/bin/browser-pwsafe
Before an upgrade
- Quit the browser completely, including any instance kept running in the background.
- Save:
browser-pwsafe save
You are prompted for a passphrase. The archive is written to the current directory as brave-passwords-<timestamp>.tar.gpg. Use -b chrome or -b chromium for the other browsers, and -o FILE to choose the file name.
- Confirm the archive contains the key:
browser-pwsafe info brave-passwords-*.tar.gpg
The output must show key=yes. If it shows key=no, the browser is either using --password-store=basic (then the v10 passwords need no keyring key) or the keyring is not running.
- Copy the archive off the machine.
After an upgrade, if the passwords are missing
- Quit the browser.
- If the profile directory survived the upgrade, which is the usual case, restore only the key:
browser-pwsafe restore --key-only brave-passwords-20261103-090000.tar.gpg
- If the profile itself was lost or replaced, restore the databases and the key:
browser-pwsafe restore brave-passwords-20261103-090000.tar.gpg
The current databases are kept as Login Data.pre-restore-<timestamp>.
- Start the browser and open
brave://password-manager/passwords.
Under oo7 the restored key goes into the keyring behind default, so that keyring must be unlocked first (see the second machine above).
Passwords saved after the upgrade but before the restore were encrypted with the new key, and become unreadable once the old key is back. Restore before saving anything new.
Encrypting to a public key instead of a passphrase
Pass gpg options in GPG_OPTS:
GPG_OPTS="--recipient you@example.org" browser-pwsafe save
The script
#!/usr/bin/env bash
set -euo pipefail
umask 077
PROG=${0##*/}
SCHEMA=chrome_libsecret_os_crypt_password_v2
GPG_OPTS=${GPG_OPTS:-}
die() { echo "$PROG: $*" >&2; exit 1; }
info() { echo "$PROG: $*" >&2; }
need() { command -v "$1" >/dev/null 2>&1 || die "'$1' not found (install $2)"; }
usage() {
sed -n '/^# Usage:/,/^# Requires:/p' "$0" | sed 's/^# \{0,1\}//' >&2
exit "${1:-1}"
}
set_browser() {
case $1 in
brave) APP=brave; LABEL="Brave Safe Storage"; DIR=$HOME/.config/BraveSoftware/Brave-Browser ;;
chrome) APP=chrome; LABEL="Chrome Safe Storage"; DIR=$HOME/.config/google-chrome ;;
chromium) APP=chromium; LABEL="Chromium Safe Storage"; DIR=$HOME/.config/chromium ;;
*) die "unknown browser '$1' (use brave, chrome or chromium)" ;;
esac
BROWSER=$1
}
running() {
local l
l=$(readlink "$DIR/SingletonLock" 2>/dev/null) || return 1
kill -0 "${l##*-}" 2>/dev/null
}
lookup_key() {
secret-tool lookup xdg:schema "$SCHEMA" application "$APP" >"$1" 2>/dev/null && [[ -s $1 ]] && return 0
secret-tool lookup application "$APP" >"$1" 2>/dev/null && [[ -s $1 ]] && return 0
: >"$1"
return 1
}
cleanup() {
[[ -n ${TMP:-} && -d $TMP ]] || return 0
find "$TMP" -type f -exec shred -u {} + 2>/dev/null || true
rm -rf "$TMP"
}
cmd_save() {
local out=$1 stage n=0 f rel
[[ -d $DIR ]] || die "no profile directory $DIR"
running && die "$BROWSER is running; quit it first (check for a background instance)"
[[ -n $out ]] || out=$PWD/$BROWSER-passwords-$(date +%Y%m%d-%H%M%S).tar.gpg
[[ -e $out ]] && die "$out already exists"
stage=$TMP/stage
mkdir -p "$stage/profiles"
while IFS= read -r -d '' f; do
rel=${f#"$DIR"/}
mkdir -p "$stage/profiles/$(dirname "$rel")"
cp -p "$f" "$stage/profiles/$rel"
n=$((n + 1))
done < <(find "$DIR" -maxdepth 2 -type f \( -name 'Login Data' -o -name 'Login Data For Account' \) -print0)
(( n )) || die "no 'Login Data' files under $DIR"
local havekey=yes
lookup_key "$stage/key" || havekey=no
{
echo "browser=$BROWSER"
echo "created=$(date -Is)"
echo "host=$(hostname)"
echo "os=$( (. /etc/os-release && echo "$PRETTY_NAME") 2>/dev/null || uname -sr)"
echo "key=$havekey"
echo "files=$n"
} >"$stage/meta"
if [[ $GPG_OPTS == *--recipient* || $GPG_OPTS == *-r\ * ]]; then
tar -C "$stage" -cf - . | gpg --encrypt $GPG_OPTS -o "$out"
else
tar -C "$stage" -cf - . | gpg --symmetric --cipher-algo AES256 $GPG_OPTS -o "$out"
fi
info "saved $n database(s) to $out"
if [[ $havekey == yes ]]; then
info "keyring key '$LABEL' included"
else
info "WARNING: no '$LABEL' key found in the keyring."
info " If the browser runs with --password-store=basic this is expected;"
info " otherwise the saved passwords cannot be decrypted after a keyring reset."
fi
}
unpack() {
[[ -f $1 ]] || die "no such file: $1"
mkdir -p "$TMP/stage"
gpg --quiet --decrypt $GPG_OPTS "$1" | tar -C "$TMP/stage" -xf -
[[ -f $TMP/stage/meta ]] || die "$1 is not a $PROG archive"
}
cmd_info() {
unpack "$1"
cat "$TMP/stage/meta"
(cd "$TMP/stage/profiles" && find . -type f | sed 's|^\./| |' | sort)
}
cmd_restore() {
local file=$1 key_only=$2 stage saved stamp f rel dest n=0
unpack "$file"
stage=$TMP/stage
saved=$(sed -n 's/^browser=//p' "$stage/meta")
[[ $saved == "$BROWSER" ]] || die "archive is for '$saved', not '$BROWSER' (use -b $saved)"
running && die "$BROWSER is running; quit it first (check for a background instance)"
if [[ $key_only == no ]]; then
stamp=$(date +%Y%m%d-%H%M%S)
while IFS= read -r -d '' f; do
rel=${f#"$stage/profiles"/}
dest=$DIR/$rel
mkdir -p "$(dirname "$dest")"
[[ -e $dest ]] && mv "$dest" "$dest.pre-restore-$stamp"
cp -p "$f" "$dest"
n=$((n + 1))
done < <(find "$stage/profiles" -type f -print0)
info "restored $n database(s) into $DIR (previous copies kept as *.pre-restore-$stamp)"
fi
if [[ -s $stage/key ]]; then
secret-tool clear application "$APP" 2>/dev/null || true
secret-tool store --label="$LABEL" xdg:schema "$SCHEMA" application "$APP" <"$stage/key"
info "keyring key '$LABEL' restored"
else
info "archive holds no keyring key (browser used --password-store=basic); keyring left unchanged"
fi
info "start $BROWSER and check its password manager"
}
main() {
local cmd=${1:-} out="" key_only=no file=""
[[ -n $cmd ]] || usage
shift
set_browser brave
while (( $# )); do
case $1 in
-b|--browser) [[ $# -ge 2 ]] || usage; set_browser "$2"; shift 2 ;;
-o|--output) [[ $# -ge 2 ]] || usage; out=$2; shift 2 ;;
--key-only) key_only=yes; shift ;;
-h|--help) usage 0 ;;
-*) die "unknown option $1" ;;
*) [[ -z $file ]] || usage; file=$1; shift ;;
esac
done
need gpg gnupg2
need tar tar
TMP=$(mktemp -d "${XDG_RUNTIME_DIR:-/tmp}/$PROG.XXXXXX")
trap cleanup EXIT
case $cmd in
save) [[ -z $file ]] || usage; need secret-tool libsecret; cmd_save "$out" ;;
restore) [[ -n $file ]] || usage; need secret-tool libsecret; cmd_restore "$file" "$key_only" ;;
info) [[ -n $file ]] || usage; cmd_info "$file" ;;
-h|--help|help) usage 0 ;;
*) usage ;;
esac
}
main "$@"
A broader lesson
A distribution upgrade can change which program answers on org.freedesktop.secrets without asking you, and anything that keeps its secrets there (browsers, mail clients, Nextcloud, network passwords) depends on that answer. Before a major upgrade, add these to your checklist:
- Back up
~/.local/share/keyrings/ together with the files whose secrets it protects. One without the other can't be decrypted.
- Make sure your keyring's password is your login password. If it isn't, oo7 can't open it at login.
- After the upgrade, start nothing that stores passwords until you have checked which daemon owns the Secret Service, and that
default is an unlocked keyring holding your old items:
busctl --user list | grep org.freedesktop.secrets
secret-tool search --all application brave
- Keep your real passwords in a dedicated manager such as KeePassXC, and treat the browser's copy as a convenience.