You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
291 lines
11 KiB
291 lines
11 KiB
7 months ago
|
#!/usr/bin/env python
|
||
|
|
||
|
import sys
|
||
|
|
||
|
DBUS_ADVICE = 'please verify that dbus-daemon is running and that the ' \
|
||
|
'DBUS_SESSION_BUS_PID and DBUS_SESSION_BUS_ADDRESS environment ' \
|
||
|
'variables are set appropriately; for example, consider ' \
|
||
|
'running "export $(dbus-launch)" in your terminal session'
|
||
|
IMPORT_ADVICE = 'please verify that:\n' \
|
||
|
'\t1. gnome-keyring-daemon and libgnome-keyring are installed\n' \
|
||
|
'\t2. site-packages for system Python include either the bindings for ' \
|
||
|
'GObject introspection (pygobject*, python*-gobject, python*-gi) or ' \
|
||
|
'the bindings for libgnome-keyring (gnome-python*-gnomekeyring)'
|
||
|
|
||
|
def import_advice(act):
|
||
|
sys.stderr.write('Cannot %s; ' % act)
|
||
|
sys.stderr.write(IMPORT_ADVICE)
|
||
|
sys.stderr.write('\n')
|
||
|
|
||
|
class GnomeKeyringException(Exception):
|
||
|
pass
|
||
|
|
||
|
#
|
||
|
# Use the traditional, deprecated (but still supported) GNOME keyring APIs.
|
||
|
#
|
||
|
# https://developer.gnome.org/gnome-keyring/stable
|
||
|
#
|
||
|
try:
|
||
|
# Use reflection-based pygobject3 bindings for GNOME keyring.
|
||
|
from gi.repository import GnomeKeyring as gk
|
||
|
def get_item(krname, item):
|
||
|
return gk.item_get_info_full_sync(krname, item, gk.ItemInfoFlags.SECRET)
|
||
|
class NoSuchKeyringError(GnomeKeyringException):
|
||
|
pass
|
||
|
except ImportError:
|
||
|
try:
|
||
|
# Fall back to pygobject2 bindings for GNOME keyring.
|
||
|
import gnomekeyring as gk
|
||
|
def get_item(krname, item_id):
|
||
|
return gk.item_get_info_sync(krname, item_id)
|
||
|
NoSuchKeyringError = gk.NoSuchKeyringError
|
||
|
try:
|
||
|
import glib
|
||
|
# Avoid: WARNING **: g_set_application_name not set.
|
||
|
glib.set_application_name('splunk')
|
||
|
except:
|
||
|
pass
|
||
|
except ImportError:
|
||
|
import_advice('import pygobject2/pygobject3 bindings for GnomeKeyring')
|
||
|
raise # re-raise
|
||
|
#
|
||
|
# from gi.repository import Secret
|
||
|
#
|
||
|
# The stable API for libsecret only supports getting, setting, and removing
|
||
|
# individual items with known attributes. For example, there is no way to
|
||
|
# iterate over an existing collection and enumerate all existing items.
|
||
|
#
|
||
|
# This means the stable APIs do not meet our requirements, which include
|
||
|
# "generic fetch of a group of related-but-arbitrary items".
|
||
|
#
|
||
|
# from gi.repository import SecretUnstable
|
||
|
#
|
||
|
# The unstable API does offer collection-oriented operations. However, per
|
||
|
# https://people.gnome.org/~stefw/libsecret-docs/using-python.html:
|
||
|
#
|
||
|
# Some parts of the libsecret API are not yet stable. It is not
|
||
|
# recommended that you use these unstable parts from python. Your code
|
||
|
# will break when the unstable API changes, and due to the lack of a
|
||
|
# compiler you will have no way of knowing when it does.
|
||
|
#
|
||
|
# In other words, we cannot use the unstable APIs either.
|
||
|
#
|
||
|
|
||
|
#
|
||
|
# XXX: Unfortunately, it appears that the Python bindings for the GLib.Array
|
||
|
# class provide no mutators, setters, meaningful constructors, or translations
|
||
|
# to native Python datastructures. As a result, instances of this class are
|
||
|
# essentially read-only, and we cannot even construct meaningful objects to
|
||
|
# pass to methods like item_create_sync().
|
||
|
#
|
||
|
# This means that we cannot make use of keyring item attributes to store extra
|
||
|
# information about each secret, and we cannot leverage attribute-based lookup
|
||
|
# of items. Instead, we must overload the label ("display name") of each item
|
||
|
# and implement our own "collision detection" and "item lookup". Terrible.
|
||
|
#
|
||
|
try:
|
||
|
from gi.repository.GLib import Array
|
||
|
except ImportError:
|
||
|
Array = dict
|
||
|
|
||
|
try:
|
||
|
from gi.repository import GObject
|
||
|
GnomeKeyringResult = GObject.GEnum
|
||
|
except ImportError:
|
||
|
try:
|
||
|
import gobject
|
||
|
GnomeKeyringResult = gobject.GEnum
|
||
|
except ImportError:
|
||
|
import_advice('import pygobject2/pygobject3 bindings for GObject.GEnum')
|
||
|
raise # re-raise
|
||
|
|
||
|
import json
|
||
|
|
||
|
DICT_LABEL = 'label'
|
||
|
DICT_SECRET = 'secret'
|
||
|
DICT_ATTRIBUTES = 'attributes'
|
||
|
|
||
|
def err(desc):
|
||
|
raise GnomeKeyringException(desc)
|
||
|
|
||
|
def verify(result, action):
|
||
|
ok = None
|
||
|
return_if_ok = None
|
||
|
if isinstance(result, tuple) and isinstance(result[0], GnomeKeyringResult):
|
||
|
# <result> is a tuple containing a return code and an actual return
|
||
|
# value. This is the convention for pygobject3 bindings on methods like
|
||
|
# get_info_sync() and item_get_info_full_sync().
|
||
|
ok = result[0]
|
||
|
return_if_ok = result[1]
|
||
|
elif isinstance(result, GnomeKeyringResult):
|
||
|
# <result> is a bare return code. This is the convention for pygobject3
|
||
|
# bindings on methods like item_delete_sync() and item_set_info_sync().
|
||
|
ok = result
|
||
|
return_if_ok = result
|
||
|
else:
|
||
|
# <result> is the return value of a successful method invocation, sans
|
||
|
# return code. This is the convention for pygobject2 bindings.
|
||
|
return result
|
||
|
if ok == gk.Result.OK:
|
||
|
return return_if_ok
|
||
|
if ok == gk.Result.NO_SUCH_KEYRING:
|
||
|
raise NoSuchKeyringError
|
||
|
msg = gk.result_to_message(ok)
|
||
|
if ok == gk.Result.IO_ERROR:
|
||
|
msg += '; '
|
||
|
msg += DBUS_ADVICE
|
||
|
err('Cannot %s: %s' % (action, msg))
|
||
|
|
||
|
def encode_label(identifying_attributes):
|
||
|
# Use each keyring item's label to store a unique ID for that item. Form
|
||
|
# the ID by serializing a dict of identifying attributes to JSON.
|
||
|
return json.dumps(identifying_attributes, sort_keys=True)
|
||
|
|
||
|
def decode_label(label_str):
|
||
|
# Deserialize a dict of identifying attributes from a label value.
|
||
|
return json.loads(label_str)
|
||
|
|
||
|
def pretty_print_label(label_str):
|
||
|
d = decode_label(label_str)
|
||
|
return '%s [%s] %s' % (d['file'], d['stanza'], d['attribute'])
|
||
|
|
||
|
def get_secrets(krname):
|
||
|
item_ids = verify(gk.list_item_ids_sync(krname), 'list items in keyring=%s' % krname)
|
||
|
|
||
|
result = []
|
||
|
for i in item_ids:
|
||
|
# Get this item's label and secret.
|
||
|
item = verify(get_item(krname, i), 'access item in keyring=%s' % krname)
|
||
|
# Emit this item as a standard Python object.
|
||
|
tmp = {DICT_LABEL: pretty_print_label(item.get_display_name()),
|
||
|
DICT_SECRET: item.get_secret(),
|
||
|
DICT_ATTRIBUTES: decode_label(item.get_display_name())}
|
||
|
result.append(tmp)
|
||
|
return result
|
||
|
|
||
|
def set_secrets(krname, to_write, overwrite_all):
|
||
|
item_ids = verify(gk.list_item_ids_sync(krname), 'list pre-existing items in keyring=%s' % krname)
|
||
|
|
||
|
if overwrite_all:
|
||
|
# Remove existing items from the keyring, leaving the container intact.
|
||
|
for i in item_ids:
|
||
|
verify(gk.item_delete_sync(krname, i), 'delete pre-existing item from keyring=%s' % krname)
|
||
|
item_ids = []
|
||
|
|
||
|
existing_items = {}
|
||
|
for i in item_ids:
|
||
|
item = verify(get_item(krname, i), 'access pre-existing item in keyring=%s' % krname)
|
||
|
existing_items[item.get_display_name()] = i
|
||
|
|
||
|
try:
|
||
|
typ = gk.ItemType.GENERIC_SECRET
|
||
|
except AttributeError:
|
||
|
typ = gk.ITEM_GENERIC_SECRET
|
||
|
|
||
|
# Write out new items.
|
||
|
for i in to_write:
|
||
|
needle = encode_label(i[DICT_ATTRIBUTES])
|
||
|
secret = i[DICT_SECRET]
|
||
|
label = pretty_print_label(needle)
|
||
|
if needle in existing_items:
|
||
|
# Matched an existing item; update it in-place.
|
||
|
info = gk.ItemInfo()
|
||
|
info.set_type(typ)
|
||
|
info.set_display_name(needle)
|
||
|
info.set_secret(secret)
|
||
|
verify(gk.item_set_info_sync(krname, existing_items[needle], info), 'edit item (%s) in keyring=%s' % (label, krname))
|
||
|
else:
|
||
|
#
|
||
|
# No existing item was present. Create a new one.
|
||
|
#
|
||
|
# XXX: We must pass update_if_exists=False because if we did not,
|
||
|
# every item would collide with every other. In other words, we
|
||
|
# would only be able to store a single item in the keyring, as we
|
||
|
# are forced to use the same identifying attributes for every item
|
||
|
# we create. This is because the only GLib.Array we can create is
|
||
|
# the empty Array(). Awful.
|
||
|
#
|
||
|
item_id = verify(gk.item_create_sync(krname, typ,
|
||
|
display_name=needle,
|
||
|
attributes=Array(),
|
||
|
secret=secret,
|
||
|
update_if_exists=False),
|
||
|
'create new item (%s) in keyring=%s' % (label, krname))
|
||
|
|
||
|
def remove_secrets(krname, to_remove):
|
||
|
item_ids = verify(gk.list_item_ids_sync(krname), 'list pre-existing items in keyring=%s' % krname)
|
||
|
|
||
|
existing_items = {}
|
||
|
for i in item_ids:
|
||
|
item = verify(get_item(krname, i), 'access pre-existing item in keyring=%s' % krname)
|
||
|
existing_items[item.get_display_name()] = i
|
||
|
|
||
|
for i in to_remove:
|
||
|
needle = encode_label(i[DICT_ATTRIBUTES])
|
||
|
label = pretty_print_label(needle)
|
||
|
if needle not in existing_items:
|
||
|
err('Cannot find item (%s) in keyring=%s' % (label, krname))
|
||
|
verify(gk.item_delete_sync(krname, existing_items[needle]), 'remove item (%s) from keyring=%s' % (label, krname))
|
||
|
|
||
|
def process(args):
|
||
|
krname = args.get('namespace')
|
||
|
if krname is None:
|
||
|
err('No namespace (keyring name) specified')
|
||
|
|
||
|
to_write = args.get('write', {})
|
||
|
to_remove = args.get('remove', {})
|
||
|
overwrite_all = args.get('overwrite', False)
|
||
|
password = args.get('password')
|
||
|
|
||
|
try:
|
||
|
keyring_info = verify(gk.get_info_sync(krname), 'access keyring=%s' % krname)
|
||
|
except NoSuchKeyringError:
|
||
|
if password is None:
|
||
|
err('Cannot create keyring=%s without a password' % krname)
|
||
|
# Desired keyring does not yet exist. Create it on-demand.
|
||
|
verify(gk.create_sync(krname, password), 'create keyring=%s' % krname)
|
||
|
# Try to get info again, now that we have created the missing keyring.
|
||
|
keyring_info = verify(gk.get_info_sync(krname), 'access keyring=%s' % krname)
|
||
|
|
||
|
if keyring_info.get_is_locked():
|
||
|
if password is None:
|
||
|
err('Cannot access locked keyring=%s without a password' % krname)
|
||
|
# Unlock the desired keyring.
|
||
|
ok = gk.unlock_sync(krname, password)
|
||
|
if ok is not None:
|
||
|
# Handle pygobject3-style invocation of unlock_sync().
|
||
|
if ok == gk.Result.IO_ERROR:
|
||
|
# An incorrect password causes an IO_ERROR for some reason. Emit a
|
||
|
# clearer error message than the default result_to_message() one.
|
||
|
err('Cannot unlock keyring=%s: Invalid password' % krname)
|
||
|
verify(ok, 'unlock keyring=%s' % krname)
|
||
|
|
||
|
result = {} # By default, emit minimal valid JSON.
|
||
|
|
||
|
if len(to_write) == 0 and len(to_remove) == 0:
|
||
|
# Given nothing to write, we emit existing secrets.
|
||
|
result = get_secrets(krname)
|
||
|
|
||
|
if len(to_write) > 0:
|
||
|
set_secrets(krname, to_write, overwrite_all)
|
||
|
|
||
|
if len(to_remove) > 0:
|
||
|
remove_secrets(krname, to_remove)
|
||
|
|
||
|
json.dump(result, sys.stdout)
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
args = json.load(sys.stdin) # error out on bad/empty JSON
|
||
|
|
||
|
try:
|
||
|
NoKeyringDaemonError = gk.NoKeyringDaemonError
|
||
|
except AttributeError:
|
||
|
NoKeyringDaemonError = GnomeKeyringException
|
||
|
|
||
|
try:
|
||
|
process(args)
|
||
|
except GnomeKeyringException:
|
||
|
raise # re-raise any generic exceptions
|
||
|
except NoKeyringDaemonError:
|
||
|
raise NoKeyringDaemonError(DBUS_ADVICE)
|