"Fossies" - the Fresh Open Source Software Archive

Member "revelation-0.5.4/src/lib/datahandler/rvl.py" (4 Oct 2020, 18099 Bytes) of package /linux/privat/revelation-0.5.4.tar.xz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "rvl.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 0.5.3_vs_0.5.4.

    1 #
    2 # Revelation - a password manager for GNOME 2
    3 # http://oss.codepoet.no/revelation/
    4 # $Id$
    5 #
    6 # Module for handling Revelation data
    7 #
    8 #
    9 # Copyright (c) 2003-2006 Erik Grinaker
   10 # Copyright (c) 2012 Mikel Olasagasti
   11 #
   12 # This program is free software; you can redistribute it and/or
   13 # modify it under the terms of the GNU General Public License
   14 # as published by the Free Software Foundation; either version 2
   15 # of the License, or (at your option) any later version.
   16 #
   17 # This program is distributed in the hope that it will be useful,
   18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
   19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   20 # GNU General Public License for more details.
   21 #
   22 # You should have received a copy of the GNU General Public License
   23 # along with this program; if not, write to the Free Software
   24 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
   25 #
   26 
   27 from . import base
   28 from revelation import config, data, entry, util
   29 from revelation.bundle import luks
   30 
   31 from Cryptodome.Protocol.KDF import PBKDF2
   32 from Cryptodome.Hash import SHA1
   33 from Cryptodome.Random import get_random_bytes
   34 
   35 import os, re, struct, xml.dom.minidom, zlib
   36 from io import BytesIO
   37 
   38 from xml.parsers.expat import ExpatError
   39 from Cryptodome.Cipher import AES
   40 
   41 import hashlib
   42 
   43 
   44 class RevelationXML(base.DataHandler):
   45     "Handler for Revelation XML data"
   46 
   47     name        = "XML"
   48     importer    = True
   49     exporter    = True
   50     encryption  = False
   51 
   52 
   53     def __init__(self):
   54         base.DataHandler.__init__(self)
   55 
   56 
   57     def __lookup_entry(self, typename):
   58         "Looks up an entry type based on an identifier"
   59 
   60         for entrytype in entry.ENTRYLIST:
   61             if entrytype().id == typename:
   62                 return entrytype
   63 
   64         else:
   65             raise entry.EntryTypeError
   66 
   67 
   68     def __lookup_field(self, fieldname):
   69         "Looks up an entry field based on an identifier"
   70 
   71         for fieldtype in entry.FIELDLIST:
   72             if fieldtype.id == fieldname:
   73                 return fieldtype
   74 
   75         else:
   76             raise entry.EntryFieldError
   77 
   78 
   79     def __xml_import_node(self, entrystore, node, parent = None):
   80         "Imports a node into an entrystore"
   81 
   82         try:
   83 
   84             # check the node
   85             if node.nodeType == node.TEXT_NODE:
   86                 return
   87 
   88             if node.nodeType != node.ELEMENT_NODE or node.nodeName != "entry":
   89                 raise base.FormatError
   90 
   91             # create an entry, iter needed for children
   92             e = self.__lookup_entry(node.attributes["type"].value)()
   93             iter = entrystore.add_entry(e, parent)
   94 
   95             # handle child nodes
   96             for child in node.childNodes:
   97 
   98                 if child.nodeType != child.ELEMENT_NODE:
   99                     continue
  100 
  101                 elif child.nodeName == "name":
  102                     e.name = util.dom_text(child)
  103 
  104                 elif child.nodeName == "notes":
  105                     e.notes = util.dom_text(child)
  106 
  107                 elif child.nodeName == "description":
  108                     e.description = util.dom_text(child)
  109 
  110                 elif child.nodeName == "updated":
  111                     e.updated = int(util.dom_text(child))
  112 
  113                 elif child.nodeName == "field":
  114                     e[self.__lookup_field(child.attributes["id"].nodeValue)] = util.dom_text(child)
  115 
  116                 elif child.nodeName == "entry":
  117                     if type(e) != entry.FolderEntry:
  118                         raise base.DataError
  119 
  120                     self.__xml_import_node(entrystore, child, iter)
  121 
  122                 else:
  123                     raise base.FormatError
  124 
  125             # update entry with actual data
  126             entrystore.update_entry(iter, e)
  127 
  128 
  129         except ( entry.EntryTypeError, entry.EntryFieldError ):
  130             raise base.DataError
  131 
  132         except KeyError:
  133             raise base.FormatError
  134 
  135         except ValueError:
  136             raise base.DataError
  137 
  138 
  139     def check(self, input):
  140         "Checks if the data is valid"
  141 
  142         if input is None:
  143             raise base.FormatError
  144 
  145         if isinstance(input, str):
  146             input = input.encode()
  147 
  148         match = re.match(b"""
  149             \s*                 # whitespace at beginning
  150             <\?xml(?:.*)\?>     # xml header
  151             \s*                 # whitespace after xml header
  152             <revelationdata     # open revelationdata tag
  153             [^>]+               # any non-closing character
  154             dataversion="(\d+)" # dataversion
  155             [^>]*               # any non-closing character
  156             >                   # close revelationdata tag
  157         """, input, re.VERBOSE)
  158 
  159         if match is None:
  160             raise base.FormatError
  161 
  162         if int(match.group(1)) != 1:
  163             raise base.VersionError
  164 
  165 
  166     def detect(self, input):
  167         "Checks if this handler can guarantee to handle some data"
  168 
  169         try:
  170             self.check(input)
  171             return True
  172 
  173         except ( base.FormatError, base.VersionError ):
  174             return False
  175 
  176 
  177     def export_data(self, entrystore, password = None, parent = None, level = 0):
  178         "Serializes data into an XML stream"
  179 
  180         xml = ""
  181         tabs = "\t" * (level + 1)
  182 
  183         for i in range(entrystore.iter_n_children(parent)):
  184             iter = entrystore.iter_nth_child(parent, i)
  185             e = entrystore.get_entry(iter)
  186 
  187             xml += "\n"
  188             xml += tabs + "<entry type=\"%s\">\n" % e.id
  189             xml += tabs + " <name>%s</name>\n" % util.escape_markup(e.name)
  190             xml += tabs + " <description>%s</description>\n" % util.escape_markup(e.description)
  191             xml += tabs + " <updated>%d</updated>\n" % e.updated
  192             xml += tabs + " <notes>%s</notes>\n" % util.escape_markup(e.notes)
  193 
  194             for field in e.fields:
  195                 xml += tabs + " <field id=\"%s\">%s</field>\n" % ( field.id, util.escape_markup(field.value) )
  196 
  197             xml += RevelationXML.export_data(self, entrystore, password, iter, level + 1)
  198             xml += tabs + "</entry>\n"
  199 
  200         if level == 0:
  201             header = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"
  202             header += "<revelationdata version=\"%s\" dataversion=\"1\">\n" % config.VERSION
  203             footer = "</revelationdata>\n"
  204 
  205             xml = header + xml + footer
  206 
  207         return xml
  208 
  209 
  210     def import_data(self, input, password = None):
  211         "Imports data from a data stream to an entrystore"
  212 
  213         RevelationXML.check(self, input)
  214 
  215         try:
  216             dom = xml.dom.minidom.parseString(input.strip())
  217 
  218         except ExpatError:
  219             raise base.FormatError
  220 
  221 
  222         if dom.documentElement.nodeName != "revelationdata":
  223             raise base.FormatError
  224 
  225         if "dataversion" not in dom.documentElement.attributes:
  226             raise base.FormatError
  227 
  228 
  229         entrystore = data.EntryStore()
  230 
  231         for node in dom.documentElement.childNodes:
  232             self.__xml_import_node(entrystore, node)
  233 
  234         return entrystore
  235 
  236 
  237 
  238 class Revelation(RevelationXML):
  239     "Handler for Revelation data"
  240 
  241     name        = "Revelation"
  242     importer    = True
  243     exporter    = True
  244     encryption  = True
  245 
  246 
  247     def __init__(self):
  248         RevelationXML.__init__(self)
  249 
  250 
  251     def __generate_header(self):
  252         "Generates a header"
  253 
  254         header = "rvl\x00"         # magic string
  255         header += "\x01"           # data version
  256         header += "\x00"           # separator
  257         header += "\x00\x04\x06"   # application version TODO
  258         header += "\x00\x00\x00"   # separator
  259 
  260         return header
  261 
  262 
  263     def __parse_header(self, header):
  264         "Parses a data header, returns the data version"
  265 
  266         if header is None:
  267             raise base.FormatError
  268 
  269         match = re.match(b"""
  270             ^               # start of header
  271             rvl\x00         # magic string
  272             (.)             # data version
  273             \x00            # separator
  274             (.{3})          # app version
  275             \x00\x00\x00    # separator
  276         """, header, re.VERBOSE)
  277 
  278         if match is None:
  279             raise base.FormatError
  280 
  281         return ord(match.group(1))
  282 
  283 
  284     def check(self, input):
  285         "Checks if the data is valid"
  286 
  287         if input is None:
  288             raise base.FormatError
  289 
  290         if len(input) < (12 + 16):
  291             raise base.FormatError
  292 
  293         dataversion = self.__parse_header(input[:12])
  294 
  295         if dataversion != 1:
  296             raise base.VersionError
  297 
  298 
  299     def detect(self, input):
  300         "Checks if the handler can guarantee to use the data"
  301 
  302         try:
  303             self.check(input)
  304             return True
  305 
  306         except ( base.FormatError, base.VersionError ):
  307             return False
  308 
  309 
  310     def export_data(self, entrystore, password):
  311         "Exports data from an entrystore"
  312 
  313         # check and pad password
  314         if password is None:
  315             raise base.PasswordError
  316 
  317         password = util.pad_right(password[:32], 32, "\0")
  318 
  319         # generate XML
  320         data = RevelationXML.export_data(self, entrystore)
  321 
  322         # compress data, and right-pad with the repeated ascii
  323         # value of the pad length
  324         data = zlib.compress(data.encode())
  325 
  326         padlen = 16 - (len(data) % 16)
  327         if padlen == 0:
  328             padlen = 16
  329 
  330         data += bytearray((padlen,)) * padlen
  331 
  332         # generate an initial vector for the CBC encryption
  333         iv = os.urandom(16)
  334 
  335         data = AES.new(password.encode("utf8"), AES.MODE_CBC, iv).encrypt(data)
  336 
  337         # encrypt the iv, and prepend it to the data with a header
  338         data = self.__generate_header().encode("utf8") + AES.new(password.encode("utf8"), AES.MODE_ECB).encrypt(iv) + data
  339 
  340         return data
  341 
  342 
  343     def import_data(self, input, password):
  344         "Imports data into an entrystore"
  345 
  346         # check and pad password
  347         if password is None:
  348             raise base.PasswordError
  349 
  350         password = util.pad_right(password[:32], 32, "\0")
  351 
  352         # check the data
  353         self.check(input)
  354         dataversion = self.__parse_header(input[:12])
  355 
  356         # handle only version 1 data files
  357         if dataversion != 1:
  358             raise base.VersionError
  359 
  360 
  361         cipher = AES.new(password.encode("utf8"), AES.MODE_ECB)
  362         iv = cipher.decrypt(input[12:28])
  363 
  364 
  365         # decrypt the data
  366         input = input[28:]
  367 
  368         if len(input) % 16 != 0:
  369             raise base.FormatError
  370 
  371         cipher = AES.new(password.encode("utf8"), AES.MODE_CBC, iv)
  372         input = cipher.decrypt(input)
  373 
  374 
  375         # decompress data
  376         padlen = input[-1]
  377         for i in input[-padlen:]:
  378             if i != padlen:
  379                 raise base.PasswordError
  380 
  381         input = zlib.decompress(input[0:-padlen]).decode()
  382 
  383 
  384         # check and import data
  385         if input.strip()[:5] != "<?xml":
  386             raise base.PasswordError
  387 
  388         entrystore = RevelationXML.import_data(self, input)
  389 
  390         return entrystore
  391 
  392 
  393 class Revelation2(RevelationXML):
  394     "Handler for Revelation data version 2"
  395 
  396     name        = "Revelation2"
  397     importer    = True
  398     exporter    = True
  399     encryption  = True
  400 
  401 
  402     def __init__(self):
  403         RevelationXML.__init__(self)
  404 
  405 
  406     def __generate_header(self):
  407         "Generates a header"
  408 
  409         header =  b"rvl\x00"        # magic string
  410         header += b"\x02"           # data version
  411         header += b"\x00"           # separator
  412         header += b"\x00\x04\x07"   # application version
  413         header += b"\x00\x00\x00"   # separator
  414 
  415         return header
  416 
  417 
  418     def __parse_header(self, header):
  419         "Parses a data header, returns the data version"
  420 
  421         if header is None:
  422             raise base.FormatError
  423 
  424         match = re.match(b"""
  425             ^               # start of header
  426             rvl\x00         # magic string
  427             (.)             # data version
  428             \x00            # separator
  429             (.{3})          # app version
  430             \x00\x00\x00    # separator
  431         """, header, re.VERBOSE)
  432 
  433         if match is None:
  434             raise base.FormatError
  435 
  436         return ord(match.group(1))
  437 
  438 
  439     def check(self, input):
  440         "Checks if the data is valid"
  441 
  442         if input is None:
  443             raise base.FormatError
  444 
  445         if len(input) < (12 + 16):
  446             raise base.FormatError
  447 
  448         dataversion = self.__parse_header(input[:12])
  449 
  450         if dataversion != 2:
  451             raise base.VersionError
  452 
  453 
  454     def detect(self, input):
  455         "Checks if the handler can guarantee to use the data"
  456 
  457         try:
  458             self.check(input)
  459             return True
  460 
  461         except ( base.FormatError, base.VersionError ):
  462             return False
  463 
  464 
  465     def export_data(self, entrystore, password):
  466         "Exports data from an entrystore"
  467 
  468         # check and hash password with a salt
  469         if password is None:
  470             raise base.PasswordError
  471 
  472         # 64-bit salt
  473         salt = get_random_bytes(8)
  474 
  475         # 256-bit key
  476         key = PBKDF2(password, salt, 32, count=12000, hmac_hash_module=SHA1)
  477 
  478         # generate XML
  479         data = RevelationXML.export_data(self, entrystore)
  480 
  481         # compress data, and right-pad with the repeated ascii
  482         # value of the pad length
  483         data = zlib.compress(data.encode())
  484 
  485         padlen = 16 - (len(data) % 16)
  486         if padlen == 0:
  487             padlen = 16
  488 
  489         data += bytearray((padlen,)) * padlen
  490 
  491         # 128-bit IV
  492         iv = get_random_bytes(16)
  493 
  494         data = AES.new(key, AES.MODE_CBC, iv).encrypt(hashlib.sha256(data).digest() + data)
  495 
  496         # encrypt the iv, and prepend it to the data with a header and the used salt
  497         data = self.__generate_header() + salt + iv + data
  498 
  499         return data
  500 
  501     def import_data(self, input, password):
  502         "Imports data into an entrystore"
  503 
  504         # check and pad password
  505         if password is None:
  506             raise base.PasswordError
  507 
  508         # check the data
  509         self.check(input)
  510         dataversion = self.__parse_header(input[:12])
  511 
  512         # handle only version 2 data files
  513         if dataversion != 2:
  514             raise base.VersionError
  515 
  516         # Fetch the used 64 bit salt
  517         salt = input[12:20]
  518         iv = input[20:36]
  519         key = PBKDF2(password, salt, 32, count=12000, hmac_hash_module=SHA1)
  520         # decrypt the data
  521         input = input[36:]
  522 
  523         if len(input) % 16 != 0:
  524             raise base.FormatError
  525 
  526         cipher = AES.new(key, AES.MODE_CBC, iv)
  527         input = cipher.decrypt(input)
  528         hash256 = input[0:32]
  529         data = input[32:]
  530 
  531         if hash256 != hashlib.sha256(data).digest():
  532             raise base.PasswordError
  533 
  534         # decompress data
  535         padlen = data[-1]
  536         for i in data[-padlen:]:
  537             if i != padlen:
  538                 raise base.FormatError
  539 
  540         data = zlib.decompress(data[0:-padlen]).decode()
  541 
  542         # check and import data
  543         if data.strip()[:5] != "<?xml":
  544             raise base.FormatError
  545 
  546         entrystore = RevelationXML.import_data(self, data)
  547 
  548         return entrystore
  549 
  550 
  551 class RevelationLUKS(RevelationXML):
  552     "Handler for Revelation XML using the LUKS on disk format"
  553 
  554     name        = "Revelation LUKS"
  555     importer    = True
  556     exporter    = True
  557     encryption  = True
  558 
  559     def __init__(self):
  560         RevelationXML.__init__(self)
  561         self.luks_header = None
  562         self.luks_buff = None
  563         self.current_slot = False
  564 
  565 
  566     def check(self, input):
  567         "Checks if the data is valid"
  568 
  569         if input is None:
  570             raise base.FormatError
  571 
  572         sbuf = BytesIO(input)
  573 
  574         l = luks.LuksFile()
  575 
  576         try:
  577             l.load_from_file(sbuf)
  578 
  579         except:
  580             l.close()
  581             raise base.FormatError
  582 
  583         l.close()
  584 
  585 
  586     def detect(self, input):
  587         "Checks if the handler can guarantee to use the data"
  588 
  589         try:
  590             self.check(input)
  591             return True
  592 
  593         except ( base.FormatError, base.VersionError ):
  594             return False
  595 
  596 
  597     def export_data(self, entrystore, password):
  598         "Exports data from an entrystore"
  599 
  600         # check and pad password
  601         if password is None:
  602             raise base.PasswordError
  603 
  604         # generate and compress XML
  605         data = RevelationXML.export_data(self, entrystore)
  606         data = zlib.compress(data.encode())
  607 
  608         # data needs to be padded to 512 bytes
  609         # We use Merkle-Damgard length padding (1 bit followed by 0 bits + size)
  610         # http://en.wikipedia.org/wiki/Merkle-Damg%C3%A5rd_hash_function
  611         padlen = 512 - (len(data) % 512)
  612 
  613         if padlen < 4:
  614             padlen = 512 + padlen
  615 
  616         if padlen > 4:
  617             data += bytes([128] + [0] * (padlen - 5))
  618 
  619         data += struct.pack("<I", padlen)
  620 
  621         # create a new luks file in memory
  622         buffer      = BytesIO()
  623         luksfile    = luks.LuksFile()
  624         luksfile.create(buffer, "aes", "cbc-essiv:sha256", "sha1", 16, 400)
  625 
  626         luksfile.set_key(0, password, 5000, 400)
  627 
  628         # encrypt the data
  629         luksfile.encrypt_data(0, data)
  630         buffer.seek(0)
  631 
  632         return buffer.read()
  633 
  634 
  635     def import_data(self, input, password):
  636         "Imports data into an entrystore"
  637 
  638         # check password
  639         if password is None:
  640             raise base.PasswordError
  641 
  642         # create a LuksFile
  643         buffer      = BytesIO(input)
  644         luksfile    = luks.LuksFile()
  645 
  646         try:
  647             luksfile.load_from_file(buffer)
  648 
  649         except:
  650             luksfile.close()
  651             buffer.close()
  652             raise base.FormatError
  653 
  654         slot = luksfile.open_any_key(password)
  655 
  656         if slot is None:
  657             luksfile.close()
  658             buffer.close()
  659             raise base.PasswordError
  660 
  661         data = luksfile.decrypt_data(0, luksfile.data_length())
  662 
  663         # remove the pad, and decompress
  664         padlen = struct.unpack("<I", data[-4:])[0]
  665         data = zlib.decompress(data[0:-padlen]).decode()
  666 
  667         if data.strip()[:5] != "<?xml":
  668             raise base.FormatError
  669 
  670         entrystore = RevelationXML.import_data(self, data)
  671 
  672         return entrystore
  673