root / elixir / trunk / elixir / ext / encrypted.py

Revision 446, 4.2 kB (checked in by ged, 4 years ago)

- Fixed encrypted extension to not encrypt several times an instance attributes

when that instance is flushed several times before being expunged from the
session.

- added note about how the encrypted extension works

Line 
1'''
2An encryption plugin for Elixir utilizing the excellent PyCrypto library, which
3can be downloaded here: http://www.amk.ca/python/code/crypto
4
5Values for columns that are specified to be encrypted will be transparently
6encrypted and safely encoded for storage in a unicode column using the powerful
7and secure Blowfish Cipher using a specified "secret" which can be passed into
8the plugin at class declaration time.
9
10Example usage:
11
12.. sourcecode:: python
13
14    from elixir import *
15    from elixir.ext.encrypted import acts_as_encrypted
16
17    class Person(Entity):
18        name = Field(Unicode)
19        password = Field(Unicode)
20        ssn = Field(Unicode)
21        acts_as_encrypted(for_fields=['password', 'ssn'],
22                          with_secret='secret')
23
24The above Person entity will automatically encrypt and decrypt the password and
25ssn columns on save, update, and load.  Different secrets can be specified on
26an entity by entity basis, for added security.
27
28**Important note**: instance attributes are encrypted in-place. This means that
29if one of the encrypted attributes of an instance is accessed after the
30instance has been flushed to the database (and thus encrypted), the value for
31that attribute will be crypted in the in-memory object in addition to the
32database row.
33'''
34
35from Crypto.Cipher import Blowfish
36from elixir.statements import Statement
37from sqlalchemy.orm import MapperExtension, EXT_CONTINUE, EXT_STOP
38
39try:
40    from sqlalchemy.orm import EXT_PASS
41    SA05orlater = False
42except ImportError:
43    SA05orlater = True
44
45__all__ = ['acts_as_encrypted']
46__doc_all__ = []
47
48
49#
50# encryption and decryption functions
51#
52
53def encrypt_value(value, secret):
54    return Blowfish.new(secret, Blowfish.MODE_CFB) \
55                   .encrypt(value).encode('string_escape')
56
57def decrypt_value(value, secret):
58    return Blowfish.new(secret, Blowfish.MODE_CFB) \
59                   .decrypt(value.decode('string_escape'))
60
61
62#
63# acts_as_encrypted statement
64#
65
66class ActsAsEncrypted(object):
67
68    def __init__(self, entity, for_fields=[], with_secret='abcdef'):
69
70        def perform_encryption(instance, encrypt=True):
71            encrypted = getattr(instance, '_elixir_encrypted', None)
72            if encrypted is encrypt:
73                # skipping encryption or decryption, as it is already done
74                return
75            else:
76                # marking instance as already encrypted/decrypted
77                instance._elixir_encrypted = encrypt
78
79            if encrypt:
80                func = encrypt_value
81            else:
82                func = decrypt_value
83
84            for column_name in for_fields:
85                current_value = getattr(instance, column_name)
86                if current_value:
87                    setattr(instance, column_name,
88                            func(current_value, with_secret))
89
90        def perform_decryption(instance):
91            perform_encryption(instance, encrypt=False)
92
93        class EncryptedMapperExtension(MapperExtension):
94
95            def before_insert(self, mapper, connection, instance):
96                perform_encryption(instance)
97                return EXT_CONTINUE
98
99            def before_update(self, mapper, connection, instance):
100                perform_encryption(instance)
101                return EXT_CONTINUE
102
103            if SA05orlater:
104                def reconstruct_instance(self, mapper, instance):
105                    perform_decryption(instance)
106                    # no special return value is required for
107                    # reconstruct_instance, but you never know...
108                    return EXT_CONTINUE
109            else:
110                def populate_instance(self, mapper, selectcontext, row,
111                                      instance, *args, **kwargs):
112                    mapper.populate_instance(selectcontext, instance, row,
113                                             *args, **kwargs)
114                    perform_decryption(instance)
115                    # EXT_STOP because we already did populate the instance and
116                    # the normal processing should not happen
117                    return EXT_STOP
118
119        # make sure that the entity's mapper has our mapper extension
120        entity._descriptor.add_mapper_extension(EncryptedMapperExtension())
121
122
123acts_as_encrypted = Statement(ActsAsEncrypted)
Note: See TracBrowser for help on using the browser.