3

I use Gnome Keyring as the password storage backend for various applications. Many of the entries are for different ways to log into the same account, accessed through different means. I've changed my password on this account, now I want to update all the entries in my keyring.

I usually use Seahorse to edit the keyring, but it only lets me edit individual entries with a slightly awkward sequence of key presses or mouse clicks. With many passwords to change, that's tedious.

How can I update my password for many entries in Gnome Keyring efficiently, i.e. without having to type it again and again?

2 Answers 2

2

Following meuh's advice I wrote a Python script to change the passwords for all the entries matching certain conditions (e.g. a certain username and server name). The script requests the old password as a sanity check and only changes entries with the specified old password. Sample usage:

keyring-change-passwords 'user|username_value=^gilles$' 'action_url|server=acme\.example\.com'

Warning: the code has satisfactorily run once. That's the extent of my testing.

Note: the API has changed over time. The code below worked on Ubuntu 20.04. An older version of this answer has code that worked on Ubuntu 14.04.

#!/usr/bin/env python3

"""Change multiple entries in the Gnome Keyring login keyring.

Prompt for the old and new password. Only entries for which the old password
matches are modified.

Condition syntax:
  ATTRIBUTE[,ATTRIBUTE...]=REGEX
e.g.
  bar,baz=^foo
Only match if the "bar" attribute starts with "foo". If there's no "bar"
attribute, use "baz" instead.
"""

import argparse
import getpass
import os
import re
import sys
import time

import keyring

def print_info():
    cfg = keyring.util.platform_.config_root() + '/keyringrc.cfg'
    print("Using keyring configuration file:", cfg)
    if os.path.exists(cfg):
        print(re.sub(r'^', r'  ', re.M), open(cfg).read())
    print("Any data files are in:", keyring.util.platform_.data_root())
    kr = keyring.get_keyring()
    print("Backend name:", kr.name)
    if hasattr(kr, 'backends'):
        print("Backends:")
        for b in kr.backends:
            print('{}; priority={}, viable={}'
                  .format(b.name, b.priority, b.viable))

def getpass2(prompt):
    input1 = getpass.getpass(prompt)
    input2 = getpass.getpass("Repeat " + prompt)
    if input1 != input2:
        raise ValueError("password mismatch")
    return input1

def format_date(seconds):
    return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(seconds))

def maybe_add_attribute(item, attributes, name, method_name=None, formatter=None):
    if name in attributes:
        return
    if method_name is None:
        method_name = 'get_' + name
    if not hasattr(item, method_name):
        return
    method = getattr(item, method_name)
    value = method()
    attributes[name] = formatter(value) if formatter else value

def get_extended_attributes(item):
    attributes = item.get_attributes()
    maybe_add_attribute(item, attributes, 'label')
    maybe_add_attribute(item, attributes, 'secret_content_type')
    maybe_add_attribute(item, attributes, 'created', formatter=format_date)
    maybe_add_attribute(item, attributes, 'modified', formatter=format_date)
    return attributes

def check_conditions(conditions, attributes):
    for (names, regexp) in conditions:
        value = ''
        for name in names:
            if name in attributes:
                value = attributes[name]
                break
        if not re.search(regexp, value): return False
    return True

def parse_condition_string(arg):
    eq = arg.index('=')
    return re.split(r'[|,]+', arg[:eq]), re.compile(arg[eq+1:])

def all_keyring_items():
    kr = keyring.get_keyring()
    if isinstance(kr, keyring.backends.chainer.ChainerBackend):
        for b in kr.backends:
            if hasattr(b, 'get_preferred_collection'):
                yield from b.get_preferred_collection().get_all_items()
    else:
        yield from kr.get_preferred_collection().get_all_items()

def keyring_items(conditions):
    for item in all_keyring_items():
        attributes = get_extended_attributes(item)
        if check_conditions(conditions, attributes):
            yield item, attributes

def change_passwords(conditions, old_password, new_password, verbosity=1):
    """Change the password in many Gnome Keyring entries to new_password.

Iterate over the keyring keyring_name. Only items matching conditions and where
the current password is old_password are considered. The argument conditions
is a list of elements of the form (names, regexp) where names is a list of
attribute names. An item matches the condition if the value of the first
attribute in names that is present on the item contains a match for regexp.
"""
    for item, attributes in keyring_items(conditions):
        label = attributes['label']
        secret_bytes = item.get_secret()
        if secret_bytes == old_password or \
           secret_bytes == bytes(old_password, 'utf-8'):
            if verbosity >= 1:
                print('Changing:' if new_password is not None else 'Would change:',
                      label)
            if new_password is not None:
                item.set_secret(new_password)
        else:
            if verbosity >= 2:
                print('Has different password, skipping:', label)

def change_password_ui(condition_strings, no_act, verbosity):
    conditions = [parse_condition_string(s) for s in condition_strings]
    old_password = getpass.getpass("Old password: ")
    if no_act:
        new_password = None
    else:
        new_password = getpass2("New password: ")
    change_passwords(conditions, old_password, new_password, verbosity)

def main(args):
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--info', action='store_true',
                        help='Print system information and exit')
    parser.add_argument('--no-act', '-n', action='store_true',
                        help='Don\'t actually change passwords, just list entries with a matching password')
    parser.add_argument('--quiet', '-q', action='store_true',
                        help='Print less information')
    parser.add_argument('--verbose', '-v', action='store_true',
                        help='Print more information')
    parser.add_argument('conditions', nargs='*', metavar='CONDITION',
                        help='Only act on entries matching this condition')
    options = parser.parse_args(args)
    if options.info:
        print_info()
        return
    change_password_ui(options.conditions,
                       no_act=options.no_act,
                       verbosity=1 + options.verbose - options.quiet)

if __name__ == '__main__':
    main(sys.argv[1:])
1

There's a python package for the gnome keyring API. You can follow a sort of tutorial: bending-gnome-keyring-with-python which has some examples of searching for and setting passwords.

There's also a command secret-tool in package libsecret which can get/set passwords in a gnome-keyring.

2
  • 1
    Thanks. I don't think secret-tool can help me because it can't enumerate all the passwords (can it?). I knew about the Python interface, but it's completely undocumented, the bending articles were helpful in cobbling together something that works. Jul 6, 2015 at 17:17
  • @Gilles Congratulations! I had the vague impression secret-tool could find all the entries, but it can only store in the default keyring, so you needed to use seahorse to change the default temporarily.
    – meuh
    Jul 6, 2015 at 17:56

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .