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