├── setup.py ├── README.rst └── pcd.py /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | # Utility function to read the README file. 5 | # Used for the long_description. It's nice, because now 1) we have a top level 6 | # README file and 2) it's easier to type in the README file than to put a raw 7 | # string in below ... 8 | def read(fname): 9 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 10 | 11 | setup( 12 | name = "persistent_crypto_dict", 13 | version = "0.5.0", 14 | author = "Stefan Marsiske", 15 | author_email = "stefan.marsiske@gmail.com", 16 | summary = ("An encrypted persistent dictionary"), 17 | license = "BSD", 18 | keywords = "crypto collections container persistency cache", 19 | py_modules=['pcd' ], 20 | install_requires = ['pycryptodome'], 21 | url = "http://packages.python.org/persistent_crypto_dict", 22 | long_description=read('README.rst'), 23 | classifiers = ["Development Status :: 4 - Beta", 24 | "License :: OSI Approved :: BSD License", 25 | "Topic :: Security :: Cryptography", 26 | ], 27 | ) 28 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Persistent Crypto Dictionary 2 | **************************** 3 | 4 | This class implements a persistent dictionary using sqlite3 and 5 | encrypts the keys and the values of the dictionary in a way, that 6 | makes it very hard to bruteforce either the key or the values in the 7 | db. 8 | 9 | example usage:: 10 | >>> from pcd import PersistentCryptoDict 11 | >>> d=PersistentCryptoDict() 12 | >>> print d 13 | 14 | >>> print d['my key'] 15 | None 16 | >>> d['my key']='secret value' 17 | >>> print d['my key'] 18 | secret value 19 | >>> d['my key']='top secret value' 20 | >>> print d['my key'] 21 | top secret value 22 | 23 | Crypto 24 | ====== 25 | 26 | The key and the value in the dict is transformed according to the 27 | following algorithm (credit: dnet): 28 | 29 | Setting values 30 | ++++++++++++++ 31 | 1. we calculate they keyhash - a hmac-sha512(salt,key) 32 | 2. we split the key in half, the first half as a hexdigest (ascii), 33 | the second we keep as a binary 34 | 3. we use the second binary half from step 2 of the keyhash to encrypt 35 | the value 36 | 4. we use the ascii keyhash from step 2 as a key to the database, and 37 | the value is the encrypted result from step 3. 38 | 39 | Getting values 40 | ++++++++++++++ 41 | 1. we calculate they keyhash - a hmac-sha512(salt,key) 42 | 2. we split the key in half, the first half as a hexdigest (ascii), 43 | the second we keep as a binary 44 | 3. we query the database using the ascii keyhash from step 2 as a key 45 | 4. we use the second binary half from step 2 of the keyhash to decrypt 46 | the value 47 | 48 | The database contains only the following pairs of data: 49 | 50 | (hmac-sha512(key, salt).hexdigest()[:64], # key 51 | aes256-ofb(hmac-sha512(key, salt).digest()[32:], value)) # value 52 | 53 | we diligently obey Schneier's law: 54 | https://www.schneier.com/blog/archives/2011/04/schneiers_law.html, and 55 | thus we would consider the task to retrieve any meaningful data 56 | without huge rainbow tables from such a database a futile task. :) 57 | -------------------------------------------------------------------------------- /pcd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file is part of PersistentCryptoDict 5 | # 6 | # (C) 2012- by Stefan Marsiske, 7 | # All rights reserved. 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions 11 | # are met: 12 | # 13 | # Redistributions of source code must retain the above copyright 14 | # notice, this list of conditions and the following disclaimer. 15 | # Redistributions in binary form must reproduce the above 16 | # copyright notice, this list of conditions and the following 17 | # disclaimer in the documentation and/or other materials provided 18 | # with the distribution. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 30 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | # POSSIBILITY OF SUCH DAMAGE. 32 | # 33 | 34 | from __future__ import with_statement 35 | import hmac, hashlib 36 | from Crypto.Cipher import AES 37 | from contextlib import closing 38 | from base64 import b64encode, b64decode 39 | import sqlite3 40 | 41 | class sha512: 42 | digest_size = 64 43 | def new(self, inp=''): 44 | return hashlib.sha512(inp) 45 | 46 | CREATE_SQL = "CREATE TABLE if not exists pcd_urlcache (key TEXT PRIMARY KEY, value TEXT);" 47 | class PersistentCryptoDict(): 48 | def __init__(self, filename='pcd.db', salt=b"3j3,xiDS"): 49 | self.salt = salt.encode('utf8') if isinstance(salt,str) else salt 50 | self.db = sqlite3.connect(filename) 51 | # create table if not existing 52 | cursor = self.db.cursor() 53 | cursor.executescript(CREATE_SQL) 54 | self.db.commit() 55 | # reconnect to the new db 56 | cursor.close() 57 | self.db.close() 58 | self.db = sqlite3.connect(filename) 59 | 60 | def __setitem__(self, key, value): 61 | # calculate keys 62 | B, C = self.get_key(key) 63 | ciphertext = self.encrypt(C, value) 64 | # store B: base64(aes(C,value)) 65 | self.query_db('INSERT OR REPLACE INTO pcd_urlcache (key, value) VALUES (?, ?)', (B, ciphertext)) 66 | 67 | def __getitem__(self,key): 68 | # calculate keys 69 | B, C = self.get_key(key) 70 | value = self.query_db("SELECT value FROM pcd_urlcache WHERE key == ? LIMIT 1", (B,)) 71 | if value: 72 | return self.decrypt(C, value) 73 | 74 | def query_db(self,query, params=[]): 75 | with closing(self.db.cursor()) as cursor: 76 | cursor.execute(query, params) 77 | self.db.commit() 78 | return (cursor.fetchone() or [None])[0] 79 | 80 | def get_key(self,key): 81 | if(isinstance(key, str)): 82 | key=str.encode('utf8') 83 | A = hmac.new(self.salt, key, sha512()) 84 | return (A.hexdigest()[:64], 85 | A.digest()[32:]) 86 | 87 | def encrypt(self, C, value): 88 | # encrypt value with second half of MAC 89 | bsize=len(C) 90 | cipher = AES.new(C, AES.MODE_OFB, b'\x00'*16) 91 | # pad value 92 | value += chr(0x08) * (-len(value) % bsize) 93 | return b64encode(b''.join([cipher.encrypt(value[i*bsize:(i+1)*bsize].encode('utf8')) 94 | for i in range(len(value)//bsize)])) 95 | 96 | def decrypt(self, C, value): 97 | # decode value 98 | value=b64decode(value) 99 | cipher = AES.new(C, AES.MODE_OFB, b'\x00'*16) 100 | bsize=len(C) 101 | return b''.join([cipher.decrypt(value[i*bsize:(i+1)*bsize]) 102 | for i in range(len(value)//bsize)]).rstrip(b'\x08') 103 | 104 | if __name__ == "__main__": 105 | import sys 106 | d=PersistentCryptoDict('pcd.db') 107 | if len(sys.argv)==3: 108 | d[sys.argv[1]]=sys.argv[2] 109 | elif len(sys.argv)==2: 110 | print(d[sys.argv[1]]) 111 | else: 112 | print(d['my key']) 113 | d['my key']='secret value' 114 | print(d['my key']) 115 | d['my key']='top secret value' 116 | print(d['my key']) 117 | --------------------------------------------------------------------------------