Gnome-Keyring

在 Gnome Keyring 中一次更改多個帳戶的密碼

  • November 3, 2021

我使用 Gnome Keyring 作為各種應用程序的密碼儲存後端。許多條目用於登錄同一帳戶的不同方式,通過不同方式訪問。我已更改此帳戶的密碼,現在我想更新我的密鑰環中的所有條目。

我通常使用 Seahorse 來編輯密鑰環,但它只能讓我通過稍微尷尬的按鍵或滑鼠點擊序列來編輯單個條目。有許多密碼要更改,這很乏味。

如何有效地更新 Gnome Keyring 中許多條目的密碼,即無需一次又一次地輸入?

按照meuh 的建議,我編寫了一個 Python 腳本來更改所有符合特定條件(例如特定使用者名和伺服器名稱)的條目的密碼。該腳本請求舊密碼作為完整性檢查,並且僅更改具有指定舊密碼的條目。範例用法:

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

警告:程式碼已令人滿意地執行一次。這就是我的測試範圍。

注意:API 隨時間而變化。下面的程式碼適用於 Ubuntu 20.04。此答案的舊版本具有可在 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:])

引用自:https://unix.stackexchange.com/questions/214126