378 lines
15 KiB
Python
378 lines
15 KiB
Python
import base64
|
|
import sqlite3
|
|
import time
|
|
|
|
from .encryption import (generate_key,
|
|
aes_cfb8_wrap, aes_cfb8_unwrap,
|
|
aes_cfb128_wrap, aes_cfb128_unwrap)
|
|
from .external_keys import load_ext_key, store_ext_key
|
|
|
|
class SecretsDatabase():
|
|
def __init__(self, path, key_path):
|
|
self.db = sqlite3.connect(path)
|
|
self.kp = key_path
|
|
self.mk = None
|
|
self.dk = None
|
|
self.ver = 0
|
|
self.initialize()
|
|
self.upgrade()
|
|
self.load_keys()
|
|
|
|
def initialize(self):
|
|
cur = self.db.cursor()
|
|
cur.execute("CREATE TABLE IF NOT EXISTS version (" \
|
|
" version INTEGER" \
|
|
")")
|
|
cur.execute("CREATE TABLE IF NOT EXISTS sequence (" \
|
|
" next INTEGER" \
|
|
")")
|
|
cur.execute("CREATE TABLE IF NOT EXISTS parameters (" \
|
|
" name TEXT," \
|
|
" value TEXT" \
|
|
")")
|
|
cur.execute("CREATE TABLE IF NOT EXISTS collections (" \
|
|
" object TEXT," \
|
|
" label TEXT," \
|
|
" created INTEGER," \
|
|
" modified INTEGER" \
|
|
")")
|
|
cur.execute("CREATE TABLE IF NOT EXISTS aliases (" \
|
|
" alias TEXT," \
|
|
" target TEXT" \
|
|
")")
|
|
cur.execute("CREATE TABLE IF NOT EXISTS items (" \
|
|
" object TEXT," \
|
|
" label TEXT," \
|
|
" created INTEGER," \
|
|
" modified INTEGER" \
|
|
")")
|
|
cur.execute("CREATE TABLE IF NOT EXISTS attributes (" \
|
|
" object TEXT," \
|
|
" attribute TEXT," \
|
|
" value TEXT" \
|
|
")")
|
|
cur.execute("CREATE TABLE IF NOT EXISTS secrets (" \
|
|
" object TEXT," \
|
|
" secret TEXT," \
|
|
" type TEXT" \
|
|
")")
|
|
self.db.commit()
|
|
|
|
# Encryption keys
|
|
|
|
def _store_mkey(self, key):
|
|
print("DB: storing master key to %r" % (self.kp))
|
|
store_ext_key(self.kp, base64.b64encode(key).decode())
|
|
|
|
def _load_mkey(self):
|
|
print("DB: loading master key from %r" % (self.kp))
|
|
try:
|
|
mkey = base64.b64decode(load_ext_key(self.kp))
|
|
if len(mkey) != 32:
|
|
raise IOError("wrong mkey length (expected 32 bytes)")
|
|
except (KeyError, FileNotFoundError):
|
|
raise RuntimeError("could not load the database key from %r" % (self.kp))
|
|
self.mk = mkey
|
|
|
|
def _load_dkey(self, *, v=0):
|
|
if (v or self.ver) == 3:
|
|
cur = self.db.cursor()
|
|
cur.execute("SELECT value FROM parameters WHERE name = 'dkey'")
|
|
dkey, = cur.fetchone()
|
|
try:
|
|
dkey = self._decrypt_buf(dkey, with_mkey=True, v=3)
|
|
except IOError as e:
|
|
raise IOError("wrong mkey (%s)" % e)
|
|
if len(dkey) != 32:
|
|
raise IOError("wrong dkey length (expected 32 bytes)")
|
|
self.dk = dkey
|
|
else:
|
|
raise NotImplementedError("unknown schema version %r" % (v or self.ver))
|
|
|
|
def load_keys(self):
|
|
if self.ver >= 2:
|
|
self._load_mkey()
|
|
self._load_dkey()
|
|
|
|
def _encrypt_buf(self, buf, *, with_mkey=False, v=0):
|
|
key = self.mk if with_mkey else self.dk
|
|
if (v or self.ver) >= 3:
|
|
return aes_cfb128_wrap(buf, key)
|
|
elif (v or self.ver) == 2:
|
|
return aes_cfb8_wrap(buf, key)
|
|
else:
|
|
raise NotImplementedError("unknown schema version %r" % (v or self.ver))
|
|
|
|
def _decrypt_buf(self, buf, *, with_mkey=None, v=0):
|
|
key = self.mk if with_mkey else self.dk
|
|
if (v or self.ver) >= 3:
|
|
return aes_cfb128_unwrap(buf, key)
|
|
elif (v or self.ver) == 2:
|
|
return aes_cfb8_unwrap(buf, key)
|
|
else:
|
|
raise NotImplementedError("unknown schema version %r" % (v or self.ver))
|
|
|
|
# Schema upgrades
|
|
|
|
def _upgrade_v0_to_v1(self):
|
|
# Undo commit affc514 "make items use bus paths underneath their collection"
|
|
cur = self.db.cursor()
|
|
cur.execute("SELECT object FROM items" \
|
|
" WHERE object LIKE '/org/freedesktop/secrets/collection/c%/i%'")
|
|
res = cur.fetchall()
|
|
for (old_object,) in res:
|
|
item_id = old_object.split("/")[-1]
|
|
new_object = "/org/freedesktop/secrets/item/%s" % item_id
|
|
print("DB: moving object %r => %r" % (old_object, new_object))
|
|
cur.execute("UPDATE items SET object = ? WHERE object = ?",
|
|
(new_object, old_object))
|
|
cur.execute("UPDATE secrets SET object = ? WHERE object = ?",
|
|
(new_object, old_object))
|
|
cur.execute("UPDATE attributes SET object = ? WHERE object = ?",
|
|
(new_object, old_object))
|
|
|
|
def _upgrade_v1_to_v3(self):
|
|
# Version 2 encrypts all secrets using the database master key
|
|
cur = self.db.cursor()
|
|
# Generate a "master key"
|
|
print("DB: generating a master key")
|
|
mkey = generate_key()
|
|
self._store_mkey(mkey)
|
|
self.mk = mkey
|
|
# Generate a "data key"
|
|
print("DB: generating a data key")
|
|
dkey = generate_key()
|
|
blob = self._encrypt_buf(dkey, with_mkey=True, v=3)
|
|
cur.execute("INSERT INTO parameters VALUES ('dkey', ?)", (blob,))
|
|
self.dk = dkey
|
|
# Encrypt all currently stored secrets
|
|
cur.execute("SELECT object, secret FROM secrets")
|
|
res = cur.fetchall()
|
|
for object, blob in res:
|
|
print("DB: encrypting secret %r" % (object,))
|
|
blob = self._encrypt_buf(blob, v=3)
|
|
cur.execute("UPDATE secrets SET secret = ? WHERE object = ?", (blob, object))
|
|
|
|
def _upgrade_v2_to_v3(self):
|
|
# Version 3 uses AES-CFB128 instead of (badly chosen) AES-CFB8
|
|
cur = self.db.cursor()
|
|
# Re-encrypt the data key
|
|
self._load_mkey()
|
|
cur.execute("SELECT value FROM parameters WHERE name = 'dkey'")
|
|
blob, = cur.fetchone()
|
|
blob = self._decrypt_buf(blob, with_mkey=True, v=2)
|
|
blob = self._encrypt_buf(blob, with_mkey=True, v=3)
|
|
cur.execute("UPDATE parameters SET value = ? WHERE name = 'dkey'", (blob,))
|
|
# Re-encrypt all currently stored secrets
|
|
self._load_dkey(v=3)
|
|
cur.execute("SELECT object, secret FROM secrets")
|
|
res = cur.fetchall()
|
|
for object, blob in res:
|
|
print("DB: re-encrypting secret %r" % (object,))
|
|
blob = self._decrypt_buf(blob, v=2)
|
|
blob = self._encrypt_buf(blob, v=3)
|
|
cur.execute("UPDATE secrets SET secret = ? WHERE object = ?", (blob, object))
|
|
|
|
def upgrade(self):
|
|
print("DB: current database version is %d" % self.get_version())
|
|
if self.get_version() == 0:
|
|
print("DB: upgrading to version %d" % (1,))
|
|
self._upgrade_v0_to_v1()
|
|
self.db.cursor().execute("UPDATE version SET version = ?", (1,))
|
|
self.db.commit()
|
|
if self.get_version() == 1:
|
|
print("DB: upgrading to version %d" % (3,))
|
|
self._upgrade_v1_to_v3()
|
|
self.db.cursor().execute("UPDATE version SET version = ?", (3,))
|
|
self.db.commit()
|
|
print("DB: vacuuming database")
|
|
self.db.cursor().execute("VACUUM")
|
|
if self.get_version() == 2:
|
|
print("DB: upgrading to version %d" % (3,))
|
|
self._upgrade_v2_to_v3()
|
|
self.db.cursor().execute("UPDATE version SET version = ?", (3,))
|
|
self.db.commit()
|
|
self.ver = self.get_version()
|
|
print("DB: new database version is %d" % self.ver)
|
|
|
|
def get_version(self):
|
|
cur = self.db.cursor()
|
|
cur.execute("SELECT version FROM version")
|
|
res = cur.fetchone()
|
|
if res:
|
|
version = res[0]
|
|
else:
|
|
version = 0
|
|
cur.execute("INSERT INTO version VALUES (?)", (version,))
|
|
self.db.commit()
|
|
return version
|
|
|
|
def get_next_object_id(self):
|
|
cur = self.db.cursor()
|
|
cur.execute("SELECT next FROM sequence")
|
|
res = cur.fetchone()
|
|
if res:
|
|
oid = res[0]
|
|
cur.execute("UPDATE sequence SET next = next + 1")
|
|
else:
|
|
oid = 0
|
|
cur.execute("INSERT INTO sequence VALUES (?)", (oid + 1,))
|
|
self.db.commit()
|
|
print("DB: allocated new object ID %r" % oid)
|
|
return oid
|
|
|
|
# Collections
|
|
|
|
def add_collection(self, object, label):
|
|
print("DB: adding collection %r with label %r" % (object, label))
|
|
now = int(time.time())
|
|
cur = self.db.cursor()
|
|
cur.execute("INSERT INTO collections VALUES (?,?,?,?)", (object, label, now, now))
|
|
self.db.commit()
|
|
|
|
def list_collections(self):
|
|
cur = self.db.cursor()
|
|
cur.execute("SELECT object FROM collections")
|
|
return [r[0] for r in cur.fetchall()]
|
|
|
|
def collection_exists(self, object):
|
|
return bool(self.get_collection_metadata(object))
|
|
|
|
def get_collection_metadata(self, object):
|
|
print("DB: getting collection metadata for %r" % (object,))
|
|
cur = self.db.cursor()
|
|
cur.execute("SELECT label, created, modified FROM collections WHERE object = ?",
|
|
(object,))
|
|
return cur.fetchone()
|
|
|
|
def set_collection_label(self, object, label):
|
|
print("DB: setting label for %r to %r" % (object, label))
|
|
now = int(time.time())
|
|
cur = self.db.cursor()
|
|
cur.execute("UPDATE collections SET label = ?, modified = ? WHERE object = ?",
|
|
(label, now, object))
|
|
self.db.commit()
|
|
|
|
def delete_collection(self, object):
|
|
print("DB: deleting collection %r" % (object,))
|
|
cur = self.db.cursor()
|
|
subquery = "SELECT object FROM attributes" \
|
|
" WHERE attribute = 'xdg:collection' AND value = ?"
|
|
cur.execute("DELETE FROM items WHERE object IN (" + subquery + ")", (object,))
|
|
cur.execute("DELETE FROM secrets WHERE object IN (" + subquery + ")", (object,))
|
|
cur.execute("DELETE FROM attributes WHERE object IN (" + subquery + ")", (object,))
|
|
cur.execute("DELETE FROM aliases WHERE target = ?", (object,))
|
|
cur.execute("DELETE FROM collections WHERE object = ?", (object,))
|
|
self.db.commit()
|
|
|
|
# Aliases
|
|
|
|
def add_alias(self, alias, target):
|
|
print("DB: adding alias %r -> %r" % (alias, target))
|
|
cur = self.db.cursor()
|
|
cur.execute("DELETE FROM aliases WHERE alias = ?", (alias,))
|
|
cur.execute("INSERT INTO aliases VALUES (?,?)", (alias, target))
|
|
self.db.commit()
|
|
|
|
def get_aliases(self):
|
|
cur = self.db.cursor()
|
|
cur.execute("SELECT alias, target FROM aliases")
|
|
return cur.fetchall()
|
|
|
|
def resolve_alias(self, alias):
|
|
print("DB: resolving alias %r" % (alias,))
|
|
cur = self.db.cursor()
|
|
cur.execute("SELECT target FROM aliases WHERE alias = ?", (alias,))
|
|
r = cur.fetchone()
|
|
return r[0] if r else None
|
|
|
|
def delete_alias(self, alias):
|
|
print("DB: deleting alias %r" % (alias,))
|
|
cur = self.db.cursor()
|
|
cur.execute("DELETE FROM aliases WHERE alias = ?", (alias,))
|
|
self.db.commit()
|
|
|
|
# Items
|
|
|
|
def add_item(self, object, label, attrs, secret, sec_type):
|
|
now = int(time.time())
|
|
cur = self.db.cursor()
|
|
cur.execute("INSERT INTO items VALUES (?,?,?,?)", (object, label, now, now))
|
|
for key, val in attrs.items():
|
|
cur.execute("INSERT INTO attributes VALUES (?,?,?)", (object, key, val))
|
|
cur.execute("INSERT INTO secrets VALUES (?,?,?)", (object, self._encrypt_buf(secret),
|
|
sec_type))
|
|
self.db.commit()
|
|
|
|
def find_items(self, match_attrs):
|
|
qry = "SELECT object FROM attributes WHERE attribute = ? AND value = ?"
|
|
qry = " INTERSECT ".join([qry] * len(match_attrs))
|
|
parvs = []
|
|
for k, v in match_attrs.items():
|
|
parvs += [k, v]
|
|
print("DB: searching for %r" % parvs)
|
|
cur = self.db.cursor()
|
|
cur.execute(qry, parvs)
|
|
return [r[0] for r in cur.fetchall()]
|
|
|
|
def item_exists(self, object):
|
|
return bool(self.get_item_metadata(object))
|
|
|
|
def get_item_metadata(self, object):
|
|
print("DB: getting metadata for %r" % object)
|
|
cur = self.db.cursor()
|
|
cur.execute("SELECT label, created, modified FROM items WHERE object = ?",
|
|
(object,))
|
|
return cur.fetchone()
|
|
|
|
def set_item_label(self, object, label):
|
|
print("DB: setting label for %r to %r" % (object, label))
|
|
now = int(time.time())
|
|
cur = self.db.cursor()
|
|
cur.execute("UPDATE items SET label = ?, modified = ? WHERE object = ?",
|
|
(label, now, object))
|
|
self.db.commit()
|
|
|
|
def get_item_attributes(self, object):
|
|
print("DB: getting attrs for %r" % object)
|
|
cur = self.db.cursor()
|
|
cur.execute("SELECT attribute, value FROM attributes WHERE object = ?", (object,))
|
|
return {k: v for k, v in cur.fetchall()}
|
|
|
|
def set_item_attributes(self, object, attrs):
|
|
print("DB: setting attrs for %r to %r" % (object, attrs))
|
|
now = int(time.time())
|
|
cur = self.db.cursor()
|
|
cur.execute("DELETE FROM attributes WHERE object = ?", (object,))
|
|
for key, val in attrs.items():
|
|
cur.execute("INSERT INTO attributes VALUES (?,?,?)", (object, key, val))
|
|
cur.execute("UPDATE items SET modified = ? WHERE object = ?", (now, object))
|
|
self.db.commit()
|
|
|
|
def get_secret(self, object):
|
|
print("DB: getting secret for %r" % object)
|
|
cur = self.db.cursor()
|
|
cur.execute("SELECT secret, type FROM secrets WHERE object = ?", (object,))
|
|
secret, sec_type = cur.fetchone()
|
|
return self._decrypt_buf(secret), sec_type
|
|
|
|
def set_secret(self, object, secret, sec_type):
|
|
print("DB: updating secret for %r" % object)
|
|
if hasattr(secret, "encode"):
|
|
raise ValueError("secret needs to be bytes, not str")
|
|
now = int(time.time())
|
|
cur = self.db.cursor()
|
|
cur.execute("UPDATE secrets SET secret = ?, type = ? WHERE object = ?",
|
|
(self._encrypt_buf(secret), sec_type, object))
|
|
cur.execute("UPDATE items SET modified = ? WHERE object = ?",
|
|
(now, object))
|
|
self.db.commit()
|
|
|
|
def delete_item(self, object):
|
|
print("DB: deleting item %r" % object)
|
|
cur = self.db.cursor()
|
|
cur.execute("DELETE FROM attributes WHERE object = ?", (object,))
|
|
cur.execute("DELETE FROM secrets WHERE object = ?", (object,))
|
|
cur.execute("DELETE FROM items WHERE object = ?", (object,))
|
|
self.db.commit()
|