forked from AlessandroZ/LaZagne
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
new module - retrieve Keepass password from memory
- Loading branch information
1 parent
72b9efc
commit daf0f25
Showing
8 changed files
with
1,425 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
# -*- coding: utf-8 -*- | ||
import io | ||
from contextlib import contextmanager | ||
|
||
from common import read_signature | ||
# from kdb3 import KDB3Reader, KDB3_SIGNATURE | ||
from kdb4 import KDB4Reader, KDB4_SIGNATURE | ||
|
||
BASE_SIGNATURE = 0x9AA2D903 | ||
|
||
_kdb_readers = { | ||
# KDB3_SIGNATURE[1]: KDB3Reader, | ||
#0xB54BFB66: KDB4Reader, # pre2.x may work, untested | ||
KDB4_SIGNATURE[1]: KDB4Reader, | ||
} | ||
|
||
@contextmanager | ||
def open(filename, **credentials): | ||
""" | ||
A contextmanager to open the KeePass file with `filename`. Use a `password` | ||
and/or `keyfile` named argument for decryption. | ||
Files are identified using their signature and a reader suitable for | ||
the file format is intialized and returned. | ||
Note: `keyfile` is currently not supported for v3 KeePass files. | ||
""" | ||
kdb = None | ||
try: | ||
with io.open(filename, 'rb') as stream: | ||
signature = read_signature(stream) | ||
cls = get_kdb_reader(signature) | ||
kdb = cls(stream, **credentials) | ||
yield kdb | ||
kdb.close() | ||
except: | ||
if kdb: kdb.close() | ||
raise | ||
|
||
def add_kdb_reader(sub_signature, cls): | ||
""" | ||
Add or overwrite the class used to process a KeePass file. | ||
KeePass uses two signatures to identify files. The base signature is | ||
always `0x9AA2D903`. The second/sub signature varies. For example | ||
KeePassX uses the v3 sub signature `0xB54BFB65` and KeePass2 the v4 sub | ||
signature `0xB54BFB67`. | ||
Use this method to add or replace a class by givin a `sub_signature` as | ||
integer and a class, which should be a subclass of | ||
`keepass.common.KDBFile`. | ||
""" | ||
_kdb_readers[sub_signature] = cls | ||
|
||
def get_kdb_reader(signature): | ||
""" | ||
Retrieve the class used to process a KeePass file by `signature`, which | ||
is a a tuple or list with two elements. The first being the base signature | ||
and the second the sub signature as integers. | ||
""" | ||
if signature[0] != BASE_SIGNATURE: | ||
raise IOError('Unknown base signature.') | ||
|
||
if signature[1] not in _kdb_readers: | ||
raise IOError('Unknown sub signature.') | ||
|
||
return _kdb_readers[signature[1]] | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,278 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
# file header | ||
|
||
class HeaderDictionary(dict): | ||
""" | ||
A dictionary on steroids for comfortable header field storage and | ||
manipulation. | ||
Header fields must be defined in the `fields` property before filling the | ||
dictionary with data. The `fields` property is a simple dictionary, where | ||
keys are field names (string) and values are field ids (int):: | ||
>>> h.fields['rounds'] = 4 | ||
Now you can set and get values using the field id or the field name | ||
interchangeably:: | ||
>>> h[4] = 3000 | ||
>>> print h['rounds'] | ||
3000 | ||
>>> h['rounds'] = 6000 | ||
>>> print h[4] | ||
6000 | ||
It is also possible to get and set data using the field name as an | ||
attribute:: | ||
>>> h.rounds = 9000 | ||
>>> print h[4] | ||
9000 | ||
>>> print h.rounds | ||
9000 | ||
For some fields it is more comfortable to unpack their byte value into | ||
a numeric or character value (eg. the transformation rounds). For those | ||
fields add a format string to the `fmt` dictionary. Use the field id as | ||
key:: | ||
>>> h.fmt[4] = '<q' | ||
Continue setting the value as before if you have it as a number and if you | ||
need it as a number, get it like before. Only when you have the packed value | ||
use a different interface:: | ||
>>> h.b.rounds = '\x70\x17\x00\x00\x00\x00\x00\x00' | ||
>>> print h.b.rounds | ||
'\x70\x17\x00\x00\x00\x00\x00\x00' | ||
>>> print h.rounds | ||
6000 | ||
The `b` (binary?) attribute is a special way to set and get data in its | ||
packed format, while the usual attribute or dictionary access allows | ||
setting and getting a numeric value:: | ||
>>> h.rounds = 3000 | ||
>>> print h.b.rounds | ||
'\xb8\x0b\x00\x00\x00\x00\x00\x00' | ||
>>> print h.rounds | ||
3000 | ||
""" | ||
fields = {} | ||
fmt = {} | ||
|
||
def __init__(self, *args): | ||
dict.__init__(self, *args) | ||
|
||
def __getitem__(self, key): | ||
if isinstance(key, int): | ||
return dict.__getitem__(self, key) | ||
else: | ||
return dict.__getitem__(self, self.fields[key]) | ||
|
||
def __setitem__(self, key, val): | ||
if isinstance(key, int): | ||
dict.__setitem__(self, key, val) | ||
else: | ||
dict.__setitem__(self, self.fields[key], val) | ||
|
||
def __getattr__(self, key): | ||
class wrap(object): | ||
def __init__(self, d): | ||
object.__setattr__(self, 'd', d) | ||
def __getitem__(self, key): | ||
fmt = self.d.fmt.get(self.d.fields.get(key, key)) | ||
if fmt: return struct.pack(fmt, self.d[key]) | ||
else: return self.d[key] | ||
__getattr__ = __getitem__ | ||
def __setitem__(self, key, val): | ||
fmt = self.d.fmt.get(self.d.fields.get(key, key)) | ||
if fmt: self.d[key] = struct.unpack(fmt, val)[0] | ||
else: self.d[key] = val | ||
__setattr__ = __setitem__ | ||
|
||
if key == 'b': | ||
return wrap(self) | ||
try: | ||
return self.__getitem__(key) | ||
except KeyError: | ||
raise AttributeError(key) | ||
|
||
def __setattr__(self, key, val): | ||
try: | ||
return self.__setitem__(key, val) | ||
except KeyError: | ||
return dict.__setattr__(self, key, val) | ||
|
||
|
||
# file baseclass | ||
|
||
import io | ||
from crypto import sha256 | ||
|
||
class KDBFile(object): | ||
def __init__(self, stream=None, **credentials): | ||
# list of hashed credentials (pre-transformation) | ||
self.keys = [] | ||
self.add_credentials(**credentials) | ||
|
||
# the buffer containing the decrypted/decompressed payload from a file | ||
self.in_buffer = None | ||
# the buffer filled with data for writing back to a file before | ||
# encryption/compression | ||
self.out_buffer = None | ||
# position in the `in_buffer` where the payload begins | ||
self.header_length = None | ||
# decryption success flag, set this to true upon verification of the | ||
# encryption masterkey. if this is True `in_buffer` must contain | ||
# clear data. | ||
self.opened = False | ||
|
||
# the raw/basic file handle, expect it to be closed after __init__! | ||
if stream is not None: | ||
if not isinstance(stream, io.IOBase): | ||
raise TypeError('Stream does not have the buffer interface.') | ||
self.read_from(stream) | ||
|
||
def read_from(self, stream): | ||
if not (isinstance(stream, io.IOBase) or isinstance(stream, file)): | ||
raise TypeError('Stream does not have the buffer interface.') | ||
self._read_header(stream) | ||
self._decrypt(stream) | ||
|
||
def _read_header(self, stream): | ||
raise NotImplementedError('The _read_header method was not ' | ||
'implemented propertly.') | ||
|
||
def _decrypt(self, stream): | ||
self._make_master_key() | ||
# move read pointer beyond the file header | ||
if self.header_length is None: | ||
raise IOError('Header length unknown. Parse the header first!') | ||
stream.seek(self.header_length) | ||
|
||
def write_to(self, stream): | ||
raise NotImplementedError('The write_to() method was not implemented.') | ||
|
||
def add_credentials(self, **credentials): | ||
if credentials.has_key('password'): | ||
self.add_key_hash(sha256(credentials['password'])) | ||
if credentials.has_key('keyfile'): | ||
self.add_key_hash(load_keyfile(credentials['keyfile'])) | ||
|
||
def clear_credentials(self): | ||
"""Remove all previously set encryption key hashes.""" | ||
self.keys = [] | ||
|
||
def add_key_hash(self, key_hash): | ||
""" | ||
Add an encryption key hash, can be a hashed password or a hashed | ||
keyfile. Two things are important: must be SHA256 hashes and sequence is | ||
important: first password if any, second key file if any. | ||
""" | ||
if key_hash is not None: | ||
self.keys.append(key_hash) | ||
|
||
def _make_master_key(self): | ||
if len(self.keys) == 0: | ||
raise IndexError('No credentials found.') | ||
|
||
def close(self): | ||
if self.in_buffer: | ||
self.in_buffer.close() | ||
|
||
def read(self, n=-1): | ||
""" | ||
Read the decrypted and uncompressed data after the file header. | ||
For example, in KDB4 this would be plain, utf-8 xml. | ||
Note that this is the source data for the lxml.objectify element tree | ||
at `self.obj_root`. Any changes made to the parsed element tree will | ||
NOT be reflected in that data stream! Use `self.pretty_print` to get | ||
XML output from the element tree. | ||
""" | ||
if self.in_buffer: | ||
return self.in_buffer.read(n) | ||
|
||
def seek(self, offset, whence=io.SEEK_SET): | ||
if self.in_buffer: | ||
return self.in_buffer.seek(offset, whence) | ||
|
||
def tell(self): | ||
if self.in_buffer: | ||
return self.in_buffer.tell() | ||
|
||
|
||
# loading keyfiles | ||
|
||
import base64 | ||
import hashlib | ||
from lxml import etree | ||
|
||
def load_keyfile(filename): | ||
try: | ||
return load_xml_keyfile(filename) | ||
except: | ||
pass | ||
try: | ||
return load_plain_keyfile(filename) | ||
except: | ||
pass | ||
|
||
def load_xml_keyfile(filename): | ||
""" | ||
// Sample XML file: | ||
// <?xml version="1.0" encoding="utf-8"?> | ||
// <KeyFile> | ||
// <Meta> | ||
// <Version>1.00</Version> | ||
// </Meta> | ||
// <Key> | ||
// <Data>ySFoKuCcJblw8ie6RkMBdVCnAf4EedSch7ItujK6bmI=</Data> | ||
// </Key> | ||
// </KeyFile> | ||
""" | ||
with open(filename, 'r') as f: | ||
# ignore meta, currently there is only version "1.00" | ||
tree = etree.parse(f).getroot() | ||
# read text from key, data and convert from base64 | ||
return base64.b64decode(tree.find('Key/Data').text) | ||
raise IOError('Could not parse XML keyfile.') | ||
|
||
def load_plain_keyfile(filename): | ||
""" | ||
A "plain" keyfile is a file containing only the key. | ||
Any other file (JPEG, MP3, ...) can also be used as keyfile. | ||
""" | ||
with open(filename, 'rb') as f: | ||
key = f.read() | ||
# if the length is 32 bytes we assume it is the key | ||
if len(key) == 32: | ||
return key | ||
# if the length is 64 bytes we assume the key is hex encoded | ||
if len(key) == 64: | ||
return key.decode('hex') | ||
# anything else may be a file to hash for the key | ||
return sha256(key) | ||
raise IOError('Could not read keyfile.') | ||
|
||
import struct | ||
|
||
def stream_unpack(stream, offset, length, typecode='I'): | ||
if offset is not None: | ||
stream.seek(offset) | ||
data = stream.read(length) | ||
return struct.unpack('<'+typecode, data)[0] | ||
|
||
def read_signature(stream): | ||
sig1 = stream_unpack(stream, 0, 4) | ||
sig2 = stream_unpack(stream, None, 4) | ||
#ver_minor = stream_unpack(stream, None, 2, 'h') | ||
#ver_major = stream_unpack(stream, None, 2, 'h') | ||
#return (sig1, sig2, ver_major, ver_minor) | ||
return (sig1, sig2) | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
# -*- coding: utf-8 -*- | ||
import hashlib | ||
import struct | ||
from Crypto.Cipher import AES | ||
from pureSalsa20 import Salsa20 | ||
|
||
AES_BLOCK_SIZE = 16 | ||
|
||
def sha256(s): | ||
"""Return SHA256 digest of the string `s`.""" | ||
return hashlib.sha256(s).digest() | ||
|
||
def transform_key(key, seed, rounds): | ||
"""Transform `key` with `seed` `rounds` times using AES ECB.""" | ||
# create transform cipher with transform seed | ||
cipher = AES.new(seed, AES.MODE_ECB) | ||
# transform composite key rounds times | ||
for n in range(0, rounds): | ||
key = cipher.encrypt(key) | ||
# return hash of transformed key | ||
return sha256(key) | ||
|
||
def aes_cbc_decrypt(data, key, enc_iv): | ||
"""Decrypt and return `data` with AES CBC.""" | ||
cipher = AES.new(key, AES.MODE_CBC, enc_iv) | ||
return cipher.decrypt(data) | ||
|
||
def aes_cbc_encrypt(data, key, enc_iv): | ||
cipher = AES.new(key, AES.MODE_CBC, enc_iv) | ||
return cipher.encrypt(data) | ||
|
||
def unpad(data): | ||
extra = ord(data[-1]) | ||
return data[:len(data)-extra] | ||
|
||
def pad(s): | ||
n = AES_BLOCK_SIZE - len(s) % AES_BLOCK_SIZE | ||
return s + n * struct.pack('b', n) | ||
|
||
def xor(aa, bb): | ||
"""Return a bytearray of a bytewise XOR of `aa` and `bb`.""" | ||
result = bytearray() | ||
for a, b in zip(bytearray(aa), bytearray(bb)): | ||
result.append(a ^ b) | ||
return result |
Oops, something went wrong.