"Fossies" - the Fresh Open Source Software Archive

Member "Tardis-1.2.1/src/Tardis/TardisCrypto.py" (9 Jun 2021, 18794 Bytes) of package /linux/privat/Tardis-1.2.1.tar.gz:


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 "TardisCrypto.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 1.1.5_vs_1.2.1.

    1 # vim: set et sw=4 sts=4 fileencoding=utf-8:
    2 #
    3 # Tardis: A Backup System
    4 # Copyright 2013-2020, Eric Koldinger, All Rights Reserved.
    5 # kolding@washington.edu
    6 #
    7 # Redistribution and use in source and binary forms, with or without
    8 # modification, are permitted provided that the following conditions are met:
    9 #
   10 #     * Redistributions of source code must retain the above copyright
   11 #       notice, this list of conditions and the following disclaimer.
   12 #     * Redistributions in binary form must reproduce the above copyright
   13 #       notice, this list of conditions and the following disclaimer in the
   14 #       documentation and/or other materials provided with the distribution.
   15 #     * Neither the name of the copyright holder nor the
   16 #       names of its contributors may be used to endorse or promote products
   17 #       derived from this software without specific prior written permission.
   18 #
   19 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   20 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   21 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
   22 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
   23 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
   24 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
   25 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
   26 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
   27 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
   28 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   29 # POSSIBILITY OF SUCH DAMAGE.
   30 
   31 import hashlib
   32 import hmac
   33 import os
   34 import os.path
   35 import sys
   36 import base64
   37 import binascii
   38 
   39 from Cryptodome.Cipher import AES, ChaCha20_Poly1305
   40 from Cryptodome.Protocol.KDF import PBKDF2, scrypt
   41 from Cryptodome.Util.Padding import pad, unpad
   42 import Cryptodome.Random
   43 import srp
   44 
   45 import Tardis.Defaults as Defaults
   46 from functools import reduce
   47 
   48 defaultCryptoScheme = 4
   49 maxCryptoScheme = 4
   50 noCryptoScheme = 0
   51 
   52 def getCrypto(scheme, password, client=None, fsencoding=sys.getfilesystemencoding()):
   53     scheme = int(scheme)
   54 
   55     if scheme == 0:
   56         return Crypto_Null(password, client, fsencoding)
   57     elif scheme == 1:
   58         return Crypto_AES_CBC_HMAC__AES_ECB(password, client, fsencoding)
   59     elif scheme == 2:
   60         return Crypto_AES_CBC_HMAC__AES_SIV(password, client, fsencoding)
   61     elif scheme == 3:
   62         return Crypto_AES_GCM__AES_SIV(password, client, fsencoding)
   63     elif scheme == 4:
   64         return Crypto_ChaCha20_Poly1305__AES_SIV(password, client, fsencoding)
   65     else:
   66         raise Exception(f"Unknown Crypto Scheme: {scheme}")
   67 
   68 
   69 def getCryptoNames(scheme=None):
   70     if scheme is None:
   71         x = range(0, 5)
   72     else:
   73         x = range(scheme, scheme + 1)
   74 
   75     names = []
   76     for i in x:
   77         crypto = getCrypto(i, 'password')
   78         names.append(f"{i}: {crypto._cryptoName}")
   79     return '\n'.join(names)
   80 
   81 
   82 class HasherMixin:
   83     hasher = None
   84 
   85     def __init__(self, cipher, hasher):
   86         self.hasher = hasher
   87         super().__init__(cipher)
   88 
   89     def update(self, data):
   90         self.hasher.update(data)
   91 
   92     def encrypt(self, data):
   93         ct = super().encrypt(data)
   94         if ct:
   95             self.hasher.update(ct)
   96         return ct
   97 
   98     def finish(self):
   99         ct = super().finish()
  100         self.hasher.update(ct)
  101         return ct
  102 
  103     def decrypt(self, ct, last=False):
  104         self.hasher.update(ct)
  105         plain = super().decrypt(ct, last)
  106         return plain
  107 
  108     def digest(self):
  109         return self.hasher.digest()
  110 
  111     def verify(self, tag):
  112         if not hmac.compare_digest(tag, self.hasher.digest()):
  113             raise ValueError("MAC did not match")
  114 
  115     def getDigestSize(self):
  116         return self.hasher.digest_size
  117 
  118 class BlockEncryptor:
  119     done = False
  120     prev = None
  121     iv = None
  122 
  123     def __init__(self, cipher):
  124         self.cipher = cipher
  125         self.iv = cipher.iv
  126         self.update(self.iv)
  127 
  128     def update(self, data):
  129         self.cipher.update(data)
  130 
  131     def encrypt(self, data):
  132         if self.done:
  133             raise Exception("Already completed")
  134         if self.prev:
  135             data = self.prev + data
  136             self.prev = None
  137         remainder = len(data) % self.cipher.block_size
  138         if remainder != 0:
  139             self.prev = data[-remainder:]
  140             data = data[0:-remainder]
  141         if data:
  142             ret = self.cipher.encrypt(data)
  143             if ret:
  144                 return ret
  145             else:
  146                 return b''
  147         else:
  148             return b''
  149 
  150     def decrypt(self, data, last=False):
  151         if self.done:
  152             raise Exception("Already completed")
  153         if self.prev:
  154             data = self.prev + data
  155             self.prev = None
  156         remainder = len(data) % self.cipher.block_size
  157         if remainder != 0:
  158             self.prev = data[-remainder:]
  159             data = data[0:-remainder]
  160         if data:
  161             output = self.cipher.decrypt(data)
  162             if last:
  163                 self.done = True
  164                 output = unpad(output, self.cipher.block_size)
  165             return output
  166         else:
  167             return b''
  168 
  169     def finish(self):
  170         if self.done:
  171             raise Exception("Already completed")
  172         self.done = True
  173         if self.prev:
  174             padded = pad(self.prev, self.cipher.block_size)
  175         else:
  176             padded = pad(b'', self.cipher.block_size)
  177         return self.cipher.encrypt(padded)
  178 
  179     def digest(self):
  180         if not self.done and self.prev:
  181             raise Exception("Not yet finished encrypting")
  182         self.done = True
  183         return self.cipher.digest()
  184 
  185     def verify(self, tag):
  186         self.cipher.verify(tag)
  187 
  188     def getDigestSize(self):
  189         # these all seem to be 128 hashers
  190         return 16
  191 
  192 class HashingBlockEncryptor(HasherMixin, BlockEncryptor):
  193     def __init__(self, cipher, hasher):
  194         HasherMixin.__init__(self, cipher, hasher)
  195 
  196 class StreamEncryptor:
  197     done = False
  198     iv = None
  199 
  200     def __init__(self, cipher):
  201         self.cipher = cipher
  202         self.iv = cipher.nonce
  203         self.update(self.iv)
  204 
  205     def update(self, data):
  206         self.cipher.update(data)
  207 
  208     def encrypt(self, data):
  209         return self.cipher.encrypt(data)
  210 
  211     def decrypt(self, data, last=False):
  212         return self.cipher.decrypt(data)
  213 
  214     def finish(self):
  215         return b''
  216 
  217     def digest(self):
  218         return self.cipher.digest()
  219 
  220     def verify(self, tag):
  221         self.cipher.verify(tag)
  222 
  223     def getDigestSize(self):
  224         # these all seem to be 128 hashers
  225         return 16
  226 
  227 def HashingStreamEncryptor(HasherMixin, StreamEncryptor):
  228     pass
  229 
  230 
  231 class NullEncryptor:
  232     iv = b''
  233 
  234     def encrypt(self, data):
  235         return data
  236     def decrypt(self, data, last=False):
  237         return data
  238     def finish(self):
  239         return b''
  240     def digest(self):
  241         return b''
  242     def verify(self, tag):
  243         pass
  244 
  245 class Crypto_Null:
  246     _cryptoScheme = '0'
  247     _cryptoName   = 'None'
  248     _contentKey  = None
  249     _filenameKey = None
  250     _keyKey      = None
  251     _random      = None
  252     _filenameEnc = None
  253     _fsEncoding  = None
  254     _blocksize   = AES.block_size
  255     _keysize     = AES.key_size[-1]                                              # last (largest) acceptable _keysize
  256     _altchars    = b'#@'
  257 
  258     ivLength    = 0
  259 
  260     class NullCipher():
  261         def encrypt(data):
  262             return data
  263 
  264     def __init__(self, password=None, client=None, fsencoding=sys.getfilesystemencoding()):
  265         pass
  266 
  267     def getCryptoScheme(self):
  268         return self._cryptoScheme
  269 
  270     def encrypting(self):
  271         return False
  272 
  273     def getContentCipher(self, iv):
  274         return NullCipher()
  275 
  276     def getContentEncryptor(self, iv=None):
  277         return NullEncryptor()
  278 
  279     def encryptFilename(self, name):
  280         return name
  281 
  282     def decryptFilename(self, name):
  283         if isinstance(name, bytes):
  284             return name.decode('utf8')
  285         else:
  286             return name
  287 
  288 
  289     def getHash(self, func=hashlib.md5):
  290         return func()
  291 
  292     def getIV(self):
  293         return None
  294 
  295     def pad(self, data, length=None):
  296         return data
  297 
  298     def unpad(self, data):
  299         return data
  300 
  301     def checkpad(self, data):
  302         pass
  303 
  304     def padzero(self, data, length=None):
  305         return 
  306 
  307     def encryptPath(self, path):
  308         return path
  309 
  310     def decryptPath(self, path):
  311         return path
  312 
  313     def encryptFilename(self, name):
  314         return name
  315 
  316     def genKeys(self):
  317         pass
  318 
  319     def setKeys(self, filenameKey, contentKey):
  320         pass
  321 
  322     def getKeys(self):
  323         return (None, None)
  324 
  325 
  326 
  327 
  328 class Crypto_AES_CBC_HMAC__AES_ECB(Crypto_Null):
  329     """ Original Crypto Scheme.
  330     AES-256 CBC encyrption for files, with HMAC/SHA-512 for authentication.
  331     AES-256 ECB for filenames with no authentictaion.
  332     No authentication of key values.
  333     For backwards compatibility only.
  334     """
  335     _cryptoScheme = '1'
  336     _cryptoName   = 'AES-CBC-HMAC/AES-ECB/PBKDF2'
  337     _contentKey  = None
  338     _filenameKey = None
  339     _keyKey      = None
  340     _random      = None
  341     _filenameEnc = None
  342     _fsEncoding  = None
  343     _blocksize   = AES.block_size
  344     _keysize     = AES.key_size[-1]                                              # last (largest) acceptable _keysize
  345     _altchars    = b'#@'
  346 
  347     ivLength    = _blocksize
  348 
  349     def __init__(self, password, client=None, fsencoding=sys.getfilesystemencoding()):
  350         self._random = Cryptodome.Random.new()
  351         if client is None:
  352             client = Defaults.getDefault('TARDIS_CLIENT')
  353 
  354         self.client = bytes(client, 'utf8')
  355         self.salt = hashlib.sha256(self.client).digest()
  356         keys = self.genKeyKey(password)
  357         self._keyKey     = keys[0:self._keysize]                                      # First 256 bit key
  358 
  359         self._fsEncoding = fsencoding
  360 
  361     def encrypting(self):
  362         return True
  363 
  364     def genKeyKey(self, password):
  365         return PBKDF2(password, self.salt, count=20000, dkLen=self._keysize * 2)      # 2x256 bit keys
  366 
  367     def getContentCipher(self, iv):
  368         if iv is None:
  369             iv = self.getIV()
  370         return AES.new(self._contentKey, AES.MODE_CBC, IV=iv)
  371 
  372     def getContentEncryptor(self, iv=None):
  373         return HashingBlockEncryptor(self.getContentCipher(iv), self.getHash(hashlib.sha512))
  374 
  375     def getHash(self, func=hashlib.md5):
  376         return hmac.new(self._contentKey, digestmod=func)
  377 
  378     def getIV(self):
  379         return self._random.read(self.ivLength)
  380 
  381     def pad(self, data, length=None):
  382         if length is None:
  383             length = len(data)
  384         pad = self._blocksize - (length % self._blocksize)
  385         data += bytes(chr(pad) * pad, 'utf8')
  386         return data
  387 
  388     def unpad(self, data):
  389         #if validate:
  390             #self.checkpad(data)
  391         l = data[-1]
  392         x = len(data) - l
  393         return data[:x]
  394 
  395     def checkpad(self, data):
  396         l = data[-1]
  397         # Make sure last L bytes are all set to L
  398         pad = chr(l) * l
  399         if data[-l:] != pad:
  400             raise Exception("Invalid padding: %s (%d)", binascii.hexlify(data[-l:]), l)
  401 
  402     def padzero(self, x):
  403         remainder = len(x) % self._blocksize
  404         if remainder == 0:
  405             return x
  406         else:
  407             return x + (self._blocksize - remainder) * b'\0'
  408 
  409     def encryptPath(self, path):
  410         rooted = False
  411         comps = path.split(os.sep)
  412         if comps[0] == '':
  413             rooted = True
  414             comps.pop(0)
  415         enccomps = [self.encryptFilename(x) for x in comps]
  416         encpath = reduce(os.path.join, enccomps)
  417         if rooted:
  418             encpath = os.path.join(os.sep, encpath)
  419         return encpath
  420 
  421     def decryptPath(self, path):
  422         rooted = False
  423         comps = path.split(os.sep)
  424         if comps[0] == '':
  425             rooted = True
  426             comps.pop(0)
  427         enccomps = [self.decryptFilename(x) for x in comps]
  428         encpath = reduce(os.path.join, enccomps)
  429         if rooted:
  430             encpath = os.path.join(os.sep, encpath)
  431         return encpath
  432 
  433     def encryptFilename(self, name):
  434         n = self.padzero(bytes(name, 'utf8'))
  435         return str(base64.b64encode(self._filenameEnc.encrypt(n), self._altchars), 'utf8')
  436 
  437     def decryptFilename(self, name):
  438         return str(self._filenameEnc.decrypt(base64.b64decode(name, self._altchars)), 'utf8').rstrip('\0')
  439 
  440     def genKeys(self):
  441         self._contentKey  = self._random.read(self._keysize)
  442         self._filenameKey = self._random.read(self._keysize)
  443         self._filenameEnc = AES.new(self._filenameKey, AES.MODE_ECB)
  444 
  445     def setKeys(self, filenameKey, contentKey):
  446         cipher = AES.new(self._keyKey, AES.MODE_ECB)
  447         self._contentKey  = cipher.decrypt(base64.b64decode(contentKey))
  448         self._filenameKey = cipher.decrypt(base64.b64decode(filenameKey))
  449         self._filenameEnc = AES.new(self._filenameKey, AES.MODE_ECB)
  450 
  451     def getKeys(self):
  452         if self._filenameKey and self._contentKey:
  453             cipher = AES.new(self._keyKey, AES.MODE_ECB)
  454             _contentKey  = str(base64.b64encode(cipher.encrypt(self._contentKey)), 'utf8')
  455             _filenameKey = str(base64.b64encode(cipher.encrypt(self._filenameKey)), 'utf8')
  456             return (_filenameKey, _contentKey)
  457         else:
  458             return (None, None)
  459 
  460 
  461 class Crypto_AES_CBC_HMAC__AES_SIV(Crypto_AES_CBC_HMAC__AES_ECB):
  462     """
  463     Improved crypto scheme.
  464     Still uses AES-256 CBC with HMAC/SHA-512 Authentication.
  465     Changes Filename encryption to using AES-256 SIV encryption and authentication.  On upgraded systems (ie,
  466     those formerly using Crypto_AES_CBC_HMAC__AES_ECB), AES-128 SIV encryption and authentication is used.
  467     Uses AES-128 SIV encryption and validation on the keys.
  468     """
  469     _cryptoScheme = '2'
  470     _cryptoName   = 'AES-CBC-HMAC/AES-SIV/scrypt'
  471 
  472     def __init__(self, password, client=None, fsencoding=sys.getfilesystemencoding()):
  473         super().__init__(password, client, fsencoding)
  474 
  475     def genKeyKey(self, password):
  476         return scrypt(password, self.salt, 32, 65536, 8, 1)
  477 
  478     def _encryptSIV(self, key, value, name=None):
  479         cipher = AES.new(key, AES.MODE_SIV)
  480         if name:
  481             cipher.update(name.encode('utf8'))
  482         (ctext, tag) = cipher.encrypt_and_digest(value)
  483         return ctext + tag
  484 
  485     def _decryptSIV(self, key, value, name=None):
  486         cipher = AES.new(key, AES.MODE_SIV)
  487         if name:
  488             cipher.update(name.encode('utf8'))
  489 
  490         ctext = value[0:-cipher.block_size]
  491         tag   = value[-cipher.block_size:]
  492         return cipher.decrypt_and_verify(ctext, tag)
  493 
  494     def encryptFilename(self, name):
  495         encrypted = self._encryptSIV(self._filenameKey, name.encode('utf8'))
  496         return base64.b64encode(encrypted, self._altchars).decode('utf8')
  497 
  498     def decryptFilename(self, name):
  499         return self._decryptSIV(self._filenameKey, base64.b64decode(name, self._altchars)).decode('utf8')
  500 
  501     def genKeys(self):
  502         self._contentKey  = self._random.read(self._keysize)
  503         self._filenameKey = self._random.read(2 * self._keysize)
  504 
  505     def setKeys(self, filenameKey, contentKey):
  506         ckey = base64.b64decode(contentKey)
  507         fkey = base64.b64decode(filenameKey)
  508 
  509         try:
  510             self._contentKey   = self._decryptSIV(self._keyKey, ckey, "ContentKey")
  511             self._filenameKey  = self._decryptSIV(self._keyKey, fkey, "FilenameKey")
  512         except ValueError as e:
  513             raise ValueError(f"Keys failed to authenticate: {str(e)}")
  514 
  515     def getKeys(self):
  516         if self._filenameKey and self._contentKey:
  517             _contentKey  = str(base64.b64encode(self._encryptSIV(self._keyKey, self._contentKey, "ContentKey")), 'utf8')
  518             _filenameKey = str(base64.b64encode(self._encryptSIV(self._keyKey, self._filenameKey, "FilenameKey")), 'utf8')
  519             return (_filenameKey, _contentKey)
  520         else:
  521             return (None, None)
  522 
  523 
  524 class Crypto_AES_GCM__AES_SIV(Crypto_AES_CBC_HMAC__AES_SIV):
  525     """
  526     Improved crypto scheme.
  527     Still uses AES-256 GCM for encryption and authentication
  528     Uses ASE-256 SIV encryption and authentaction for files
  529     """
  530     _cryptoScheme = '3'
  531     _cryptoName   = 'AES-GCM/AES-SIV/scrypt'
  532 
  533     def __init__(self, password, client=None, fsencoding=sys.getfilesystemencoding()):
  534         super().__init__(password, client, fsencoding)
  535 
  536     def getContentCipher(self, iv=None):
  537         if iv is None:
  538             iv = self.getIV()
  539         return AES.new(self._contentKey, AES.MODE_GCM, nonce=iv)
  540 
  541     def getContentEncryptor(self, iv=None):
  542         return StreamEncryptor(self.getContentCipher(iv))
  543 
  544 
  545 class Crypto_ChaCha20_Poly1305__AES_SIV(Crypto_AES_CBC_HMAC__AES_SIV):
  546     """
  547     Improved crypto scheme.
  548     Uses ChaCha20/Poly1305  for encryption and authentication
  549     Uses ASE-256 SIV encryption and authentaction for files
  550     """
  551     _cryptoScheme = '4'
  552     _cryptoName   = 'ChaCha20-Poly1305/AES-SIV/scrypt'
  553 
  554     ivLength    = 12
  555 
  556     def __init__(self, password, client=None, fsencoding=sys.getfilesystemencoding()):
  557         super().__init__(password, client, fsencoding)
  558 
  559     def getContentCipher(self, iv):
  560         return ChaCha20_Poly1305.new(key=self._contentKey, nonce=iv)
  561 
  562     def getContentEncryptor(self, iv=None):
  563         return StreamEncryptor(self.getContentCipher(iv))
  564 
  565 
  566 if __name__ == '__main__':
  567     string = b'abcdefghijknlmnopqrstuvwxyz' + \
  568              b'ABCDEFGHIJKNLMNOPQRSTUVWXYZ' + \
  569              b'1234567890!@#$%&*()[]{}-_,.<>'
  570 
  571     #print(string)
  572     path = '/srv/home/kolding/this is a/test.onlyATest/X'
  573     fname ='test.only1234'
  574 
  575     print(getCryptoNames())
  576     print(getCryptoNames(3))
  577 
  578     for i in range(0, 5):
  579         print(f"\nTesting {i}")
  580         try:
  581             c = getCrypto(i, 'PassWordXYZ123')
  582             print(f"Type: {c._cryptoName}")
  583             c.genKeys()
  584 
  585             print(f"DigestSize: {c.getDigestSize()}")
  586 
  587             print("--- Testing Content Encryptor ---")
  588             e = c.getContentEncryptor()
  589             d = c.getContentEncryptor(e.iv)
  590 
  591             ct = e.encrypt(string) + e.finish()
  592             pt = d.decrypt(ct, True)
  593 
  594             assert(pt == string)
  595             d.verify(e.digest())
  596 
  597             print("--- Testing Filename Encryptor ---")
  598             cf = c.encryptFilename(fname)
  599             #print(cf)
  600             df = c.decryptFilename(cf)
  601 
  602             assert(fname == df)
  603 
  604             print("--- Testing FilePath Encryptor ---")
  605             cp = c.encryptPath(path)
  606             #print(cp)
  607             dp = c.decryptPath(cp)
  608 
  609             assert(path == dp)
  610             
  611         except Exception as e:
  612             print(f"Caught exception: {e}")
  613             print(e)
  614 
  615 
  616