├── 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 |
--------------------------------------------------------------------------------