##// END OF EJS Templates
encryption: unified and rewrote encryption modules to be consistent no matter what algo is used....
super-admin -
r4995:676da06b default
parent child Browse files
Show More
@@ -0,0 +1,48 b''
1 from rhodecode.lib.str_utils import safe_bytes
2 from rhodecode.lib.encrypt import encrypt_data, validate_and_decrypt_data
3 from rhodecode.lib.encrypt2 import Encryptor
4
5 ALLOWED_ALGOS = ['aes', 'fernet']
6
7
8 def get_default_algo():
9 import rhodecode
10 return rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
11
12
13 def encrypt_value(value: bytes, enc_key: bytes, algo: str = ''):
14 if not algo:
15 # not explicit algo, just use what's set by config
16 algo = get_default_algo()
17 if algo not in ALLOWED_ALGOS:
18 ValueError(f'Bad encryption algorithm, should be {ALLOWED_ALGOS}, got: {algo}')
19
20 enc_key = safe_bytes(enc_key)
21 value = safe_bytes(value)
22
23 if algo == 'aes':
24 return encrypt_data(value, enc_key=enc_key)
25 if algo == 'fernet':
26 return Encryptor(enc_key).encrypt(value)
27
28 return value
29
30
31 def decrypt_value(value: bytes, enc_key: bytes, algo: str = '', strict_mode: bool = False):
32
33 if not algo:
34 # not explicit algo, just use what's set by config
35 algo = get_default_algo()
36 if algo not in ALLOWED_ALGOS:
37 ValueError(f'Bad encryption algorithm, should be {ALLOWED_ALGOS}, got: {algo}')
38
39 enc_key = safe_bytes(enc_key)
40 value = safe_bytes(value)
41 safe = not strict_mode
42
43 if algo == 'aes':
44 return validate_and_decrypt_data(value, enc_key, safe=safe)
45 if algo == 'fernet':
46 return Encryptor(enc_key).decrypt(value, safe=safe)
47
48 return value
@@ -24,16 +24,14 b' Generic encryption library for RhodeCode'
24 24 """
25 25
26 26 import base64
27 import logging
27 28
28 29 from Crypto.Cipher import AES
29 30 from Crypto import Random
30 31 from Crypto.Hash import HMAC, SHA256
31 32
32 from rhodecode.lib.str_utils import safe_bytes
33
34
35 class SignatureVerificationError(Exception):
36 pass
33 from rhodecode.lib.str_utils import safe_bytes, safe_str
34 from rhodecode.lib.exceptions import signature_verification_error
37 35
38 36
39 37 class InvalidDecryptedValue(str):
@@ -47,9 +45,13 b' class InvalidDecryptedValue(str):'
47 45 content = '<{}({}...)>'.format(cls.__name__, content[:16])
48 46 return str.__new__(cls, content)
49 47
48 KEY_FORMAT = b'enc$aes_hmac${1}'
49
50 50
51 51 class AESCipher(object):
52 def __init__(self, key, hmac=False, strict_verification=True):
52
53 def __init__(self, key: bytes, hmac=False, strict_verification=True):
54
53 55 if not key:
54 56 raise ValueError('passed key variable is empty')
55 57 self.strict_verification = strict_verification
@@ -67,13 +69,13 b' class AESCipher(object):'
67 69 self.hmac_key, data_without_sig, digestmod=SHA256).digest()
68 70 return org_hmac_signature == recomputed_hmac
69 71
70 def encrypt(self, raw):
72 def encrypt(self, raw: bytes):
71 73 raw = self._pad(raw)
72 74 iv = Random.new().read(AES.block_size)
73 75 cipher = AES.new(self.key, AES.MODE_CBC, iv)
74 76 enc_value = cipher.encrypt(raw)
75 77
76 hmac_signature = ''
78 hmac_signature = b''
77 79 if self.hmac:
78 80 # compute hmac+sha256 on iv + enc text, we use
79 81 # encrypt then mac method to create the signature
@@ -82,57 +84,69 b' class AESCipher(object):'
82 84
83 85 return base64.b64encode(iv + enc_value + hmac_signature)
84 86
85 def decrypt(self, enc):
87 def decrypt(self, enc, safe=True) -> bytes | InvalidDecryptedValue:
86 88 enc_org = enc
89 try:
87 90 enc = base64.b64decode(enc)
91 except Exception:
92 logging.exception('Failed Base64 decode')
93 raise signature_verification_error('Failed Base64 decode')
88 94
89 95 if self.hmac and len(enc) > self.hmac_size:
90 96 if self.verify_hmac_signature(enc):
91 97 # cut off the HMAC verification digest
92 98 enc = enc[:-self.hmac_size]
93 99 else:
94 if self.strict_verification:
95 raise SignatureVerificationError(
96 "Encryption signature verification failed. "
97 "Please check your secret key, and/or encrypted value. "
98 "Secret key is stored as "
99 "`rhodecode.encrypted_values.secret` or "
100 "`beaker.session.secret` inside .ini file")
101 100
102 return InvalidDecryptedValue(enc_org)
101 decrypt_fail = InvalidDecryptedValue(safe_str(enc_org))
102 if safe:
103 return decrypt_fail
104 raise signature_verification_error(decrypt_fail)
103 105
104 106 iv = enc[:AES.block_size]
105 107 cipher = AES.new(self.key, AES.MODE_CBC, iv)
106 108 return self._unpad(cipher.decrypt(enc[AES.block_size:]))
107 109
108 110 def _pad(self, s):
109 return (s + (self.block_size - len(s) % self.block_size)
110 * chr(self.block_size - len(s) % self.block_size))
111 block_pad = (self.block_size - len(s) % self.block_size)
112 return s + block_pad * safe_bytes(chr(block_pad))
111 113
112 114 @staticmethod
113 115 def _unpad(s):
114 116 return s[:-ord(s[len(s)-1:])]
115 117
116 118
117 def validate_and_get_enc_data(enc_data, enc_key, enc_strict_mode):
119 def validate_and_decrypt_data(enc_data, enc_key, enc_strict_mode=False, safe=True):
120 enc_data = safe_str(enc_data)
121
118 122 parts = enc_data.split('$', 3)
119 if not len(parts) == 3:
120 # probably not encrypted values
121 return enc_data
122 else:
123 if len(parts) != 3:
124 raise ValueError(f'Encrypted Data has invalid format, expected {KEY_FORMAT}, got {parts}')
125
126 enc_type = parts[1]
127 enc_data_part = parts[2]
128
123 129 if parts[0] != 'enc':
124 130 # parts ok but without our header ?
125 131 return enc_data
126 132
127 133 # at that stage we know it's our encryption
128 if parts[1] == 'aes':
129 decrypted_data = AESCipher(enc_key).decrypt(parts[2])
130 elif parts[1] == 'aes_hmac':
134 if enc_type == 'aes':
135 decrypted_data = AESCipher(enc_key).decrypt(enc_data_part, safe=safe)
136 elif enc_type == 'aes_hmac':
131 137 decrypted_data = AESCipher(
132 138 enc_key, hmac=True,
133 strict_verification=enc_strict_mode).decrypt(parts[2])
139 strict_verification=enc_strict_mode).decrypt(enc_data_part, safe=safe)
140
134 141 else:
135 142 raise ValueError(
136 'Encryption type part is wrong, must be `aes` '
137 'or `aes_hmac`, got `%s` instead' % (parts[1]))
143 f'Encryption type part is wrong, must be `aes` '
144 f'or `aes_hmac`, got `{enc_type}` instead')
145
138 146 return decrypted_data
147
148
149 def encrypt_data(data, enc_key: bytes):
150 enc_key = safe_bytes(enc_key)
151 enc_value = AESCipher(enc_key, hmac=True).encrypt(safe_bytes(data))
152 return KEY_FORMAT.replace(b'{1}', enc_value)
@@ -5,12 +5,27 b' from cryptography.hazmat.backends import'
5 5 from cryptography.hazmat.primitives import hashes
6 6 from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
7 7
8 from rhodecode.lib.str_utils import safe_str
9 from rhodecode.lib.exceptions import signature_verification_error
10
11
12 class InvalidDecryptedValue(str):
13
14 def __new__(cls, content):
15 """
16 This will generate something like this::
17 <InvalidDecryptedValue(QkWusFgLJXR6m42v...)>
18 And represent a safe indicator that encryption key is broken
19 """
20 content = '<{}({}...)>'.format(cls.__name__, content[:16])
21 return str.__new__(cls, content)
22
8 23
9 24 class Encryptor(object):
10 key_format = 'enc2$salt:{}$data:{}'
25 key_format = b'enc2$salt:{1}$data:{2}'
11 26 pref_len = 5 # salt:, data:
12 27
13 def __init__(self, enc_key):
28 def __init__(self, enc_key: bytes):
14 29 self.enc_key = enc_key
15 30
16 31 def b64_encode(self, data):
@@ -35,9 +50,9 b' class Encryptor(object):'
35 50 return Fernet(key)
36 51
37 52 def _get_parts(self, enc_data):
38 parts = enc_data.split('$', 3)
53 parts = enc_data.split(b'$', 3)
39 54 if len(parts) != 3:
40 raise ValueError('Encrypted Data has invalid format, expected {}'.format(self.key_format))
55 raise ValueError(f'Encrypted Data has invalid format, expected {self.key_format}, got {parts}')
41 56 prefix, salt, enc_data = parts
42 57
43 58 try:
@@ -49,13 +64,13 b' class Encryptor(object):'
49 64 enc_data = enc_data[self.pref_len:]
50 65 return prefix, salt, enc_data
51 66
52 def encrypt(self, data):
67 def encrypt(self, data) -> bytes:
53 68 salt = os.urandom(64)
54 69 encryptor = self.get_encryptor(salt)
55 70 enc_data = encryptor.encrypt(data)
56 return self.key_format.format(self.b64_encode(salt), enc_data)
71 return self.key_format.replace(b'{1}', self.b64_encode(salt)).replace(b'{2}', enc_data)
57 72
58 def decrypt(self, data, safe=True):
73 def decrypt(self, data, safe=True) -> bytes | InvalidDecryptedValue:
59 74 parts = self._get_parts(data)
60 75 salt = parts[1]
61 76 enc_data = parts[2]
@@ -63,7 +78,7 b' class Encryptor(object):'
63 78 try:
64 79 return encryptor.decrypt(enc_data)
65 80 except (InvalidToken,):
81 decrypt_fail = InvalidDecryptedValue(safe_str(data))
66 82 if safe:
67 return ''
68 else:
69 raise
83 return decrypt_fail
84 raise signature_verification_error(decrypt_fail)
@@ -63,10 +63,10 b' from rhodecode.lib.utils2 import ('
63 63 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time)
64 64 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
65 65 JsonRaw
66 from rhodecode.lib import ext_json
67 from rhodecode.lib import enc_utils
66 68 from rhodecode.lib.ext_json import json
67 69 from rhodecode.lib.caching_query import FromCache
68 from rhodecode.lib.encrypt import AESCipher, validate_and_get_enc_data
69 from rhodecode.lib.encrypt2 import Encryptor
70 70 from rhodecode.lib.exceptions import (
71 71 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
72 72 from rhodecode.model.meta import Base, Session
@@ -81,7 +81,7 b' log = logging.getLogger(__name__)'
81 81 # this is propagated from .ini file rhodecode.encrypted_values.secret or
82 82 # beaker.session.secret if first is not set.
83 83 # and initialized at environment.py
84 ENCRYPTION_KEY = None
84 ENCRYPTION_KEY = ''
85 85
86 86 # used to sort permissions by types, '#' used here is not allowed to be in
87 87 # usernames, and it's very early in sorted string.printable table.
@@ -184,12 +184,7 b' class EncryptedTextValue(TypeDecorator):'
184 184 'ie. not starting with enc$ or enc2$')
185 185
186 186 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
187 if algo == 'aes':
188 return 'enc$aes_hmac$%s' % AESCipher(ENCRYPTION_KEY, hmac=True).encrypt(value)
189 elif algo == 'fernet':
190 return Encryptor(ENCRYPTION_KEY).encrypt(value)
191 else:
192 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
187 return enc_utils.encrypt_value(value, enc_key=ENCRYPTION_KEY, algo=algo)
193 188
194 189 def process_result_value(self, value, dialect):
195 190 """
@@ -200,15 +195,9 b' class EncryptedTextValue(TypeDecorator):'
200 195 if not value:
201 196 return value
202 197
203 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
204 198 enc_strict_mode = str2bool(rhodecode.CONFIG.get('rhodecode.encrypted_values.strict') or True)
205 if algo == 'aes':
206 decrypted_data = validate_and_get_enc_data(value, ENCRYPTION_KEY, enc_strict_mode)
207 elif algo == 'fernet':
208 return Encryptor(ENCRYPTION_KEY).decrypt(value)
209 else:
210 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
211 return decrypted_data
199
200 return enc_utils.decrypt_value(value, enc_key=ENCRYPTION_KEY, strict_mode=enc_strict_mode)
212 201
213 202
214 203 class BaseModel(object):
@@ -308,13 +297,13 b' class BaseModel(object):'
308 297 attr_name, value, exist_in_session)
309 298
310 299 def __repr__(self):
311 if hasattr(self, '__unicode__'):
300 if hasattr(self, '__str__'):
312 301 # python repr needs to return str
313 302 try:
314 return safe_str(self.__unicode__())
303 return self.__str__()
315 304 except UnicodeDecodeError:
316 305 pass
317 return '<DB:%s>' % (self.__class__.__name__)
306 return f'<DB:{self.__class__.__name__}>'
318 307
319 308
320 309 class RhodeCodeSetting(Base, BaseModel):
@@ -358,7 +347,7 b' class RhodeCodeSetting(Base, BaseModel):'
358 347 # decode the encrypted value
359 348 if 'encrypted' in self.app_settings_type:
360 349 cipher = EncryptedTextValue()
361 v = safe_unicode(cipher.process_result_value(v, None))
350 v = safe_str(cipher.process_result_value(v, None))
362 351
363 352 converter = self.SETTINGS_TYPES.get(_type) or \
364 353 self.SETTINGS_TYPES['unicode']
@@ -375,7 +364,7 b' class RhodeCodeSetting(Base, BaseModel):'
375 364 # encode the encrypted value
376 365 if 'encrypted' in self.app_settings_type:
377 366 cipher = EncryptedTextValue()
378 val = safe_unicode(cipher.process_bind_param(val, None))
367 val = safe_str(cipher.process_bind_param(val, None))
379 368 self._app_settings_value = val
380 369
381 370 @hybrid_property
@@ -395,8 +384,8 b' class RhodeCodeSetting(Base, BaseModel):'
395 384 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
396 385 .all()
397 386
398 def __unicode__(self):
399 return u"<%s('%s:%s[%s]')>" % (
387 def __str__(self):
388 return "<%s('%s:%s[%s]')>" % (
400 389 self.__class__.__name__,
401 390 self.app_settings_name, self.app_settings_value,
402 391 self.app_settings_type
General Comments 0
You need to be logged in to leave comments. Login now