wip: aaaaaaaa
This commit is contained in:
parent
26efd8e1a7
commit
63560563ea
3 changed files with 251 additions and 50 deletions
75
kakushi/dhcrypto.py
Normal file
75
kakushi/dhcrypto.py
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
# stolen from https://github.com/mitya57/secretstorage/blob/acf549440f86062a8f616530ff9a487fafffd92c/secretstorage/dhcrypto.py
|
||||||
|
|
||||||
|
import hmac
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
|
||||||
|
from hashlib import sha256
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
|
||||||
|
# A standard 1024 bits (128 bytes) prime number for use in Diffie-Hellman exchange
|
||||||
|
DH_PRIME_1024_BYTES = (
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC9, 0x0F, 0xDA, 0xA2, 0x21, 0x68,
|
||||||
|
0xC2, 0x34, 0xC4, 0xC6, 0x62, 0x8B, 0x80, 0xDC, 0x1C, 0xD1, 0x29, 0x02, 0x4E, 0x08,
|
||||||
|
0x8A, 0x67, 0xCC, 0x74, 0x02, 0x0B, 0xBE, 0xA6, 0x3B, 0x13, 0x9B, 0x22, 0x51, 0x4A,
|
||||||
|
0x08, 0x79, 0x8E, 0x34, 0x04, 0xDD, 0xEF, 0x95, 0x19, 0xB3, 0xCD, 0x3A, 0x43, 0x1B,
|
||||||
|
0x30, 0x2B, 0x0A, 0x6D, 0xF2, 0x5F, 0x14, 0x37, 0x4F, 0xE1, 0x35, 0x6D, 0x6D, 0x51,
|
||||||
|
0xC2, 0x45, 0xE4, 0x85, 0xB5, 0x76, 0x62, 0x5E, 0x7E, 0xC6, 0xF4, 0x4C, 0x42, 0xE9,
|
||||||
|
0xA6, 0x37, 0xED, 0x6B, 0x0B, 0xFF, 0x5C, 0xB6, 0xF4, 0x06, 0xB7, 0xED, 0xEE, 0x38,
|
||||||
|
0x6B, 0xFB, 0x5A, 0x89, 0x9F, 0xA5, 0xAE, 0x9F, 0x24, 0x11, 0x7C, 0x4B, 0x1F, 0xE6,
|
||||||
|
0x49, 0x28, 0x66, 0x51, 0xEC, 0xE6, 0x53, 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def int_to_bytes(number: int) -> bytes:
|
||||||
|
return number.to_bytes(math.ceil(number.bit_length() / 8), 'big')
|
||||||
|
|
||||||
|
|
||||||
|
DH_PRIME_1024 = int.from_bytes(DH_PRIME_1024_BYTES, 'big')
|
||||||
|
|
||||||
|
|
||||||
|
class Session:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.object_path: Optional[str] = None
|
||||||
|
self.aes_key: Optional[bytes] = None
|
||||||
|
self.encrypted = False
|
||||||
|
# 128-bytes-long strong random number
|
||||||
|
self.my_private_key = int.from_bytes(os.urandom(0x80), 'big')
|
||||||
|
self.my_public_key = pow(2, self.my_private_key, DH_PRIME_1024)
|
||||||
|
|
||||||
|
def set_client_public_key(self, client_public_key: int) -> None:
|
||||||
|
self.encrypted = True
|
||||||
|
common_secret_int = pow(client_public_key, self.my_private_key,
|
||||||
|
DH_PRIME_1024)
|
||||||
|
common_secret = int_to_bytes(common_secret_int)
|
||||||
|
# Prepend NULL bytes if needed
|
||||||
|
common_secret = b'\x00' * (0x80 - len(common_secret)) + common_secret
|
||||||
|
# HKDF with null salt, empty info and SHA-256 hash
|
||||||
|
salt = b'\x00' * 0x20
|
||||||
|
pseudo_random_key = hmac.new(salt, common_secret, sha256).digest()
|
||||||
|
output_block = hmac.new(pseudo_random_key, b'\x01', sha256).digest()
|
||||||
|
# Resulting AES key should be 128-bit
|
||||||
|
self.aes_key = output_block[:0x10]
|
||||||
|
|
||||||
|
def get_my_public_key(self):
|
||||||
|
return int_to_bytes(self.my_public_key)
|
||||||
|
|
||||||
|
def encrypt(self, secret):
|
||||||
|
if isinstance(secret, str):
|
||||||
|
secret = secret.encode('utf-8')
|
||||||
|
|
||||||
|
if not self.encrypted:
|
||||||
|
return b'', secret
|
||||||
|
|
||||||
|
padding = 0x10 - (len(secret) & 0xf)
|
||||||
|
secret += bytes((padding,) * padding)
|
||||||
|
aes_iv = os.urandom(0x10)
|
||||||
|
aes = algorithms.AES(self.aes_key)
|
||||||
|
encryptor = Cipher(aes, modes.CBC(aes_iv), default_backend()).encryptor()
|
||||||
|
encrypted_secret = encryptor.update(secret) + encryptor.finalize()
|
||||||
|
|
||||||
|
return aes_iv, encrypted_secret
|
|
@ -2,6 +2,29 @@ import socket
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import typing
|
import typing
|
||||||
|
import shlex
|
||||||
|
|
||||||
|
class Key(dict):
|
||||||
|
def from_line(line):
|
||||||
|
key = Key()
|
||||||
|
for token in shlex.split(line):
|
||||||
|
if '=' in token:
|
||||||
|
k, v = token.split('=')
|
||||||
|
key[k] = v
|
||||||
|
elif token.endswith('!'):
|
||||||
|
key[token] = None
|
||||||
|
return key
|
||||||
|
|
||||||
|
def has_secret(self):
|
||||||
|
return any(map(lambda kv: kv[0].endswith('!'), self.items()))
|
||||||
|
|
||||||
|
def to_query(self):
|
||||||
|
return dict(filter(lambda kv: kv[1] is not None, self.items()))
|
||||||
|
|
||||||
|
# this is silly but apparently according to the freedesktop shitspec there can only be one,
|
||||||
|
# so return whichever is first
|
||||||
|
def get_secret(self):
|
||||||
|
return next(filter(lambda kv: kv[0].endswith('!'), self.items()))[1]
|
||||||
|
|
||||||
class Client:
|
class Client:
|
||||||
def __init__(self, addr = None):
|
def __init__(self, addr = None):
|
||||||
|
@ -30,12 +53,25 @@ class Client:
|
||||||
|
|
||||||
self.sock.send(qs.encode('utf-8'))
|
self.sock.send(qs.encode('utf-8'))
|
||||||
raw_res = self._read_raw_response()
|
raw_res = self._read_raw_response()
|
||||||
return raw_res
|
|
||||||
|
res = []
|
||||||
|
for line in raw_res:
|
||||||
|
if line.startswith('error '):
|
||||||
|
raise Exception(line[6:])
|
||||||
|
if not line.startswith('key '):
|
||||||
|
raise Exception(f'parse error: {line}')
|
||||||
|
line = line[4:]
|
||||||
|
# parse 'key ...' line
|
||||||
|
key = Key.from_line(line)
|
||||||
|
res.append(key)
|
||||||
|
return res
|
||||||
|
|
||||||
def _read_raw_response(self):
|
def _read_raw_response(self):
|
||||||
res = ''
|
res = ''
|
||||||
while not '\nend\n' in res:
|
while not '\nend\n' in res:
|
||||||
res += self.sock.recv(4096).decode('utf-8')
|
res += self.sock.recv(4096).decode('utf-8')
|
||||||
|
if res.startswith('end\n'):
|
||||||
|
break
|
||||||
res = res.strip()
|
res = res.strip()
|
||||||
res_lines = res.splitlines()
|
res_lines = res.splitlines()
|
||||||
res_lines.pop()
|
res_lines.pop()
|
||||||
|
|
188
kakushi/main.py
188
kakushi/main.py
|
@ -1,48 +1,53 @@
|
||||||
|
from curses import KEY_CANCEL
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from random import randint, random
|
||||||
from pydbus import SessionBus
|
from pydbus import SessionBus
|
||||||
from pydbus.generic import signal
|
from pydbus.generic import signal
|
||||||
from gi.repository import GLib
|
from gi.repository import GLib
|
||||||
import himitsu
|
import himitsu
|
||||||
|
import dhcrypto
|
||||||
|
|
||||||
|
|
||||||
loop = GLib.MainLoop()
|
loop = GLib.MainLoop()
|
||||||
bus = SessionBus()
|
bus = SessionBus()
|
||||||
hiq = himitsu.Client()
|
hiq = himitsu.Client()
|
||||||
|
|
||||||
|
|
||||||
|
SESSIONS_NS = '/org/freedesktop/secrets/sessions/'
|
||||||
|
|
||||||
|
|
||||||
class Collection(object):
|
class Collection(object):
|
||||||
'''
|
'''
|
||||||
<node>
|
<node>
|
||||||
<interface name="org.freedesktop.Secret.Collection">
|
<interface name="org.freedesktop.Secret.Collection">
|
||||||
<property name="Items" type="ao" access="read" />
|
|
||||||
<property name="Private" type="s" access="read" />
|
|
||||||
<property name="Label" type="s" access="readwrite" />
|
|
||||||
<property name="Locked" type="b" access="read" />
|
|
||||||
<property name="Created" type="t" access="read" />
|
|
||||||
<property name="Modified" type="t" access="read" />
|
|
||||||
|
|
||||||
<method name="Delete">
|
<method name="Delete">
|
||||||
<arg name="prompt" type="o" direction="out" />
|
<arg type="o" name="prompt" direction="out" />
|
||||||
</method>
|
</method>
|
||||||
|
|
||||||
<method name="SearchItems">
|
<method name="SearchItems">
|
||||||
<arg name="attributes" type="a{ss}" direction="in" />
|
<arg type="a{ss}" name="attributes" direction="in" />
|
||||||
<arg name="results" type="ao" direction="out" />
|
<arg type="ao" name="results" direction="out" />
|
||||||
</method>
|
</method>
|
||||||
|
|
||||||
<method name="CreateItem">
|
<method name="CreateItem">
|
||||||
<arg name="properties" type="a{sv}" direction="in" />
|
<arg type="a{sv}" name="properties" direction="in" />
|
||||||
<arg name="secret" type="(sayay)" direction="in" />
|
<arg type="(oayays)" name="secret" direction="in" />
|
||||||
<arg name="replace" type="b" direction="in" />
|
<arg type="b" name="replace" direction="in" />
|
||||||
<arg name="item" type="o" direction="out" />
|
<arg type="o" name="item" direction="out" />
|
||||||
<arg name="prompt" type="o" direction="out" />
|
<arg type="o" name="prompt" direction="out" />
|
||||||
</method>
|
</method>
|
||||||
|
|
||||||
<signal name="ItemCreated">
|
<signal name="ItemCreated">
|
||||||
<arg name="item" type="o" />
|
<arg type="o" name="item" />
|
||||||
</signal>
|
</signal>
|
||||||
|
|
||||||
<signal name="ItemDeleted">
|
<signal name="ItemDeleted">
|
||||||
<arg name="item" type="o" />
|
<arg type="o" name="item" />
|
||||||
</signal>
|
</signal>
|
||||||
|
<signal name="ItemChanged">
|
||||||
|
<arg type="o" name="item" />
|
||||||
|
</signal>
|
||||||
|
<property type="ao" name="Items" access="read" />
|
||||||
|
<property type="s" name="Label" access="readwrite" />
|
||||||
|
<property type="b" name="Locked" access="read" />
|
||||||
|
<property type="t" name="Created" access="read" />
|
||||||
|
<property type="t" name="Modified" access="read" />
|
||||||
</interface>
|
</interface>
|
||||||
</node>
|
</node>
|
||||||
'''
|
'''
|
||||||
|
@ -59,58 +64,142 @@ class Collection(object):
|
||||||
|
|
||||||
ItemCreated = signal()
|
ItemCreated = signal()
|
||||||
ItemDeleted = signal()
|
ItemDeleted = signal()
|
||||||
|
ItemChanged = signal()
|
||||||
|
|
||||||
class Service(object):
|
class Service(object):
|
||||||
'''
|
'''
|
||||||
<node>
|
<node>
|
||||||
<interface name="org.freedesktop.Secret.Service">
|
<interface name="org.freedesktop.Secret.Service">
|
||||||
<property name="Collections" type="ao" access="read" />
|
|
||||||
<property name="DefaultCollection" type="o" access="readwrite" />
|
|
||||||
|
|
||||||
<method name="OpenSession">
|
<method name="OpenSession">
|
||||||
<arg name="result" type="o" direction="out" />
|
<arg type="s" name="algorithm" direction="in" />
|
||||||
|
<arg type="v" name="input" direction="in" />
|
||||||
|
<arg type="v" name="output" direction="out" />
|
||||||
|
<arg type="o" name="result" direction="out" />
|
||||||
</method>
|
</method>
|
||||||
|
|
||||||
<method name="CreateCollection">
|
<method name="CreateCollection">
|
||||||
<arg name="label" type="s" direction="in" />
|
<arg type="a{sv}" name="properties" direction="in" />
|
||||||
<arg name="private" type="b" direction="in" />
|
<arg type="s" name="alias" direction="in" />
|
||||||
|
<arg type="o" name="collection" direction="out" />
|
||||||
|
<arg type="o" name="prompt" direction="out" />
|
||||||
|
</method>
|
||||||
|
<method name="SearchItems">
|
||||||
|
<arg type="a{ss}" name="attributes" direction="in" />
|
||||||
|
<arg type="ao" name="unlocked" direction="out" />
|
||||||
|
<arg type="ao" name="locked" direction="out" />
|
||||||
|
</method>
|
||||||
|
<method name="Unlock">
|
||||||
|
<arg type="ao" name="objects" direction="in" />
|
||||||
|
<arg type="ao" name="unlocked" direction="out" />
|
||||||
|
<arg type="o" name="prompt" direction="out" />
|
||||||
|
</method>
|
||||||
|
<method name="Lock">
|
||||||
|
<arg type="ao" name="objects" direction="in" />
|
||||||
|
<arg type="ao" name="locked" direction="out" />
|
||||||
|
<arg type="o" name="Prompt" direction="out" />
|
||||||
</method>
|
</method>
|
||||||
|
|
||||||
<method name="LockService" />
|
<method name="LockService" />
|
||||||
|
<method name="ChangeLock">
|
||||||
<method name="SearchCollections">
|
<arg type="o" name="collection" direction="in" />
|
||||||
<arg name="fields" type="a{ss}" direction="in" />
|
<arg type="o" name="prompt" direction="out" />
|
||||||
<arg name="results" type="ao" direction="out" />
|
|
||||||
<arg name="locked" type="ao" direction="out" />
|
|
||||||
</method>
|
</method>
|
||||||
|
<method name="GetSecrets">
|
||||||
<method name="RetrieveSecrets">
|
<arg type="ao" name="items" direction="in" />
|
||||||
<arg name="items" type='as' direction='in' />
|
<arg type="o" name="session" direction="in" />
|
||||||
<arg name="secrets" type='ao' direction='out' />
|
<arg type="a{o(oayays)}" name="secrets" direction="out" />
|
||||||
|
</method>
|
||||||
|
<method name="ReadAlias">
|
||||||
|
<arg type="s" name="name" direction="in" />
|
||||||
|
<arg type="o" name="collection" direction="out" />
|
||||||
|
</method>
|
||||||
|
<method name="SetAlias">
|
||||||
|
<arg type="s" name="name" direction="in" />
|
||||||
|
<arg type="o" name="collection" direction="in" />
|
||||||
</method>
|
</method>
|
||||||
|
|
||||||
<signal name="CollectionCreated">
|
<signal name="CollectionCreated">
|
||||||
<arg name="collection" type="o" />
|
<arg type="o" name="collection" />
|
||||||
</signal>
|
</signal>
|
||||||
|
|
||||||
<signal name="CollectionDeleted">
|
<signal name="CollectionDeleted">
|
||||||
<arg name="collection" type="o" />
|
<arg type="o" name="collection" />
|
||||||
</signal>
|
</signal>
|
||||||
|
<signal name="CollectionChanged">
|
||||||
|
<arg type="o" name="collection" />
|
||||||
|
</signal>
|
||||||
|
<property type="ao" name="Collections" access="read" />
|
||||||
</interface>
|
</interface>
|
||||||
|
<node name="collection" />
|
||||||
</node>
|
</node>
|
||||||
'''
|
'''
|
||||||
sessions = []
|
sessions = {}
|
||||||
|
keycache = []
|
||||||
|
|
||||||
def OpenSession(self):
|
def OpenSession(self, algorithm, input):
|
||||||
print('OpenSession')
|
print('OpenSession', algorithm, input)
|
||||||
pass
|
session_id = len(self.sessions)
|
||||||
|
session_path = f'{SESSIONS_NS}{str(session_id)}'
|
||||||
|
session = dhcrypto.Session()
|
||||||
|
self.sessions[session_id] = session
|
||||||
|
|
||||||
|
if algorithm == "plain":
|
||||||
|
return (GLib.Variant('s', 'plain'), session_path)
|
||||||
|
if algorithm == "dh-ietf1024-sha256-aes128-cbc-pkcs7":
|
||||||
|
pubkey = int.from_bytes(input, 'big')
|
||||||
|
session.set_client_public_key(pubkey)
|
||||||
|
return (GLib.Variant('ay', session.get_my_public_key()), session_path)
|
||||||
|
|
||||||
|
# if algorithm != "plain":
|
||||||
|
# # i hate this
|
||||||
|
# exc = NotSupported("org.freedesktop.DBus.Error.NotSupported")
|
||||||
|
# type(exc).__name__ = 'org.freedesktop.DBus.Error.NotSupported'
|
||||||
|
# raise exc
|
||||||
|
raise "you're not supposed to be here."
|
||||||
|
return (None, None)
|
||||||
|
|
||||||
def LockService(self):
|
def LockService(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def SearchCollections(self, fields):
|
def SearchItems(self, attributes):
|
||||||
print('SearchCollections', fields)
|
print('SearchItems', attributes)
|
||||||
return ([], [])
|
results = hiq.query(**attributes)
|
||||||
|
print('SearchItems#results', results)
|
||||||
|
|
||||||
|
unlocked = []
|
||||||
|
locked = []
|
||||||
|
|
||||||
|
for res in results:
|
||||||
|
objID = None
|
||||||
|
if res in self.keycache:
|
||||||
|
objID = self.keycache.index(res)
|
||||||
|
else:
|
||||||
|
objID = len(self.keycache)
|
||||||
|
self.keycache.append(res)
|
||||||
|
objPath = f'/org/freedesktop/secrets/items/{str(objID)}'
|
||||||
|
if res.has_secret():
|
||||||
|
locked.append(objPath)
|
||||||
|
else:
|
||||||
|
unlocked.append(objPath)
|
||||||
|
|
||||||
|
print('SearchItems#locked', locked)
|
||||||
|
print('SearchItems#unlocked', unlocked)
|
||||||
|
return (locked, unlocked)
|
||||||
|
|
||||||
|
def GetSecrets(self, items, session_path):
|
||||||
|
print('GetSecrets', items, session_path)
|
||||||
|
ret = {}
|
||||||
|
for path in items:
|
||||||
|
key = self.keycache[int(path.replace('/org/freedesktop/secrets/items/', ''))]
|
||||||
|
session = self.sessions[int(session_path.replace(SESSIONS_NS, ''))]
|
||||||
|
|
||||||
|
dec_key = hiq.query_decrypt(**key.to_query())
|
||||||
|
# uhhhhhh himitsu can return multiple keys here, narrow it down to whichever has a secret
|
||||||
|
dec_key = next(filter(lambda k: k.has_secret(), dec_key))
|
||||||
|
secret = dec_key.get_secret().encode('utf-8')
|
||||||
|
|
||||||
|
# handle encryption if necessary
|
||||||
|
iv, enc = session.encrypt(secret)
|
||||||
|
|
||||||
|
ret[path] = (session_path, iv, enc, 'text/plain')
|
||||||
|
print('GetSecrets#ret', ret)
|
||||||
|
return ret
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def Collections(self):
|
def Collections(self):
|
||||||
|
@ -123,6 +212,7 @@ class Service(object):
|
||||||
CollectionChanged = signal()
|
CollectionChanged = signal()
|
||||||
|
|
||||||
def entrypoint():
|
def entrypoint():
|
||||||
|
print('Starting kakushi...')
|
||||||
bus.publish(
|
bus.publish(
|
||||||
"org.freedesktop.secrets", Service(),
|
"org.freedesktop.secrets", Service(),
|
||||||
('collection/default', Collection())
|
('collection/default', Collection())
|
||||||
|
|
Loading…
Reference in a new issue