Erik Grinaker is sharing code with you

Bitbucket is a code hosting site. Unlimited public and private repositories. Free for small teams.

Don't show this again

erikg / Revelation

Revelation is a password manager for the GNOME desktop, released under the GNU GPL license. It stores all your accounts and passwords in a single, secure place, and gives you access to it through a user-friendly graphical interface.

Clone this repository (size: 2.1 MB): HTTPS / SSH
hg clone https://bitbucket.org/erikg/revelation
hg clone ssh://hg@bitbucket.org/erikg/revelation

Revelation / src / lib / datahandler / rvl.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
#
# Revelation - a password manager for GNOME 2
# http://oss.codepoet.no/revelation/
# $Id$
#
# Module for handling Revelation data
#
#
# Copyright (c) 2003-2006 Erik Grinaker
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#

import base
from revelation import config, data, entry, util
from revelation.bundle import luks

import re, StringIO, struct, xml.dom.minidom, zlib

from xml.parsers.expat import ExpatError
from Crypto.Cipher import AES



class RevelationXML(base.DataHandler):
        "Handler for Revelation XML data"

        name            = "XML"
        importer        = True
        exporter        = True
        encryption      = False


        def __init__(self):
                base.DataHandler.__init__(self)


        def __lookup_entry(self, typename):
                "Looks up an entry type based on an identifier"

                for entrytype in entry.ENTRYLIST:
                        if entrytype().id == typename:
                                return entrytype

                else:
                        raise entry.EntryTypeError


        def __lookup_field(self, fieldname):
                "Looks up an entry field based on an identifier"

                for fieldtype in entry.FIELDLIST:
                        if fieldtype.id == fieldname:
                                return fieldtype

                else:
                        raise entry.EntryFieldError


        def __xml_import_node(self, entrystore, node, parent = None):
                "Imports a node into an entrystore"

                try:

                        # check the node
                        if node.nodeType == node.TEXT_NODE:
                                return

                        if node.nodeType != node.ELEMENT_NODE or node.nodeName != "entry":
                                raise base.FormatError

                        # create an entry, iter needed for children
                        e = self.__lookup_entry(node.attributes["type"].value)()
                        iter = entrystore.add_entry(e, parent)

                        # handle child nodes
                        for child in node.childNodes:

                                if child.nodeType != child.ELEMENT_NODE:
                                        continue

                                elif child.nodeName == "name":
                                        e.name = util.dom_text(child)

                                elif child.nodeName == "description":
                                        e.description = util.dom_text(child)

                                elif child.nodeName == "updated":
                                        e.updated = int(util.dom_text(child))

                                elif child.nodeName == "field":
                                        e[self.__lookup_field(child.attributes["id"].nodeValue)] = util.dom_text(child)

                                elif child.nodeName == "entry":
                                        if type(e) != entry.FolderEntry:
                                                raise base.DataError

                                        self.__xml_import_node(entrystore, child, iter)

                                else:
                                        raise base.FormatError

                        # update entry with actual data
                        entrystore.update_entry(iter, e)


                except ( entry.EntryTypeError, entry.EntryFieldError ):
                        raise base.DataError

                except KeyError:
                        raise base.FormatError

                except ValueError:
                        raise base.DataError


        def check(self, input):
                "Checks if the data is valid"

                if input is None:
                        raise base.FormatError

                match = re.match("""
                        \s*                     # whitespace at beginning
                        <\?xml(?:.*)\?>         # xml header
                        \s*                     # whitespace after xml header
                        <revelationdata         # open revelationdata tag
                        [^>]+                   # any non-closing character
                        dataversion="(\d+)"     # dataversion
                        [^>]*                   # any non-closing character
                        >                       # close revelationdata tag
                """, input, re.VERBOSE)

                if match is None:
                        raise base.FormatError

                if int(match.group(1)) != 1:
                        raise base.VersionError


        def detect(self, input):
                "Checks if this handler can guarantee to handle some data"

                try:
                        self.check(input)
                        return True

                except ( base.FormatError, base.VersionError ):
                        return False


        def export_data(self, entrystore, password = None, parent = None, level = 0):
                "Serializes data into an XML stream"

                xml = ""
                tabs = "\t" * (level + 1)

                for i in range(entrystore.iter_n_children(parent)):
                        iter = entrystore.iter_nth_child(parent, i)
                        e = entrystore.get_entry(iter)

                        xml += "\n"
                        xml += tabs + "<entry type=\"%s\">\n" % e.id
                        xml += tabs + " <name>%s</name>\n" % util.escape_markup(e.name)
                        xml += tabs + " <description>%s</description>\n" % util.escape_markup(e.description)
                        xml += tabs + " <updated>%d</updated>\n" % e.updated

                        for field in e.fields:
                                xml += tabs + " <field id=\"%s\">%s</field>\n" % ( field.id, util.escape_markup(field.value) )

                        xml += RevelationXML.export_data(self, entrystore, password, iter, level + 1)
                        xml += tabs + "</entry>\n"

                if level == 0:
                        header = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"
                        header += "<revelationdata version=\"%s\" dataversion=\"1\">\n" % config.VERSION
                        footer = "</revelationdata>\n"

                        xml = header + xml + footer

                return xml


        def import_data(self, input, password = None):
                "Imports data from a data stream to an entrystore"

                RevelationXML.check(self, input)

                try:
                        dom = xml.dom.minidom.parseString(input.strip())

                except ExpatError:
                        raise base.FormatError


                if dom.documentElement.nodeName != "revelationdata":
                        raise base.FormatError

                if not dom.documentElement.attributes.has_key("dataversion"):
                        raise base.FormatError


                entrystore = data.EntryStore()

                for node in dom.documentElement.childNodes:
                        self.__xml_import_node(entrystore, node)

                return entrystore



class Revelation(RevelationXML):
        "Handler for Revelation data"

        name            = "Revelation"
        importer        = True
        exporter        = True
        encryption      = True


        def __init__(self):
                RevelationXML.__init__(self)


        def __generate_header(self):
                "Generates a header"

                header = "rvl\x00"              # magic string
                header += "\x01"                # data version
                header += "\x00"                # separator
                header += "\x00\x04\x06"        # application version TODO
                header += "\x00\x00\x00"        # separator

                return header


        def __parse_header(self, header):
                "Parses a data header, returns the data version"

                if header is None:
                        raise base.FormatError

                match = re.match("""
                        ^                       # start of header
                        rvl\x00                 # magic string
                        (.)                     # data version
                        \x00                    # separator
                        (.{3})                  # app version
                        \x00\x00\x00            # separator
                """, header, re.VERBOSE)

                if match is None:
                        raise base.FormatError

                return ord(match.group(1))


        def check(self, input):
                "Checks if the data is valid"

                if input is None:
                        raise base.FormatError

                if len(input) < (12 + 16):
                        raise base.FormatError

                dataversion = self.__parse_header(input[:12])

                if dataversion != 1:
                        raise base.VersionError


        def detect(self, input):
                "Checks if the handler can guarantee to use the data"

                try:
                        self.check(input)
                        return True

                except ( base.FormatError, base.VersionError ):
                        return False


        def export_data(self, entrystore, password):
                "Exports data from an entrystore"

                # check and pad password
                if password is None:
                        raise base.PasswordError

                password = util.pad_right(password[:32], 32, "\0")

                # generate XML
                data = RevelationXML.export_data(self, entrystore)

                # compress data, and right-pad with the repeated ascii
                # value of the pad length
                data = zlib.compress(data)

                padlen = 16 - (len(data) % 16)
                if padlen == 0:
                        padlen = 16

                data += chr(padlen) * padlen

                # generate an initial vector for the CBC encryption
                iv = util.random_string(16)

                # encrypt data
                AES.block_size = 16
                AES.key_size = 32

                data = AES.new(password, AES.MODE_CBC, iv).encrypt(data)

                # encrypt the iv, and prepend it to the data with a header
                data = self.__generate_header() + AES.new(password).encrypt(iv) + data

                return data


        def import_data(self, input, password):
                "Imports data into an entrystore"

                # check and pad password
                if password is None:
                        raise base.PasswordError

                password = util.pad_right(password[:32], 32, "\0")

                # check the data
                self.check(input)
                dataversion = self.__parse_header(input[:12])

                # handle only version 1 data files
                if dataversion != 1:
                        raise base.VersionError


                # fetch and decrypt the initial vector for CBC decryption
                AES.block_size = 16
                AES.key_size = 32

                cipher = AES.new(password)
                iv = cipher.decrypt(input[12:28])


                # decrypt the data
                input = input[28:]

                if len(input) % 16 != 0:
                        raise base.FormatError

                cipher = AES.new(password, AES.MODE_CBC, iv)
                input = cipher.decrypt(input)


                # decompress data
                padlen = ord(input[-1])
                for i in input[-padlen:]:
                        if ord(i) != padlen:
                                raise base.PasswordError

                input = zlib.decompress(input[0:-padlen])


                # check and import data
                if input.strip()[:5] != "<?xml":
                        raise base.PasswordError

                entrystore = RevelationXML.import_data(self, input)

                return entrystore


class RevelationLUKS(RevelationXML):
        "Handler for Revelation XML using the LUKS on disk format"

        name            = "Revelation LUKS"
        importer        = True
        exporter        = True
        encryption      = True

        def __init__(self):
                RevelationXML.__init__(self)
                self.luks_header = None
                self.luks_buff = None
                self.current_slot = False


        def check(self, input):
                "Checks if the data is valid"

                if input is None:
                        raise base.FormatError

                sbuf = StringIO.StringIO(input)

                l = luks.LuksFile()

                try:
                        l.load_from_file(sbuf)

                except:
                        l.close()
                        raise base.FormatError

                l.close()


        def detect(self, input):
                "Checks if the handler can guarantee to use the data"

                try:
                        self.check(input)
                        return True

                except ( base.FormatError, base.VersionError ):
                        return False


        def export_data(self, entrystore, password):
                "Exports data from an entrystore"

                # check and pad password
                if password is None:
                        raise base.PasswordError

                # generate and compress XML
                data = RevelationXML.export_data(self, entrystore)
                data = zlib.compress(data)

                # data needs to be padded to 512 bytes
                # We use Merkle-Damgard length padding (1 bit followed by 0 bits + size)
                # http://en.wikipedia.org/wiki/Merkle-Damg%C3%A5rd_hash_function
                padlen = 512 - (len(data) % 512)

                if padlen < 4:
                        padlen = 512 + padlen

                if padlen > 4:
                        data += "\x80" + "\x00" * (padlen - 5)

                data += struct.pack("<I", padlen)

                # create a new luks file in memory
                buffer          = StringIO.StringIO()
                luksfile        = luks.LuksFile()
                luksfile.create(buffer, "aes", "cbc-essiv:sha256", "sha1", 16, 400)

                luksfile.set_key(0, password, 5000, 400)

                # encrypt the data
                luksfile.encrypt_data(0, data)
                buffer.seek(0)

                return buffer.read()


        def import_data(self, input, password):
                "Imports data into an entrystore"

                # check password
                if password is None:
                        raise base.PasswordError

                # create a LuksFile
                buffer          = StringIO.StringIO(input)
                luksfile        = luks.LuksFile()

                try:
                        luksfile.load_from_file(buffer)

                except:
                        luksfile.close()
                        buffer.close()
                        raise base.FormatError

                slot = luksfile.open_any_key(password)

                if slot == None:
                        luksfile.close()
                        buffer.close()
                        raise base.PasswordError

                data = luksfile.decrypt_data(0, luksfile.data_length())

                # remove the pad, and decompress
                padlen = struct.unpack("<I", data[-4:])[0]
                data = zlib.decompress(data[0:-padlen])

                if data.strip()[:5] != "<?xml":
                        raise base.FormatError

                entrystore = RevelationXML.import_data(self, data)

                return entrystore