Advanced

Fedora 45 Ate My Browser Passwords (Almost): Saving Brave's Keyring Key Before You Upgrade

Posted by bob on · Post a comment
LXer; By bob
Mail this story
Print this story

On my machines, the upgrade to Fedora 45 replaced gnome-keyring with oo7, and on the first one Brave came out with an almost empty password list. Nothing was actually lost: the old passwords were still on disk, and my real password store is KeePassXC anyway. On a second machine, an XFCE desktop, I upgraded carefully, with backups, and found where it goes wrong and how to fix it.

This article explains how Chromium-family browsers store passwords on Linux, what oo7 does differently, the steps that got every password back under XFCE, and a small script, browser-pwsafe, that saves everything you need before an upgrade.

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.

  1. 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.
  2. 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).
  3. 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

  1. 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
    
  2. Move the old login keyring out of the way (keep it until everything works):
    mv ~/.local/share/keyrings/login.keyring ~/login.keyring.old
    
  3. Give the converted keyring the name oo7 uses for default:
    mv ~/.local/share/keyrings/v1/Default_keyring.keyring ~/.local/share/keyrings/v1/login.keyring
    
  4. Start oo7 with the password again:
    /usr/libexec/oo7-daemon --login
    
  5. 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.
  6. 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:

#!/usr/bin/python3 -I
# Change the oo7 "login" keyring password (old keyring password -> login password).
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

  1. Install the dependencies. On Fedora:
    sudo dnf install libsecret gnupg2 tar
    
    On Debian or Ubuntu:
    sudo apt install libsecret-tools gnupg tar
    
  2. Save the script below as ~/.local/bin/browser-pwsafe and make it executable:
    chmod +x ~/.local/bin/browser-pwsafe
    

Before an upgrade

  1. Quit the browser completely, including any instance kept running in the background.
  2. 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.
  3. 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.
  4. Copy the archive off the machine.

After an upgrade, if the passwords are missing

  1. Quit the browser.
  2. 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
    
  3. 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>.
  4. 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
# SPDX-License-Identifier: MIT
#
# browser-pwsafe - save and restore a Chromium-family browser's saved passwords
# together with the keyring key that encrypts them.
#
# On Linux, Brave, Chrome and Chromium encrypt saved passwords with a key kept
# in the desktop keyring (the Secret Service: gnome-keyring, KWallet, oo7 ...).
# If an upgrade replaces or resets that keyring, the browser generates a new
# key and the old passwords become unreadable, although they are still on disk.
# This tool saves both halves - the "Login Data" databases and the key - into
# one GPG-encrypted archive, and puts them back.
#
# Usage:
#   browser-pwsafe save    [-b BROWSER] [-o FILE]
#   browser-pwsafe restore [-b BROWSER] [--key-only] FILE
#   browser-pwsafe info    FILE
#
#   BROWSER is brave (default), chrome or chromium.
#   --key-only  restore only the keyring key, leaving Login Data untouched
#               (the usual fix when the databases survived but the keyring did not)
#
# Quit the browser before save or restore, including any background instance.
# Requires: secret-tool (libsecret), gpg, tar.
# Extra gpg options can be passed in the GPG_OPTS environment variable,
# e.g. GPG_OPTS="--recipient you@example.org" with save to use a public key.

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
}

# The browser holds a SingletonLock symlink (host-pid) in its profile directory
# while running; a stale one is left behind after a crash.
running() {
    local l
    l=$(readlink "$DIR/SingletonLock" 2>/dev/null) || return 1
    kill -0 "${l##*-}" 2>/dev/null
}

lookup_key() {   # writes the key to $1; returns 1 if the keyring holds none
    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"

    # shellcheck disable=SC2086
    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() {   # decrypt archive $1 into $TMP/stage
    [[ -f $1 ]] || die "no such file: $1"
    mkdir -p "$TMP/stage"
    # shellcheck disable=SC2086
    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:

  1. Back up ~/.local/share/keyrings/ together with the files whose secrets it protects. One without the other can't be decrypted.
  2. Make sure your keyring's password is your login password. If it isn't, oo7 can't open it at login.
  3. 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
    
  4. Keep your real passwords in a dedicated manager such as KeePassXC, and treat the browser's copy as a convenience.

  Nav
» Read more about: Groups: Chrome, Community, Fedora, LXer

» Get new stories by e-mail: nightly or as they are published

« Return to the newswire homepage

  Related stories
» More like this

Comments

No comments yet. Be the first.

Post a comment

A free account is needed to post. Write your comment here first: your text is kept in this browser while you sign up or log in, and is waiting for you when you come back.

Your message:
|||||
10
Markdown: **bold**, *italic*, [text](https://link), - lists, > quotes, $x^2$ for math. Formatting help

Sign up and post (it is free)  or  Log in and post