Skip to content

Commit ee7ec86

Browse files
committed
Merged in KRAUS-10 (pull request #14)
KRAUS-10: Updated property_encryptor.py to leverage the cryptography package instead of pycryptodome to provide PBEWithMD5AndDES encryption/decryption support Approved-by: Aaron Gary
2 parents 0d17a9d + 4c62bf5 commit ee7ec86

File tree

7 files changed

+245
-74
lines changed

7 files changed

+245
-74
lines changed

krausening-python/poetry.lock

Lines changed: 159 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

krausening-python/pyproject.toml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,11 @@ keywords = ["properties", "configuration-management"]
1313
python = "3.9.13"
1414
javaproperties = "^0.8.1"
1515
cryptography = "^37.0.4"
16-
typing_extensions = "^4.1.1"
17-
pycryptodome = "^3.15.0"
1816

1917
[tool.poetry.dev-dependencies]
2018
behave = "^1.2.6"
21-
nose = "^1.3.7"
2219
black = "^22.6.0"
20+
nose = "^1.3.7"
2321

2422
[build-system]
2523
requires = ["poetry-core>=1.0.0"]
Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,60 @@
11
import base64
2-
import hashlib
32
import re
43
import os
5-
from Crypto.Cipher import DES
4+
5+
from cryptography.hazmat.primitives.hashes import Hash, MD5
6+
from cryptography.hazmat.primitives.ciphers import Cipher
7+
from cryptography.hazmat.primitives.ciphers.algorithms import TripleDES
8+
from cryptography.hazmat.primitives.ciphers.modes import CBC
69

710

811
class PropertyEncryptor:
912
"""
10-
Class to mimic the standard Jasypt string encryption/decryption.
13+
Provides property value encryption/decryption support via PBEWithMD5AndDES, which is the
14+
default encryption algorithm used by Jasypt's CLI and StandardPBEByteEncryptor. This aligns with
15+
the same approach used for property encryption within the Krausening Java package.
1116
12-
Modified from: https://github.com/binsgit/PBEWithMD5AndDES/blob/master/python/PBEWithMD5AndDES_2.py
17+
As per https://fermenter.atlassian.net/browse/KRAUS-11, later iterations should seek to introduce
18+
a more secure encryption algorithm approach.
1319
1420
See https://bitbucket.org/cpointe/krausening/src/dev/ for details on encrypting values with Jasypt.
1521
"""
1622

17-
def encrypt(self, msg: str, password: bytes) -> bytes:
23+
def encrypt(self, value_to_encrypt: str, password: bytes) -> bytes:
1824
salt = os.urandom(8)
19-
pad_num = 8 - (len(msg) % 8)
20-
for i in range(pad_num):
21-
msg += chr(pad_num)
22-
(dk, iv) = self.get_derived_key(password, salt, 1000)
23-
crypter = DES.new(dk, DES.MODE_CBC, iv)
24-
enc_text = crypter.encrypt(msg)
25-
return base64.b64encode(salt + enc_text)
26-
27-
def decrypt(self, msg: str, password: bytes) -> str:
28-
msg_bytes = base64.b64decode(msg)
25+
(key, init_vector) = self._pbkdf1_md5(password, salt, 1000)
26+
cipher = Cipher(TripleDES(key), CBC(init_vector))
27+
encryptor = cipher.encryptor()
28+
return encryptor.update(value_to_encrypt) + encryptor.finalize()
29+
30+
def decrypt(self, encrypted_msg: str, password: bytes) -> str:
31+
msg_bytes = base64.b64decode(encrypted_msg)
2932
salt = msg_bytes[:8]
3033
enc_text = msg_bytes[8:]
31-
(dk, iv) = self.get_derived_key(password, salt, 1000)
32-
crypter = DES.new(dk, DES.MODE_CBC, iv)
33-
text = crypter.decrypt(enc_text)
34+
(key, init_vector) = self._pbkdf1_md5(password, salt, 1000)
35+
36+
cipher = Cipher(TripleDES(key), CBC(init_vector))
37+
decryptor = cipher.decryptor()
38+
text = decryptor.update(enc_text) + decryptor.finalize()
3439
# remove the padding at the end, if any
3540
return re.sub(r"[\x01-\x08]", "", text.decode("utf-8"))
3641

37-
def get_derived_key(self, password: bytes, salt: bytes, count: int) -> tuple:
38-
key = password + salt
39-
for i in range(count):
40-
m = hashlib.md5(key)
41-
key = m.digest()
42-
return (key[:8], key[8:])
42+
def _pbkdf1_md5(self, password, salt, iterations):
43+
"""
44+
Provides a Password Based Key Derivation Function (PBKDF1) as defined in RFC 2829
45+
(https://www.rfc-editor.org/rfc/rfc2898#section-5.1) that applies a MD5 hash function
46+
to derive a key from the given password.
47+
"""
48+
digest = Hash(MD5())
49+
digest.update(password)
50+
digest.update(salt)
51+
52+
key = None
53+
for i in range(iterations):
54+
key = digest.finalize()
55+
digest = Hash(MD5())
56+
digest.update(key)
57+
58+
digest.finalize()
59+
60+
return key[:8], key[8:16]

krausening-python/src/krausening/properties/property_manager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ def getProperty(self, key: str, defaultValue: Optional[T] = None):
8383
class EncryptableProperties(Properties):
8484
"""
8585
This class represents a properties file that can decrypt property values that have been encrypted
86-
with Jasypt, and is based on Jaspyt's EncryptableProperties.
86+
via PBEWithMD5AndDES, which is the default encryption algorithm used by Jasypt's CLI and StandardPBEByteEncryptor.
87+
This class is largely based on Jaspyt's EncryptableProperties.
8788
8889
See https://bitbucket.org/cpointe/krausening/src/dev/ for details on encrypting values with Jasypt.
8990
"""
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import os
22

33

4-
def after_feature(context, feature):
5-
if "properties" == feature.name:
6-
os.environ["KRAUSENING_BASE"] = None
7-
os.environ["KRAUSENING_EXTENSION"] = None
4+
def before_scenario(context, scenario):
5+
"""
6+
Clear all Krausening environment variables prior to each scenario.
7+
"""
8+
os.environ["KRAUSENING_BASE"] = ""
9+
os.environ["KRAUSENING_EXTENSIONS"] = ""
10+
os.environ["KRAUSENING_PASSWORD"] = ""

krausening-python/tests/features/properties.feature

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,18 @@
22
Feature: Property Management
33

44
Scenario: Property can be loaded from file
5-
Given a base file with property "foo" and value "bar"
6-
When the property file is loaded
7-
Then the value of "foo" is set to "bar"
5+
Given a base properties file with property "foo"
6+
When the properties file is loaded
7+
Then the retrieved value of "foo" is "bar"
88

99
Scenario: Properties can be overridden
10-
Given a base file with property "foo" and value "bar"
11-
When a new property file is read with property "foo" and value "bar2"
12-
Then the property value is set to "bar2"
10+
Given a base properties file with property "foo"
11+
And an extensions properties file with property "foo"
12+
When the properties file is loaded
13+
Then the retrieved value of "foo" is "bar2"
1314

1415
Scenario: Encrypted properties can be decrypted
15-
Given a base file with property "foo" and an encrypted value for "bar"
16-
When the property file is loaded
17-
Then the value of "foo" is set to "bar"
16+
Given a base properties file with property "foo"
17+
And the properties file contains encrypted value for the "foo" property
18+
When the properties file is loaded
19+
Then the retrieved value of "foo" is "bar"

krausening-python/tests/features/steps/core_properties_steps.py

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,46 @@
22

33
from behave import *
44
from krausening.properties import PropertyManager
5+
from nose.tools import assert_equal
56

6-
use_step_matcher("re")
77

8-
propertyManager = PropertyManager.get_instance()
9-
properties = None
10-
11-
12-
@given('a base file with property "foo" and value "bar"')
8+
@given('a base properties file with property "foo"')
139
def step_impl(context):
1410
os.environ["KRAUSENING_BASE"] = "tests/resources/config/"
1511
context.file = "test.properties"
1612

1713

18-
@given('a base file with property "foo" and an encrypted value for "bar"')
14+
@given('an extensions properties file with property "foo"')
1915
def step_impl(context):
20-
os.environ["KRAUSENING_BASE"] = "tests/resources/config/"
21-
os.environ["KRAUSENING_PASSWORD"] = "P455w0rd"
22-
context.file = "test-encrypted.properties"
16+
os.environ["KRAUSENING_EXTENSIONS"] = "tests/resources/config_extension/"
2317

2418

25-
@when("the property file is loaded")
19+
@given('the properties file contains encrypted value for the "foo" property')
2620
def step_impl(context):
27-
global properties
28-
properties = propertyManager.get_properties(context.file)
21+
os.environ["KRAUSENING_PASSWORD"] = "P455w0rd"
22+
context.file = "test-encrypted.properties"
2923

3024

31-
@then('the value of "foo" is set to "bar"')
25+
@when("the properties file is loaded")
3226
def step_impl(context):
33-
assert properties["foo"] == "bar"
27+
context.properties = PropertyManager.get_instance().get_properties(context.file)
3428

3529

36-
@when('a new property file is read with property "foo" and value "bar2"')
30+
@then('the retrieved value of "foo" is "bar"')
3731
def step_impl(context):
38-
os.environ["KRAUSENING_EXTENSIONS"] = "tests/resources/config_extension/"
39-
global properties
40-
properties = propertyManager.get_properties("test.properties")
32+
foo_property_value = context.properties["foo"]
33+
assert_equal(
34+
foo_property_value,
35+
"bar",
36+
f"Retrieved 'foo' property, which is {foo_property_value}, didn't match expected value",
37+
)
4138

4239

43-
@then('the property value is set to "bar2"')
40+
@then('the retrieved value of "foo" is "bar2"')
4441
def step_impl(context):
45-
assert properties["foo"] == "bar2"
42+
foo_property_value = context.properties["foo"]
43+
assert_equal(
44+
foo_property_value,
45+
"bar2",
46+
f"Retrieved 'foo' property, which is {foo_property_value}, didn't match expected value",
47+
)

0 commit comments

Comments
 (0)