|
1 | 1 | """ |
2 | 2 | Interface definition for encryption/decryption plugins for |
3 | | -PostgresContentsManager. |
| 3 | +PostgresContentsManager, and implementations of the interface. |
4 | 4 |
|
5 | 5 | Encryption backends should raise pgcontents.error.CorruptedFile if they |
6 | 6 | encounter an input that they cannot decrypt. |
7 | 7 | """ |
| 8 | +import sys |
| 9 | +import base64 |
| 10 | + |
| 11 | +from cryptography.fernet import Fernet |
| 12 | +from cryptography.hazmat.backends import default_backend |
| 13 | +from cryptography.hazmat.primitives import hashes |
| 14 | +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC |
| 15 | + |
8 | 16 | from .error import CorruptedFile |
9 | 17 |
|
| 18 | +if sys.version_info.major == 3: |
| 19 | + unicode = str |
| 20 | + |
10 | 21 |
|
11 | 22 | class NoEncryption(object): |
12 | 23 | """ |
@@ -127,3 +138,91 @@ def decrypt(self, s): |
127 | 138 | except CorruptedFile as e: |
128 | 139 | errors.append(e) |
129 | 140 | raise CorruptedFile(errors) |
| 141 | + |
| 142 | + |
| 143 | +def ascii_unicode_to_bytes(v): |
| 144 | + assert isinstance(v, unicode), "Expected unicode, got %s" % type(v) |
| 145 | + return v.encode('ascii') |
| 146 | + |
| 147 | + |
| 148 | +def derive_single_fernet_key(password, user_id): |
| 149 | + """ |
| 150 | + Convert a secret key and a user ID into an encryption key to use with a |
| 151 | + ``cryptography.fernet.Fernet``. |
| 152 | +
|
| 153 | + Taken from |
| 154 | + https://cryptography.io/en/latest/fernet/#using-passwords-with-fernet |
| 155 | +
|
| 156 | + Parameters |
| 157 | + ---------- |
| 158 | + password : unicode |
| 159 | + ascii-encodable key to derive |
| 160 | + user_id : unicode |
| 161 | + ascii-encodable user_id to use as salt |
| 162 | + """ |
| 163 | + password = ascii_unicode_to_bytes(password) |
| 164 | + user_id = ascii_unicode_to_bytes(user_id) |
| 165 | + |
| 166 | + kdf = PBKDF2HMAC( |
| 167 | + algorithm=hashes.SHA256(), |
| 168 | + length=32, |
| 169 | + salt=user_id, |
| 170 | + iterations=100000, |
| 171 | + backend=default_backend(), |
| 172 | + ) |
| 173 | + return base64.urlsafe_b64encode(kdf.derive(password)) |
| 174 | + |
| 175 | + |
| 176 | +def derive_fallback_fernet_keys(passwords, user_id): |
| 177 | + """ |
| 178 | + Derive a list of per-user Fernet keys from a list of master keys and a |
| 179 | + username. |
| 180 | +
|
| 181 | + If a None is encountered in ``passwords``, it is forwarded. |
| 182 | +
|
| 183 | + Parameters |
| 184 | + ---------- |
| 185 | + passwords : list[unicode] |
| 186 | + List of ascii-encodable keys to derive. |
| 187 | + user_id : unicode or None |
| 188 | + ascii-encodable user_id to use as salt |
| 189 | + """ |
| 190 | + # Normally I wouldn't advocate for these kinds of assertions, but we really |
| 191 | + # really really don't want to mess up deriving encryption keys. |
| 192 | + assert isinstance(passwords, (list, tuple)), \ |
| 193 | + "Expected list or tuple of keys, got %s." % type(passwords) |
| 194 | + |
| 195 | + def derive_single_allow_none(k): |
| 196 | + if k is None: |
| 197 | + return None |
| 198 | + return derive_single_fernet_key(k, user_id).decode('ascii') |
| 199 | + |
| 200 | + return list(map(derive_single_allow_none, passwords)) |
| 201 | + |
| 202 | + |
| 203 | +def no_password_crypto_factory(): |
| 204 | + """ |
| 205 | + Create and return a function suitable for passing as a crypto_factory to |
| 206 | + ``pgcontents.utils.sync.reencrypt_all_users`` |
| 207 | +
|
| 208 | + The factory here always returns NoEncryption(). This is useful when passed |
| 209 | + as ``old_crypto_factory`` to a database that hasn't yet been encrypted. |
| 210 | + """ |
| 211 | + def factory(user_id): |
| 212 | + return NoEncryption() |
| 213 | + return factory |
| 214 | + |
| 215 | + |
| 216 | +def single_password_crypto_factory(password): |
| 217 | + """ |
| 218 | + Create and return a function suitable for passing as a crypto_factory to |
| 219 | + ``pgcontents.utils.sync.reencrypt_all_users`` |
| 220 | +
|
| 221 | + The factory here returns a ``FernetEncryption`` that uses a key derived |
| 222 | + from ``password`` and salted with the supplied user_id. |
| 223 | + """ |
| 224 | + def factory(user_id): |
| 225 | + return FernetEncryption( |
| 226 | + Fernet(derive_single_fernet_key(password, user_id)) |
| 227 | + ) |
| 228 | + return factory |
0 commit comments