1#============================================================================ 2# This file is part of Pwman3. 3# 4# Pwman3 is free software; you can redistribute it and/or modify 5# it under the terms of the GNU General Public License, version 2 6# as published by the Free Software Foundation; 7# 8# Pwman3 is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License 14# along with Pwman3; if not, write to the Free Software 15# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 16#============================================================================ 17# Copyright (C) 2006 Ivan Kelly <ivan@ivankelly.net> 18#============================================================================ 19 20"""Encryption Module used by PwmanDatabase 21 22Supports AES, ARC2, Blowfish, CAST, DES, DES3, IDEA, RC5. 23 24Usage: 25import pwman.util.crypto.CryptoEngine as CryptoEngine 26 27class myCallback(CryptoEngine.Callback): 28 def execute(self): 29 return "mykey" 30 31params = {'encryptionAlgorithm': 'AES', 32 'encryptionCallback': callbackFunction} 33 34CryptoEngine.init(params) 35 36crypto = CryptoEngine.get() 37ciphertext = crypto.encrypt("plaintext") 38plaintext = cyypto.decrypt(ciphertext) 39 40""" 41from Crypto.Cipher import * 42from Crypto.Util.randpool import RandomPool 43from pwman.util.callback import Callback 44import pwman.util.config as config 45import cPickle 46import time 47 48_instance = None 49 50# Use this to tell if crypto is successful or not 51_TAG = "PWMANCRYPTO" 52 53class CryptoException(Exception): 54 """Generic Crypto Exception.""" 55 def __init__(self, message): 56 self.message = message 57 def __str__(self): 58 return "CryptoException: " + self.message 59 60class CryptoUnsupportedException(CryptoException): 61 """Unsupported feature requested.""" 62 def __str__(self): 63 return "CryptoUnsupportedException: " +self.message 64 65class CryptoBadKeyException(CryptoException): 66 """Encryption key is incorrect.""" 67 def __str__(self): 68 return "CryptoBadKeyException: " + self.message 69 70class CryptoNoKeyException(CryptoException): 71 """No key has been initalised.""" 72 def __str__(self): 73 return "CryptoNoKeyException: " + self.message 74 75class CryptoNoCallbackException(CryptoException): 76 """No Callback has been set.""" 77 def __str__(self): 78 return "CryptoNoCallbackException: " + self.message 79 80class CryptoPasswordMismatchException(CryptoException): 81 """Entered passwords do not match.""" 82 def __str__(self): 83 return "CryptoPasswordMismatchException: " + self.message 84 85 86class CryptoEngine: 87 """Cryptographic Engine""" 88 _timeoutcount = 0 89 _instance = None 90 _callback = None 91 92 def get(cls): 93 """ 94 get() -> CryptoEngine 95 Return an instance of CryptoEngine. 96 If no instance is found, a CryptoException is raised. 97 """ 98 if (CryptoEngine._instance == None): 99 algo = config.get_value("Encryption", "algorithm") 100 if algo == "Dummy": 101 CryptoEngine._instance = DummyCryptoEngine() 102 else: 103 CryptoEngine._instance = CryptoEngine() 104 return CryptoEngine._instance 105 get = classmethod(get) 106 107 def __init__(self): 108 """Initialise the Cryptographic Engine 109 110 params is a dictionary. Valid keys are: 111 algorithm: Which cipher to use 112 callback: Callback class. 113 keycrypted: This should be set by the database layer. 114 timeout: Time after which key will be forgotten. 115 Default is -1 (disabled). 116 """ 117 algo = config.get_value("Encryption", "algorithm") 118 if len(algo) > 0: 119 self._algo = algo 120 else: 121 raise CryptoException("Parameters missing [%s]" % (e) ) 122 123 callback = config.get_value("Encryption", "callback") 124 if isinstance(callback, Callback): 125 self._callback = callback 126 else: 127 self._callback = None 128 129 keycrypted = config.get_value("Encryption", "keycrypted") 130 if len(keycrypted) > 0: 131 self._keycrypted = keycrypted 132 else: 133 self._keycrypted = None 134 135 timeout = config.get_value("Encryption", "timeout") 136 if timeout.isdigit(): 137 self._timeout = timeout 138 else: 139 self._timeout = -1 140 self._cipher = None 141 142 def encrypt(self, obj): 143 """ 144 encrypt(obj) -> ciphertext 145 Encrypt obj and return its ciphertext. obj must be a picklable class. 146 Can raise a CryptoException and CryptoUnsupportedException""" 147 cipher = self._getcipher() 148 plaintext = self._preparedata(obj, cipher.block_size) 149 ciphertext = cipher.encrypt(plaintext) 150 return str(ciphertext).encode('base64') 151 152 def decrypt(self, ciphertext): 153 """ 154 decrypt(ciphertext) -> obj 155 Decrypt ciphertext and returns the obj that was encrypted. 156 If key is bad, a CryptoBadKeyException is raised 157 Can also raise a CryptoException and CryptoUnsupportedException""" 158 cipher = self._getcipher() 159 ciphertext = str(ciphertext).decode('base64') 160 plaintext = cipher.decrypt(ciphertext) 161 return self._retrievedata(plaintext) 162 163 def set_cryptedkey(self, key): 164 self._keycrypted = key 165 166 def get_cryptedkey(self): 167 return self._keycrypted 168 169 def set_callback(self, callback): 170 self._callback = callback 171 172 def get_callback(self): 173 return self._callback 174 175 def changepassword(self): 176 """ 177 Creates a new key. The key itself is actually stored in 178 the database in crypted form. This key is encrypted using the 179 password that the user provides. This makes it easy to change the 180 password for the database. 181 If oldKeyCrypted is none, then a new password is generated.""" 182 if (self._callback == None): 183 raise CryptoNoCallbackException("No call back class has been specified") 184 if (self._keycrypted == None): 185 # Generate a new key, 32 bits in length, if that's 186 # too long for the Cipher, _getCipherReal will sort it out 187 random = RandomPool() 188 key = str(random.get_bytes(32)).encode('base64') 189 else: 190 password = self._callback.getsecret("Please enter your current password") 191 cipher = self._getcipher_real(password, self._algo) 192 plainkey = cipher.decrypt(str(self._keycrypted).decode('base64')) 193 key = self._retrievedata(plainkey) 194 newpassword1 = self._callback.getsecret("Please enter your new password"); 195 newpassword2 = self._callback.getsecret("Please enter your new password again"); 196 if (newpassword1 != newpassword2): 197 raise CryptoPasswordMismatchException("Passwords do not match") 198 newcipher = self._getcipher_real(newpassword1, self._algo) 199 self._keycrypted = str(newcipher.encrypt(self._preparedata(key, newcipher.block_size))).encode('base64') 200 201 # we also want to create the cipher if there isn't one already 202 # so this CryptoEngine can be used from now on 203 if (self._cipher == None): 204 self._cipher = self._getcipher_real(str(key).decode('base64'), self._algo) 205 CryptoEngine._timeoutcount = time.time() 206 207 return self._keycrypted 208 209 def alive(self): 210 if (self._cipher != None): 211 return True 212 else: 213 return False 214 215 def forget(self): 216 self._cipher = None 217 218 def _getcipher(self): 219 if (self._cipher != None 220 and (self._timeout == -1 221 or (time.time() - CryptoEngine._timeoutcount) < self._timeout)): 222 return self._cipher 223 if (self._callback == None): 224 raise CryptoNoCallbackException("No Callback exception") 225 if (self._keycrypted == None): 226 raise CryptoNoKeyException("Encryption key has not been generated") 227 228 password = self._callback.getsecret("Please enter your password") 229 tmpcipher = self._getcipher_real(password, self._algo) 230 plainkey = tmpcipher.decrypt(str(self._keycrypted).decode('base64')) 231 key = self._retrievedata(plainkey) 232 233 self._cipher = self._getcipher_real(str(key).decode('base64'), self._algo) 234 235 CryptoEngine._timeoutcount = time.time() 236 return self._cipher 237 238 239 def _getcipher_real(self, key, algo): 240 if (algo == "AES"): 241 key = self._padkey(key, [16, 24, 32]) 242 cipher = AES.new(key, AES.MODE_ECB) 243 elif (algo == 'ARC2'): 244 cipher = ARC2.new(key, ARC2.MODE_ECB) 245 elif (algo == 'ARC4'): 246 raise CryptoUnsupportedException("ARC4 is currently unsupported") 247 elif (algo == 'Blowfish'): 248 cipher = Blowfish.new(key, Blowfish.MODE_ECB) 249 elif (algo == 'CAST'): 250 cipher = CAST.new(key, CAST.MODE_ECB) 251 elif (algo == 'DES'): 252 self._padkey(key, [8]) 253 cipher = DES.new(key, DES.MODE_ECB) 254 elif (algo == 'DES3'): 255 key = self._padkey(key, [16, 24]) 256 cipher = DES3.new(key, DES3.MODE_ECB) 257 elif (algo == 'IDEA'): 258 key = self._padkey(key, [16]) 259 cipher = IDEA.new(key, IDEA.MODE_ECB) 260 elif (algo == 'RC5'): 261 cipher = RC5.new(key, RC5.MODE_ECB) 262 elif (algo == 'XOR'): 263 raise CryptoUnsupportedException("XOR is currently unsupported") 264 else: 265 raise CryptoException("Invalid algorithm specified") 266 return cipher 267 268 def _padkey(self, key, acceptable_lengths): 269 maxlen = max(acceptable_lengths) 270 keylen = len(key) 271 if (keylen > maxlen): 272 return key[0:maxlen] 273 acceptable_lengths.sort() 274 acceptable_lengths.reverse() 275 newkeylen = None 276 for i in acceptable_lengths: 277 if (i < keylen): 278 break 279 newkeylen = i 280 return key.ljust(newkeylen) 281 282 def _preparedata(self, obj, blocksize): 283 plaintext = cPickle.dumps(obj) 284 plaintext = _TAG + plaintext 285 numblocks = (len(plaintext)/blocksize) + 1 286 newdatasize = blocksize*numblocks 287 return plaintext.ljust(newdatasize) 288 289 def _retrievedata(self, plaintext): 290 if (plaintext.startswith(_TAG)): 291 plaintext = plaintext[len(_TAG):] 292 else: 293 raise CryptoBadKeyException("Error decrypting, bad key") 294 return cPickle.loads(plaintext) 295 296 297class DummyCryptoEngine(CryptoEngine): 298 """Dummy CryptoEngine used when database doesn't ask for encryption. 299 Only for testing and debugging the DB drivers really.""" 300 def __init__(self): 301 pass 302 303 def encrypt(self, obj): 304 """Return the object pickled.""" 305 return cPickle.dumps(obj) 306 307 def decrypt(self, ciphertext): 308 """Unpickle the object.""" 309 return cPickle.loads(str(ciphertext)) 310 311 def changepassword(self): 312 return '' 313