├── requirements.txt ├── tests ├── old1.kdb ├── sample5.kdb ├── sample6.kdb ├── sample1.kdbx ├── sample2.kdbx ├── sample3.kdbx ├── sample4.kdbx ├── sample7_kpx.kdb ├── sample3_keyfile.exe ├── sample2_keyfile.key └── tests.py ├── README.md ├── .gitignore ├── LICENSE.txt └── keepass ├── crypto.py ├── __init__.py ├── kdb3.py ├── hbio.py ├── common.py ├── pureSalsa20.py └── kdb4.py /requirements.txt: -------------------------------------------------------------------------------- 1 | lxml==3.2.1 2 | nose==1.3.0 3 | pycrypto==2.6 -------------------------------------------------------------------------------- /tests/old1.kdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fdemmer/libkeepass/HEAD/tests/old1.kdb -------------------------------------------------------------------------------- /tests/sample5.kdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fdemmer/libkeepass/HEAD/tests/sample5.kdb -------------------------------------------------------------------------------- /tests/sample6.kdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fdemmer/libkeepass/HEAD/tests/sample6.kdb -------------------------------------------------------------------------------- /tests/sample1.kdbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fdemmer/libkeepass/HEAD/tests/sample1.kdbx -------------------------------------------------------------------------------- /tests/sample2.kdbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fdemmer/libkeepass/HEAD/tests/sample2.kdbx -------------------------------------------------------------------------------- /tests/sample3.kdbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fdemmer/libkeepass/HEAD/tests/sample3.kdbx -------------------------------------------------------------------------------- /tests/sample4.kdbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fdemmer/libkeepass/HEAD/tests/sample4.kdbx -------------------------------------------------------------------------------- /tests/sample7_kpx.kdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fdemmer/libkeepass/HEAD/tests/sample7_kpx.kdb -------------------------------------------------------------------------------- /tests/sample3_keyfile.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fdemmer/libkeepass/HEAD/tests/sample3_keyfile.exe -------------------------------------------------------------------------------- /tests/sample2_keyfile.key: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1.00 5 | 6 | 7 | pcKkNT7lqDbaciEq4x+7ny9KRoj9BZuE7uJma1uqxcc= 8 | 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | libkeepass 2 | ========== 3 | 4 | Python module to read KeePass 1.x/KeePassX (v3) and KeePass 2.x (v4) files. 5 | 6 | The torch passed on... 7 | ---------------------- 8 | 9 | This library is now maintained by Lukas Köll at https://github.com/phpwutz/libkeepass! 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | #Translations 24 | *.mo 25 | 26 | # tmp files 27 | *~ 28 | 29 | #Mr Developer 30 | .mr.developer.cfg 31 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | python-keepass is Copyright (C) 2012 Brett Viren. 2 | libkeepass is Copyright (c) 2013 Florian Demmer. 3 | 4 | 5 | This code is free software; you can redistribute it and/or modify it 6 | under the terms of the GNU General Public License as published by the 7 | Free Software Foundation; either version 2, or (at your option) any 8 | later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | For the complete terms of the GNU General Public License, please see this URL: 16 | http://www.gnu.org/licenses/gpl-2.0.html 17 | 18 | 19 | pureSalsa20.py is Copyright (c) Larry Bugbee and provided "free for any use". 20 | 21 | -------------------------------------------------------------------------------- /keepass/crypto.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import hashlib 3 | import struct 4 | from Crypto.Cipher import AES 5 | from pureSalsa20 import Salsa20 6 | 7 | AES_BLOCK_SIZE = 16 8 | 9 | def sha256(s): 10 | """Return SHA256 digest of the string `s`.""" 11 | return hashlib.sha256(s).digest() 12 | 13 | def transform_key(key, seed, rounds): 14 | """Transform `key` with `seed` `rounds` times using AES ECB.""" 15 | # create transform cipher with transform seed 16 | cipher = AES.new(seed, AES.MODE_ECB) 17 | # transform composite key rounds times 18 | for n in range(0, rounds): 19 | key = cipher.encrypt(key) 20 | # return hash of transformed key 21 | return sha256(key) 22 | 23 | def aes_cbc_decrypt(data, key, enc_iv): 24 | """Decrypt and return `data` with AES CBC.""" 25 | cipher = AES.new(key, AES.MODE_CBC, enc_iv) 26 | return cipher.decrypt(data) 27 | 28 | def aes_cbc_encrypt(data, key, enc_iv): 29 | cipher = AES.new(key, AES.MODE_CBC, enc_iv) 30 | return cipher.encrypt(data) 31 | 32 | def unpad(data): 33 | extra = ord(data[-1]) 34 | return data[:len(data)-extra] 35 | 36 | def pad(s): 37 | n = AES_BLOCK_SIZE - len(s) % AES_BLOCK_SIZE 38 | return s + n * struct.pack('b', n) 39 | 40 | def xor(aa, bb): 41 | """Return a bytearray of a bytewise XOR of `aa` and `bb`.""" 42 | result = bytearray() 43 | for a, b in zip(bytearray(aa), bytearray(bb)): 44 | result.append(a ^ b) 45 | return result 46 | -------------------------------------------------------------------------------- /keepass/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import io 3 | from contextlib import contextmanager 4 | 5 | from common import read_signature 6 | from kdb3 import KDB3Reader, KDB3_SIGNATURE 7 | from kdb4 import KDB4Reader, KDB4_SIGNATURE 8 | 9 | BASE_SIGNATURE = 0x9AA2D903 10 | 11 | _kdb_readers = { 12 | KDB3_SIGNATURE[1]: KDB3Reader, 13 | #0xB54BFB66: KDB4Reader, # pre2.x may work, untested 14 | KDB4_SIGNATURE[1]: KDB4Reader, 15 | } 16 | 17 | @contextmanager 18 | def open(filename, **credentials): 19 | """ 20 | A contextmanager to open the KeePass file with `filename`. Use a `password` 21 | and/or `keyfile` named argument for decryption. 22 | 23 | Files are identified using their signature and a reader suitable for 24 | the file format is intialized and returned. 25 | 26 | Note: `keyfile` is currently not supported for v3 KeePass files. 27 | """ 28 | kdb = None 29 | try: 30 | with io.open(filename, 'rb') as stream: 31 | signature = read_signature(stream) 32 | cls = get_kdb_reader(signature) 33 | kdb = cls(stream, **credentials) 34 | yield kdb 35 | kdb.close() 36 | except: 37 | if kdb: kdb.close() 38 | raise 39 | 40 | def add_kdb_reader(sub_signature, cls): 41 | """ 42 | Add or overwrite the class used to process a KeePass file. 43 | 44 | KeePass uses two signatures to identify files. The base signature is 45 | always `0x9AA2D903`. The second/sub signature varies. For example 46 | KeePassX uses the v3 sub signature `0xB54BFB65` and KeePass2 the v4 sub 47 | signature `0xB54BFB67`. 48 | 49 | Use this method to add or replace a class by givin a `sub_signature` as 50 | integer and a class, which should be a subclass of 51 | `keepass.common.KDBFile`. 52 | """ 53 | _kdb_readers[sub_signature] = cls 54 | 55 | def get_kdb_reader(signature): 56 | """ 57 | Retrieve the class used to process a KeePass file by `signature`, which 58 | is a a tuple or list with two elements. The first being the base signature 59 | and the second the sub signature as integers. 60 | """ 61 | if signature[0] != BASE_SIGNATURE: 62 | raise IOError('Unknown base signature.') 63 | 64 | if signature[1] not in _kdb_readers: 65 | raise IOError('Unknown sub signature.') 66 | 67 | return _kdb_readers[signature[1]] 68 | 69 | -------------------------------------------------------------------------------- /keepass/kdb3.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import io 3 | import uuid 4 | import zlib 5 | import struct 6 | import hashlib 7 | import base64 8 | 9 | from crypto import xor, sha256, aes_cbc_decrypt 10 | from crypto import transform_key, unpad 11 | 12 | from common import load_keyfile, stream_unpack 13 | from common import KDBFile, HeaderDictionary 14 | 15 | 16 | KDB3_SIGNATURE = (0x9AA2D903, 0xB54BFB65) 17 | 18 | 19 | class KDB3Header(HeaderDictionary): 20 | fields = { 21 | # encryption type/flag 22 | 'Flags': 0, 23 | 'Version': 1, 24 | # seed to hash the transformed master key 25 | 'MasterSeed': 2, 26 | 'EncryptionIV': 3, 27 | # fields describing data structure 28 | 'Groups': 4, 29 | 'Entries': 5, 30 | # hash of the whole decrypted data 31 | 'ContentHash': 6, 32 | # seed for key transformation 33 | 'MasterSeed2': 7, 34 | # number of transformation rounds 35 | 'KeyEncRounds': 8, 36 | } 37 | 38 | fmt = { 0: ' 8: 74 | break 75 | 76 | # this is impossible, as long as noone messes with self.header.lengths 77 | if self.header_length != stream.tell(): 78 | raise IOError('Unexpected header length! What did you do!?') 79 | 80 | def _decrypt(self, stream): 81 | super(KDB3File, self)._decrypt(stream) 82 | 83 | data = aes_cbc_decrypt(stream.read(), self.master_key, 84 | self.header.EncryptionIV) 85 | data = unpad(data) 86 | 87 | if self.header.ContentHash == sha256(data): 88 | # put data in bytes io 89 | self.in_buffer = io.BytesIO(data) 90 | # set successful decryption flag 91 | self.opened = True 92 | else: 93 | raise IOError('Master key invalid.') 94 | 95 | def _make_master_key(self): 96 | """ 97 | Make the master key by (1) combining the credentials to create 98 | a composite hash, (2) transforming the hash using the transform seed 99 | for a specific number of rounds and (3) finally hashing the result in 100 | combination with the master seed. 101 | """ 102 | super(KDB3File, self)._make_master_key() 103 | #print "masterkey:", ''.join(self.keys).encode('hex') 104 | #composite = sha256(''.join(self.keys)) 105 | #TODO python-keepass does not support keyfiles, there seems to be a 106 | # different way to hash those keys in kdb3 107 | composite = self.keys[0] 108 | tkey = transform_key(composite, 109 | self.header.MasterSeed2, 110 | self.header.KeyEncRounds) 111 | self.master_key = sha256(self.header.MasterSeed + tkey) 112 | 113 | 114 | class KDBExtension: 115 | """ 116 | The KDB3 payload is a ... #TODO ... 117 | """ 118 | def __init__(self): 119 | pass 120 | 121 | class KDB3Reader(KDB3File, KDBExtension): 122 | def __init__(self, stream=None, **credentials): 123 | KDB3File.__init__(self, stream, **credentials) 124 | KDBExtension.__init__(self) 125 | 126 | -------------------------------------------------------------------------------- /keepass/hbio.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import io 3 | import struct 4 | import hashlib 5 | 6 | # default from KeePass2 source 7 | BLOCK_LENGTH = 1024*1024 8 | #HEADER_LENGTH = 4+32+4 9 | 10 | def read_int(stream, length): 11 | try: 12 | return struct.unpack(' 0: 67 | data = block_stream.read(length) 68 | if hashlib.sha256(data).digest() == bhash: 69 | return data 70 | else: 71 | raise IOError('Block hash mismatch error.') 72 | return bytes() 73 | 74 | def write_block_stream(self, stream, block_length=BLOCK_LENGTH): 75 | """ 76 | Write all data in this buffer, starting at stream position 0, formatted 77 | in hashed blocks to the given `stream`. 78 | 79 | For example, writing data from one file into another as hashed blocks:: 80 | 81 | # create new hashed block io without input stream or data 82 | hb = HashedBlockIO() 83 | # read from a file, write into the empty hb 84 | with open('sample.dat', 'rb') as infile: 85 | hb.write(infile.read()) 86 | # write from the hb into a new file 87 | with open('hb_sample.dat', 'w') as outfile: 88 | hb.write_block_stream(outfile) 89 | """ 90 | if not (isinstance(stream, io.IOBase) or isinstance(stream, file)): 91 | raise TypeError('Stream does not have the buffer interface.') 92 | index = 0 93 | self.seek(0) 94 | while True: 95 | data = self.read(block_length) 96 | if data: 97 | stream.write(struct.pack('>> h.fields['rounds'] = 4 15 | 16 | Now you can set and get values using the field id or the field name 17 | interchangeably:: 18 | 19 | >>> h[4] = 3000 20 | >>> print h['rounds'] 21 | 3000 22 | >>> h['rounds'] = 6000 23 | >>> print h[4] 24 | 6000 25 | 26 | It is also possible to get and set data using the field name as an 27 | attribute:: 28 | 29 | >>> h.rounds = 9000 30 | >>> print h[4] 31 | 9000 32 | >>> print h.rounds 33 | 9000 34 | 35 | For some fields it is more comfortable to unpack their byte value into 36 | a numeric or character value (eg. the transformation rounds). For those 37 | fields add a format string to the `fmt` dictionary. Use the field id as 38 | key:: 39 | 40 | >>> h.fmt[4] = '>> h.b.rounds = '\x70\x17\x00\x00\x00\x00\x00\x00' 47 | >>> print h.b.rounds 48 | '\x70\x17\x00\x00\x00\x00\x00\x00' 49 | >>> print h.rounds 50 | 6000 51 | 52 | The `b` (binary?) attribute is a special way to set and get data in its 53 | packed format, while the usual attribute or dictionary access allows 54 | setting and getting a numeric value:: 55 | 56 | >>> h.rounds = 3000 57 | >>> print h.b.rounds 58 | '\xb8\x0b\x00\x00\x00\x00\x00\x00' 59 | >>> print h.rounds 60 | 3000 61 | 62 | """ 63 | fields = {} 64 | fmt = {} 65 | 66 | def __init__(self, *args): 67 | dict.__init__(self, *args) 68 | 69 | def __getitem__(self, key): 70 | if isinstance(key, int): 71 | return dict.__getitem__(self, key) 72 | else: 73 | return dict.__getitem__(self, self.fields[key]) 74 | 75 | def __setitem__(self, key, val): 76 | if isinstance(key, int): 77 | dict.__setitem__(self, key, val) 78 | else: 79 | dict.__setitem__(self, self.fields[key], val) 80 | 81 | def __getattr__(self, key): 82 | class wrap(object): 83 | def __init__(self, d): 84 | object.__setattr__(self, 'd', d) 85 | def __getitem__(self, key): 86 | fmt = self.d.fmt.get(self.d.fields.get(key, key)) 87 | if fmt: return struct.pack(fmt, self.d[key]) 88 | else: return self.d[key] 89 | __getattr__ = __getitem__ 90 | def __setitem__(self, key, val): 91 | fmt = self.d.fmt.get(self.d.fields.get(key, key)) 92 | if fmt: self.d[key] = struct.unpack(fmt, val)[0] 93 | else: self.d[key] = val 94 | __setattr__ = __setitem__ 95 | 96 | if key == 'b': 97 | return wrap(self) 98 | try: 99 | return self.__getitem__(key) 100 | except KeyError: 101 | raise AttributeError(key) 102 | 103 | def __setattr__(self, key, val): 104 | try: 105 | return self.__setitem__(key, val) 106 | except KeyError: 107 | return dict.__setattr__(self, key, val) 108 | 109 | 110 | # file baseclass 111 | 112 | import io 113 | from crypto import sha256 114 | 115 | class KDBFile(object): 116 | def __init__(self, stream=None, **credentials): 117 | # list of hashed credentials (pre-transformation) 118 | self.keys = [] 119 | self.add_credentials(**credentials) 120 | 121 | # the buffer containing the decrypted/decompressed payload from a file 122 | self.in_buffer = None 123 | # the buffer filled with data for writing back to a file before 124 | # encryption/compression 125 | self.out_buffer = None 126 | # position in the `in_buffer` where the payload begins 127 | self.header_length = None 128 | # decryption success flag, set this to true upon verification of the 129 | # encryption masterkey. if this is True `in_buffer` must contain 130 | # clear data. 131 | self.opened = False 132 | 133 | # the raw/basic file handle, expect it to be closed after __init__! 134 | if stream is not None: 135 | if not isinstance(stream, io.IOBase): 136 | raise TypeError('Stream does not have the buffer interface.') 137 | self.read_from(stream) 138 | 139 | def read_from(self, stream): 140 | if not (isinstance(stream, io.IOBase) or isinstance(stream, file)): 141 | raise TypeError('Stream does not have the buffer interface.') 142 | self._read_header(stream) 143 | self._decrypt(stream) 144 | 145 | def _read_header(self, stream): 146 | raise NotImplementedError('The _read_header method was not ' 147 | 'implemented propertly.') 148 | 149 | def _decrypt(self, stream): 150 | self._make_master_key() 151 | # move read pointer beyond the file header 152 | if self.header_length is None: 153 | raise IOError('Header length unknown. Parse the header first!') 154 | stream.seek(self.header_length) 155 | 156 | def write_to(self, stream): 157 | raise NotImplementedError('The write_to() method was not implemented.') 158 | 159 | def add_credentials(self, **credentials): 160 | if credentials.has_key('password'): 161 | self.add_key_hash(sha256(credentials['password'])) 162 | if credentials.has_key('keyfile'): 163 | self.add_key_hash(load_keyfile(credentials['keyfile'])) 164 | 165 | def clear_credentials(self): 166 | """Remove all previously set encryption key hashes.""" 167 | self.keys = [] 168 | 169 | def add_key_hash(self, key_hash): 170 | """ 171 | Add an encryption key hash, can be a hashed password or a hashed 172 | keyfile. Two things are important: must be SHA256 hashes and sequence is 173 | important: first password if any, second key file if any. 174 | """ 175 | if key_hash is not None: 176 | self.keys.append(key_hash) 177 | 178 | def _make_master_key(self): 179 | if len(self.keys) == 0: 180 | raise IndexError('No credentials found.') 181 | 182 | def close(self): 183 | if self.in_buffer: 184 | self.in_buffer.close() 185 | 186 | def read(self, n=-1): 187 | """ 188 | Read the decrypted and uncompressed data after the file header. 189 | For example, in KDB4 this would be plain, utf-8 xml. 190 | 191 | Note that this is the source data for the lxml.objectify element tree 192 | at `self.obj_root`. Any changes made to the parsed element tree will 193 | NOT be reflected in that data stream! Use `self.pretty_print` to get 194 | XML output from the element tree. 195 | """ 196 | if self.in_buffer: 197 | return self.in_buffer.read(n) 198 | 199 | def seek(self, offset, whence=io.SEEK_SET): 200 | if self.in_buffer: 201 | return self.in_buffer.seek(offset, whence) 202 | 203 | def tell(self): 204 | if self.in_buffer: 205 | return self.in_buffer.tell() 206 | 207 | 208 | # loading keyfiles 209 | 210 | import base64 211 | import hashlib 212 | from lxml import etree 213 | 214 | def load_keyfile(filename): 215 | try: 216 | return load_xml_keyfile(filename) 217 | except: 218 | pass 219 | try: 220 | return load_plain_keyfile(filename) 221 | except: 222 | pass 223 | 224 | def load_xml_keyfile(filename): 225 | """ 226 | // Sample XML file: 227 | // 228 | // 229 | // 230 | // 1.00 231 | // 232 | // 233 | // ySFoKuCcJblw8ie6RkMBdVCnAf4EedSch7ItujK6bmI= 234 | // 235 | // 236 | """ 237 | with open(filename, 'r') as f: 238 | # ignore meta, currently there is only version "1.00" 239 | tree = etree.parse(f).getroot() 240 | # read text from key, data and convert from base64 241 | return base64.b64decode(tree.find('Key/Data').text) 242 | raise IOError('Could not parse XML keyfile.') 243 | 244 | def load_plain_keyfile(filename): 245 | """ 246 | A "plain" keyfile is a file containing only the key. 247 | Any other file (JPEG, MP3, ...) can also be used as keyfile. 248 | """ 249 | with open(filename, 'rb') as f: 250 | key = f.read() 251 | # if the length is 32 bytes we assume it is the key 252 | if len(key) == 32: 253 | return key 254 | # if the length is 64 bytes we assume the key is hex encoded 255 | if len(key) == 64: 256 | return key.decode('hex') 257 | # anything else may be a file to hash for the key 258 | return sha256(key) 259 | raise IOError('Could not read keyfile.') 260 | 261 | # 262 | 263 | import struct 264 | 265 | def stream_unpack(stream, offset, length, typecode='I'): 266 | if offset is not None: 267 | stream.seek(offset) 268 | data = stream.read(length) 269 | return struct.unpack('<'+typecode, data)[0] 270 | 271 | def read_signature(stream): 272 | sig1 = stream_unpack(stream, 0, 4) 273 | sig2 = stream_unpack(stream, None, 4) 274 | #ver_minor = stream_unpack(stream, None, 2, 'h') 275 | #ver_major = stream_unpack(stream, None, 2, 'h') 276 | #return (sig1, sig2, ver_major, ver_minor) 277 | return (sig1, sig2) 278 | 279 | 280 | 281 | -------------------------------------------------------------------------------- /keepass/pureSalsa20.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | """ 5 | pureSalsa20.py -- a pure Python implementation of the Salsa20 cipher 6 | ==================================================================== 7 | There are comments here by two authors about three pieces of software: 8 | comments by Larry Bugbee about 9 | Salsa20, the stream cipher by Daniel J. Bernstein 10 | (including comments about the speed of the C version) and 11 | pySalsa20, Bugbee's own Python wrapper for salsa20.c 12 | (including some references), and 13 | comments by Steve Witham about 14 | pureSalsa20, Witham's pure Python 2.5 implementation of Salsa20, 15 | which follows pySalsa20's API, and is in this file. 16 | 17 | Salsa20: a Fast Streaming Cipher (comments by Larry Bugbee) 18 | ----------------------------------------------------------- 19 | 20 | Salsa20 is a fast stream cipher written by Daniel Bernstein 21 | that basically uses a hash function and XOR making for fast 22 | encryption. (Decryption uses the same function.) Salsa20 23 | is simple and quick. 24 | 25 | Some Salsa20 parameter values... 26 | design strength 128 bits 27 | key length 128 or 256 bits, exactly 28 | IV, aka nonce 64 bits, always 29 | chunk size must be in multiples of 64 bytes 30 | 31 | Salsa20 has two reduced versions, 8 and 12 rounds each. 32 | 33 | One benchmark (10 MB): 34 | 1.5GHz PPC G4 102/97/89 MB/sec for 8/12/20 rounds 35 | AMD Athlon 2500+ 77/67/53 MB/sec for 8/12/20 rounds 36 | (no I/O and before Python GC kicks in) 37 | 38 | Salsa20 is a Phase 3 finalist in the EU eSTREAM competition 39 | and appears to be one of the fastest ciphers. It is well 40 | documented so I will not attempt any injustice here. Please 41 | see "References" below. 42 | 43 | ...and Salsa20 is "free for any use". 44 | 45 | 46 | pySalsa20: a Python wrapper for Salsa20 (Comments by Larry Bugbee) 47 | ------------------------------------------------------------------ 48 | 49 | pySalsa20.py is a simple ctypes Python wrapper. Salsa20 is 50 | as it's name implies, 20 rounds, but there are two reduced 51 | versions, 8 and 12 rounds each. Because the APIs are 52 | identical, pySalsa20 is capable of wrapping all three 53 | versions (number of rounds hardcoded), including a special 54 | version that allows you to set the number of rounds with a 55 | set_rounds() function. Compile the version of your choice 56 | as a shared library (not as a Python extension), name and 57 | install it as libsalsa20.so. 58 | 59 | Sample usage: 60 | from pySalsa20 import Salsa20 61 | s20 = Salsa20(key, IV) 62 | dataout = s20.encryptBytes(datain) # same for decrypt 63 | 64 | This is EXPERIMENTAL software and intended for educational 65 | purposes only. To make experimentation less cumbersome, 66 | pySalsa20 is also free for any use. 67 | 68 | THIS PROGRAM IS PROVIDED WITHOUT WARRANTY OR GUARANTEE OF 69 | ANY KIND. USE AT YOUR OWN RISK. 70 | 71 | Enjoy, 72 | 73 | Larry Bugbee 74 | bugbee@seanet.com 75 | April 2007 76 | 77 | 78 | References: 79 | ----------- 80 | http://en.wikipedia.org/wiki/Salsa20 81 | http://en.wikipedia.org/wiki/Daniel_Bernstein 82 | http://cr.yp.to/djb.html 83 | http://www.ecrypt.eu.org/stream/salsa20p3.html 84 | http://www.ecrypt.eu.org/stream/p3ciphers/salsa20/salsa20_p3source.zip 85 | 86 | 87 | Prerequisites for pySalsa20: 88 | ---------------------------- 89 | - Python 2.5 (haven't tested in 2.4) 90 | 91 | 92 | pureSalsa20: Salsa20 in pure Python 2.5 (comments by Steve Witham) 93 | ------------------------------------------------------------------ 94 | 95 | pureSalsa20 is the stand-alone Python code in this file. 96 | It implements the underlying Salsa20 core algorithm 97 | and emulates pySalsa20's Salsa20 class API (minus a bug(*)). 98 | 99 | pureSalsa20 is MUCH slower than libsalsa20.so wrapped with pySalsa20-- 100 | about 1/1000 the speed for Salsa20/20 and 1/500 the speed for Salsa20/8, 101 | when encrypting 64k-byte blocks on my computer. 102 | 103 | pureSalsa20 is for cases where portability is much more important than 104 | speed. I wrote it for use in a "structured" random number generator. 105 | 106 | There are comments about the reasons for this slowness in 107 | http://www.tiac.net/~sw/2010/02/PureSalsa20 108 | 109 | Sample usage: 110 | from pureSalsa20 import Salsa20 111 | s20 = Salsa20(key, IV) 112 | dataout = s20.encryptBytes(datain) # same for decrypt 113 | 114 | I took the test code from pySalsa20, added a bunch of tests including 115 | rough speed tests, and moved them into the file testSalsa20.py. 116 | To test both pySalsa20 and pureSalsa20, type 117 | python testSalsa20.py 118 | 119 | (*)The bug (?) in pySalsa20 is this. The rounds variable is global to the 120 | libsalsa20.so library and not switched when switching between instances 121 | of the Salsa20 class. 122 | s1 = Salsa20( key, IV, 20 ) 123 | s2 = Salsa20( key, IV, 8 ) 124 | In this example, 125 | with pySalsa20, both s1 and s2 will do 8 rounds of encryption. 126 | with pureSalsa20, s1 will do 20 rounds and s2 will do 8 rounds. 127 | Perhaps giving each instance its own nRounds variable, which 128 | is passed to the salsa20wordtobyte() function, is insecure. I'm not a 129 | cryptographer. 130 | 131 | pureSalsa20.py and testSalsa20.py are EXPERIMENTAL software and 132 | intended for educational purposes only. To make experimentation less 133 | cumbersome, pureSalsa20.py and testSalsa20.py are free for any use. 134 | 135 | Revisions: 136 | ---------- 137 | p3.2 Fixed bug that initialized the output buffer with plaintext! 138 | Saner ramping of nreps in speed test. 139 | Minor changes and print statements. 140 | p3.1 Took timing variability out of add32() and rot32(). 141 | Made the internals more like pySalsa20/libsalsa . 142 | Put the semicolons back in the main loop! 143 | In encryptBytes(), modify a byte array instead of appending. 144 | Fixed speed calculation bug. 145 | Used subclasses instead of patches in testSalsa20.py . 146 | Added 64k-byte messages to speed test to be fair to pySalsa20. 147 | p3 First version, intended to parallel pySalsa20 version 3. 148 | 149 | More references: 150 | ---------------- 151 | http://www.seanet.com/~bugbee/crypto/salsa20/ [pySalsa20] 152 | http://cr.yp.to/snuffle.html [The original name of Salsa20] 153 | http://cr.yp.to/snuffle/salsafamily-20071225.pdf [ Salsa20 design] 154 | http://www.tiac.net/~sw/2010/02/PureSalsa20 155 | 156 | THIS PROGRAM IS PROVIDED WITHOUT WARRANTY OR GUARANTEE OF 157 | ANY KIND. USE AT YOUR OWN RISK. 158 | 159 | Cheers, 160 | 161 | Steve Witham sw at remove-this tiac dot net 162 | February, 2010 163 | """ 164 | 165 | from array import array 166 | from struct import Struct 167 | little_u64 = Struct( "= 2**64" 221 | ctx = self.ctx 222 | ctx[ 8],ctx[ 9] = little2_i32.unpack( little_u64.pack( counter ) ) 223 | 224 | def getCounter( self ): 225 | return little_u64.unpack( little2_i32.pack( *self.ctx[ 8:10 ] ) ) [0] 226 | 227 | 228 | def setRounds(self, rounds, testing=False ): 229 | assert testing or rounds in [8, 12, 20], 'rounds must be 8, 12, 20' 230 | self.rounds = rounds 231 | 232 | 233 | def encryptBytes(self, data): 234 | assert type(data) == str, 'data must be byte string' 235 | assert self._lastChunk64, 'previous chunk not multiple of 64 bytes' 236 | lendata = len(data) 237 | munged = array( 'c', '\x00' * lendata ) 238 | for i in xrange( 0, lendata, 64 ): 239 | h = salsa20_wordtobyte( self.ctx, self.rounds, checkRounds=False ) 240 | self.setCounter( ( self.getCounter() + 1 ) % 2**64 ) 241 | # Stopping at 2^70 bytes per nonce is user's responsibility. 242 | for j in xrange( min( 64, lendata - i ) ): 243 | munged[ i+j ] = chr( ord( data[ i+j ] ) ^ ord( h[j] ) ) 244 | 245 | self._lastChunk64 = not lendata % 64 246 | return munged.tostring() 247 | 248 | decryptBytes = encryptBytes # encrypt and decrypt use same function 249 | 250 | #-------------------------------------------------------------------------- 251 | 252 | def salsa20_wordtobyte( input, nRounds=20, checkRounds=True ): 253 | """ Do nRounds Salsa20 rounds on a copy of 254 | input: list or tuple of 16 ints treated as little-endian unsigneds. 255 | Returns a 64-byte string. 256 | """ 257 | 258 | assert( type(input) in ( list, tuple ) and len(input) == 16 ) 259 | assert( not(checkRounds) or ( nRounds in [ 8, 12, 20 ] ) ) 260 | 261 | x = list( input ) 262 | 263 | def XOR( a, b ): return a ^ b 264 | ROTATE = rot32 265 | PLUS = add32 266 | 267 | for i in range( nRounds / 2 ): 268 | # These ...XOR...ROTATE...PLUS... lines are from ecrypt-linux.c 269 | # unchanged except for indents and the blank line between rounds: 270 | x[ 4] = XOR(x[ 4],ROTATE(PLUS(x[ 0],x[12]), 7)); 271 | x[ 8] = XOR(x[ 8],ROTATE(PLUS(x[ 4],x[ 0]), 9)); 272 | x[12] = XOR(x[12],ROTATE(PLUS(x[ 8],x[ 4]),13)); 273 | x[ 0] = XOR(x[ 0],ROTATE(PLUS(x[12],x[ 8]),18)); 274 | x[ 9] = XOR(x[ 9],ROTATE(PLUS(x[ 5],x[ 1]), 7)); 275 | x[13] = XOR(x[13],ROTATE(PLUS(x[ 9],x[ 5]), 9)); 276 | x[ 1] = XOR(x[ 1],ROTATE(PLUS(x[13],x[ 9]),13)); 277 | x[ 5] = XOR(x[ 5],ROTATE(PLUS(x[ 1],x[13]),18)); 278 | x[14] = XOR(x[14],ROTATE(PLUS(x[10],x[ 6]), 7)); 279 | x[ 2] = XOR(x[ 2],ROTATE(PLUS(x[14],x[10]), 9)); 280 | x[ 6] = XOR(x[ 6],ROTATE(PLUS(x[ 2],x[14]),13)); 281 | x[10] = XOR(x[10],ROTATE(PLUS(x[ 6],x[ 2]),18)); 282 | x[ 3] = XOR(x[ 3],ROTATE(PLUS(x[15],x[11]), 7)); 283 | x[ 7] = XOR(x[ 7],ROTATE(PLUS(x[ 3],x[15]), 9)); 284 | x[11] = XOR(x[11],ROTATE(PLUS(x[ 7],x[ 3]),13)); 285 | x[15] = XOR(x[15],ROTATE(PLUS(x[11],x[ 7]),18)); 286 | 287 | x[ 1] = XOR(x[ 1],ROTATE(PLUS(x[ 0],x[ 3]), 7)); 288 | x[ 2] = XOR(x[ 2],ROTATE(PLUS(x[ 1],x[ 0]), 9)); 289 | x[ 3] = XOR(x[ 3],ROTATE(PLUS(x[ 2],x[ 1]),13)); 290 | x[ 0] = XOR(x[ 0],ROTATE(PLUS(x[ 3],x[ 2]),18)); 291 | x[ 6] = XOR(x[ 6],ROTATE(PLUS(x[ 5],x[ 4]), 7)); 292 | x[ 7] = XOR(x[ 7],ROTATE(PLUS(x[ 6],x[ 5]), 9)); 293 | x[ 4] = XOR(x[ 4],ROTATE(PLUS(x[ 7],x[ 6]),13)); 294 | x[ 5] = XOR(x[ 5],ROTATE(PLUS(x[ 4],x[ 7]),18)); 295 | x[11] = XOR(x[11],ROTATE(PLUS(x[10],x[ 9]), 7)); 296 | x[ 8] = XOR(x[ 8],ROTATE(PLUS(x[11],x[10]), 9)); 297 | x[ 9] = XOR(x[ 9],ROTATE(PLUS(x[ 8],x[11]),13)); 298 | x[10] = XOR(x[10],ROTATE(PLUS(x[ 9],x[ 8]),18)); 299 | x[12] = XOR(x[12],ROTATE(PLUS(x[15],x[14]), 7)); 300 | x[13] = XOR(x[13],ROTATE(PLUS(x[12],x[15]), 9)); 301 | x[14] = XOR(x[14],ROTATE(PLUS(x[13],x[12]),13)); 302 | x[15] = XOR(x[15],ROTATE(PLUS(x[14],x[13]),18)); 303 | 304 | for i in range( len( input ) ): 305 | x[i] = PLUS( x[i], input[i] ) 306 | return little16_i32.pack( *x ) 307 | 308 | #--------------------------- 32-bit ops ------------------------------- 309 | 310 | def trunc32( w ): 311 | """ Return the bottom 32 bits of w as a Python int. 312 | This creates longs temporarily, but returns an int. """ 313 | w = int( ( w & 0x7fffFFFF ) | -( w & 0x80000000 ) ) 314 | assert type(w) == int 315 | return w 316 | 317 | 318 | def add32( a, b ): 319 | """ Add two 32-bit words discarding carry above 32nd bit, 320 | and without creating a Python long. 321 | Timing shouldn't vary. 322 | """ 323 | lo = ( a & 0xFFFF ) + ( b & 0xFFFF ) 324 | hi = ( a >> 16 ) + ( b >> 16 ) + ( lo >> 16 ) 325 | return ( -(hi & 0x8000) | ( hi & 0x7FFF ) ) << 16 | ( lo & 0xFFFF ) 326 | 327 | 328 | def rot32( w, nLeft ): 329 | """ Rotate 32-bit word left by nLeft or right by -nLeft 330 | without creating a Python long. 331 | Timing depends on nLeft but not on w. 332 | """ 333 | nLeft &= 31 # which makes nLeft >= 0 334 | if nLeft == 0: 335 | return w 336 | 337 | # Note: now 1 <= nLeft <= 31. 338 | # RRRsLLLLLL There are nLeft RRR's, (31-nLeft) LLLLLL's, 339 | # => sLLLLLLRRR and one s which becomes the sign bit. 340 | RRR = ( ( ( w >> 1 ) & 0x7fffFFFF ) >> ( 31 - nLeft ) ) 341 | sLLLLLL = -( (1<<(31-nLeft)) & w ) | (0x7fffFFFF>>nLeft) & w 342 | return RRR | ( sLLLLLL << nLeft ) 343 | 344 | 345 | # --------------------------------- end ----------------------------------- 346 | -------------------------------------------------------------------------------- /tests/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import sys 4 | import unittest 5 | 6 | sys.path.append(os.path.abspath(".")) 7 | sys.path.append(os.path.abspath("..")) 8 | 9 | from keepass.crypto import sha256, transform_key, aes_cbc_decrypt, xor, pad 10 | from keepass.crypto import AES_BLOCK_SIZE 11 | 12 | 13 | class TextCrypto(unittest.TestCase): 14 | def test_sha256(self): 15 | self.assertEquals(sha256(''), 16 | "\xe3\xb0\xc4B\x98\xfc\x1c\x14\x9a\xfb\xf4\xc8\x99o\xb9$'\xaeA" 17 | "\xe4d\x9b\x93L\xa4\x95\x99\x1bxR\xb8U") 18 | self.assertEquals(len(sha256('')), 32) 19 | self.assertEquals(len(sha256('asdf')), 32) 20 | 21 | def test_transform_key(self): 22 | self.assertEquals(transform_key(sha256('a'), sha256('b'), 1), 23 | '"$\xe6\x83\xb7\xbf\xa9|\x82W\x01J\xce=\xaa\x8d{\x18\x99|0\x1f' 24 | '\xbbLT4"F\x83\xd0\xc8\xf9') 25 | self.assertEquals(transform_key(sha256('a'), sha256('b'), 2000), 26 | '@\xe5Y\x98\xf7\x97$\x0b\x91!\xbefX\xe8\xb6\xbb\t\xefX>\xb3E\x85' 27 | '\xedz\x15\x9c\x96\x03K\x8a\xa1') 28 | 29 | def test_aes_cbc_decrypt(self): 30 | self.assertEquals(aes_cbc_decrypt('datamustbe16byte', sha256('b'), 31 | 'ivmustbe16bytesl'), 32 | 'x]\xb5\xa6\xe3\x10\xf4\x88\x91_\x03\xc6\xb9\xfb`)') 33 | self.assertEquals(aes_cbc_decrypt('datamustbe16byte', sha256('c'), 34 | 'ivmustbe16bytesl'), 35 | '\x06\x91 \xd9\\\xd8\x14\xa0\xdc\xd7\x82\xa0\x92\xfb\xe8l') 36 | 37 | def test_xor(self): 38 | self.assertEquals(xor('', ''), '') 39 | self.assertEquals(xor('\x00', '\x00'), '\x00') 40 | self.assertEquals(xor('\x01', '\x00'), '\x01') 41 | self.assertEquals(xor('\x01\x01', '\x00\x01'), '\x01\x00') 42 | self.assertEquals(xor('banana', 'ananas'), '\x03\x0f\x0f\x0f\x0f\x12') 43 | 44 | def test_pad(self): 45 | self.assertEquals(pad(''), 46 | '\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10') 47 | self.assertEquals(pad('\xff'), 48 | '\xff\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f') 49 | self.assertEquals(pad('\xff\xff'), 50 | '\xff\xff\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e') 51 | self.assertEquals(pad('\xff\xff\xff'), 52 | '\xff\xff\xff\x0d\x0d\x0d\x0d\x0d\x0d\x0d\x0d\x0d\x0d\x0d\x0d\x0d') 53 | self.assertEquals(pad('\xff\xff\xff\xff'), 54 | '\xff\xff\xff\xff\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c') 55 | self.assertEquals(pad('\xff\xff\xff\xff\xff'), 56 | '\xff\xff\xff\xff\xff\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b') 57 | self.assertEquals(len(pad('\xff')), AES_BLOCK_SIZE) 58 | self.assertEquals(len(pad('\xff'*0)), AES_BLOCK_SIZE) 59 | self.assertEquals(len(pad('\xff'*1)), AES_BLOCK_SIZE) 60 | self.assertEquals(len(pad('\xff'*2)), AES_BLOCK_SIZE) 61 | self.assertEquals(len(pad('\xff'*15)), AES_BLOCK_SIZE) 62 | self.assertEquals(len(pad('\xff'*16)), 2*AES_BLOCK_SIZE) 63 | self.assertEquals(len(pad('\xff'*17)), 2*AES_BLOCK_SIZE) 64 | 65 | 66 | import keepass 67 | 68 | 69 | class TestModule(unittest.TestCase): 70 | 71 | def test_get_kdb_class(self): 72 | # v3 73 | self.assertIsNotNone(keepass.get_kdb_reader([0x9AA2D903, 0xB54BFB65])) 74 | self.assertEquals(keepass.get_kdb_reader([0x9AA2D903, 0xB54BFB65]), 75 | keepass.kdb3.KDB3Reader) 76 | # v4 77 | self.assertIsNotNone(keepass.get_kdb_reader([0x9AA2D903, 0xB54BFB67])) 78 | self.assertEquals(keepass.get_kdb_reader([0x9AA2D903, 0xB54BFB67]), 79 | keepass.kdb4.KDB4Reader) 80 | 81 | # mythical pre2.x signature 82 | with self.assertRaisesRegexp(IOError, "Unknown sub signature."): 83 | keepass.get_kdb_reader([0x9AA2D903, 0xB54BFB66, 3, 0]) 84 | 85 | # unknown sub signature 86 | with self.assertRaisesRegexp(IOError, "Unknown sub signature."): 87 | keepass.get_kdb_reader([0x9AA2D903, 0xB54BFB60, 3, 0]) 88 | # valid sub signature, unknown base signature 89 | with self.assertRaisesRegexp(IOError, "Unknown base signature."): 90 | keepass.get_kdb_reader([0x9AA2D900, 0xB54BFB65, 3, 0]) 91 | # unknown sub signature, unknown base signature 92 | with self.assertRaisesRegexp(IOError, "Unknown base signature."): 93 | keepass.get_kdb_reader([0x9AA2D900, 0xB54BFB60, 3, 0]) 94 | 95 | 96 | class TestCommon(unittest.TestCase): 97 | 98 | def test_header_dict(self): 99 | h = keepass.common.HeaderDictionary() 100 | # configure fields 101 | h.fields = {'first': 1, 'second': 2} 102 | 103 | # set and get via int or name 104 | h[1] = '1_eins' 105 | self.assertEquals(h[1], '1_eins') 106 | self.assertEquals(h['first'], '1_eins') 107 | h['first'] = '2_eins' 108 | self.assertEquals(h[1], '2_eins') 109 | self.assertEquals(h['first'], '2_eins') 110 | 111 | # in fields, but not set 112 | self.assertRaises(KeyError, lambda: h[2]) 113 | self.assertRaises(KeyError, lambda: h['second']) 114 | 115 | # not even in fields 116 | self.assertRaises(KeyError, lambda: h[3]) 117 | self.assertRaises(KeyError, lambda: h['third']) 118 | 119 | # attribute access (reading) 120 | self.assertEquals(h.first, '2_eins') 121 | self.assertRaises(AttributeError, lambda: h.second) 122 | self.assertRaises(AttributeError, lambda: h.third) 123 | 124 | # attribute writing 125 | h.first = '3_eins' 126 | self.assertEquals(h.first, '3_eins') 127 | h.second = '1_zwei' 128 | self.assertEquals(h.second, '1_zwei') 129 | self.assertEquals(h[2], '1_zwei') 130 | self.assertEquals(h['second'], '1_zwei') 131 | 132 | # add another field and data 133 | h.fields['third'] = 3 134 | h.third = '1_drei' 135 | self.assertEquals(h.third, '1_drei') 136 | self.assertEquals(h[3], '1_drei') 137 | self.assertEquals(h['third'], '1_drei') 138 | h['third'] = '2_drei' 139 | self.assertEquals(h.third, '2_drei') 140 | self.assertEquals(h[3], '2_drei') 141 | self.assertEquals(h['third'], '2_drei') 142 | 143 | # implicit nice to raw conversion 144 | h.fields['rounds'] = 4 145 | h.fmt[4] = '\n10 is undefined 108 | if not field_id in self.header.fields.values(): 109 | raise IOError('Unknown header field found.') 110 | 111 | # two byte (short) length of field data 112 | length = stream_unpack(stream, None, 2, 'h') 113 | if length > 0: 114 | data = stream_unpack(stream, None, length, '{}s'.format(length)) 115 | self.header.b[field_id] = data 116 | 117 | # set position in data stream of end of header 118 | if field_id == 0: 119 | self.header_length = stream.tell() 120 | break 121 | 122 | def _write_header(self, stream): 123 | """Serialize the header fields from self.header into a byte stream, prefix 124 | with file signature and version before writing header and out-buffer 125 | to `stream`. 126 | 127 | Note, that `stream` is flushed, but not closed!""" 128 | # serialize header to stream 129 | header = bytearray() 130 | # write file signature 131 | header.extend(struct.pack(' len(self._salsa_buffer): 339 | new_salsa = self.salsa.encryptBytes(str(bytearray(64))) 340 | self._salsa_buffer.extend(new_salsa) 341 | nacho = self._salsa_buffer[:length] 342 | del self._salsa_buffer[:length] 343 | return nacho 344 | 345 | def _unprotect(self, string): 346 | """ 347 | Base64 decode and XOR the given `string` with the next salsa. 348 | Returns an unprotected string. 349 | """ 350 | tmp = base64.b64decode(string) 351 | return str(xor(tmp, self._get_salsa(len(tmp)))) 352 | 353 | def _protect(self, string): 354 | """ 355 | XORs the given `string` with the next salsa and base64 encodes it. 356 | Returns a protected string. 357 | """ 358 | tmp = str(xor(string, self._get_salsa(len(string)))) 359 | return base64.b64encode(tmp) 360 | 361 | class KDB4Reader(KDB4File, KDBXmlExtension): 362 | """ 363 | Usually you would want to use the `keepass.open` context manager to open a 364 | file. It checks the file signature and creates a suitable reader-instance. 365 | 366 | doing it by hand is also possible:: 367 | 368 | kdb = keepass.KDB4Reader() 369 | kdb.add_credentials(password='secret') 370 | with open('passwords.kdb', 'rb') as fh: 371 | kdb.read_from(fh) 372 | 373 | or...:: 374 | 375 | with open('passwords.kdb', 'rb') as fh: 376 | kdb = keepass.KDB4Reader(fh, password='secret') 377 | 378 | """ 379 | def __init__(self, stream=None, **credentials): 380 | KDB4File.__init__(self, stream, **credentials) 381 | 382 | def read_from(self, stream, unprotect=True): 383 | KDB4File.read_from(self, stream) 384 | # the extension requires parsed header and decrypted self.in_buffer, so 385 | # initialize only here 386 | KDBXmlExtension.__init__(self, unprotect) 387 | 388 | def write_to(self, stream, use_etree=True): 389 | """ 390 | Write the KeePass database back to a KeePass2 compatible file. 391 | 392 | :arg stream: A file-like object or IO buffer. 393 | :arg use_tree: Serialize the element tree to XML to save (default: 394 | True), Set to False to write the data currently in the in-buffer 395 | instead. 396 | """ 397 | if use_etree: 398 | KDBXmlExtension.write_to(self, stream) 399 | KDB4File.write_to(self, stream) 400 | 401 | --------------------------------------------------------------------------------