This repository was archived by the owner on May 9, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 131
Feature/encrypted data bags #82
Open
kamilbednarz
wants to merge
32
commits into
coderanger:master
Choose a base branch
from
kamilbednarz:feature/encrypted-data-bags
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
4140cc7
Extend ChefAPI with the secret_file option
kamilbednarz 9bea357
Add encryption exceptions
kamilbednarz 3211ef3
Add a class for encrypted data bag item
267a790
Add data bag decryptor for version 2
kamilbednarz 6016f60
Add Encryptor version 1
kamilbednarz 0227ddf
Add support for choosing encryption version
kamilbednarz 4b0bf1d
Add Encryptor version 2
kamilbednarz f74a87b
Remove reading secret_file knife config from knife.rb
kamilbednarz 667a816
Update encryption get_version method to support both string and int p…
kamilbednarz 2674c36
Add EncryptedDataBagItem to chef module
kamilbednarz 25b29dc
Add test for reading data bag encryption version from config file
kamilbednarz e3d863e
Add specs for EncryptedDataBagItem class
kamilbednarz 0ab75ae
Fix issue with failing Api test
kamilbednarz 97babcf
Fix paths to files in specs for EncryptedDataBagItem
kamilbednarz c13adba
Remove trash file
kamilbednarz 81b1b30
Reworked AES implementation - use ctypes instead of m2crypto
kamilbednarz b5b6ea5
Refactored aes cipher class
kamilbednarz 476123e
Replaced encrypted_data_bag_item AES encryption method to AES256Cipher
kamilbednarz 76aeaa9
Replace direct usage of simplejson with the one from chef.utils
kamilbednarz a12c4ad
Import needed class instead of the whole module
kamilbednarz 0f6eff7
Removed stripping padding chars in decrypted strings
kamilbednarz 08e00ec
Use itertools for HMAC validation
kamilbednarz e2cf2c1
Add error messages for decryption exceptions
kamilbednarz 41ca735
AES256Cipher inherits from object
kamilbednarz f3413df
Remove manuall deleting encryptors/decryptors
kamilbednarz bc62035
Refactor HMAC validation
kamilbednarz 96f7ea5
encrypted_data_bag_item module refactored. Removed nested classes
kamilbednarz eafc2f6
Merge remote-tracking branch 'upstream/master' into feature/encrypted…
kamilbednarz b8cc53c
Py3 support for encrypted databags code
fpedrini 7b266cb
Add docs for encrypted_databag
fpedrini 105b22e
Add ability to pass encryption key as string to ChefApi
fpedrini 31209f2
Fix Python3 compatibility
kamilbednarz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
import os | ||
import sys | ||
|
||
from ctypes import * | ||
from chef.rsa import _eay, SSLError | ||
|
||
c_int_p = POINTER(c_int) | ||
|
||
# void EVP_CIPHER_CTX_init(EVP_CIPHER_CTX *a); | ||
EVP_CIPHER_CTX_init = _eay.EVP_CIPHER_CTX_init | ||
EVP_CIPHER_CTX_init.argtypes = [c_void_p] | ||
EVP_CIPHER_CTX_init.restype = None | ||
|
||
#int EVP_CipherUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, | ||
# int *outl, unsigned char *in, int inl); | ||
EVP_CipherUpdate = _eay.EVP_CipherUpdate | ||
EVP_CipherUpdate.argtypes = [c_void_p, c_char_p, c_int_p, c_char_p, c_int] | ||
EVP_CipherUpdate.restype = c_int | ||
|
||
#int EVP_CipherFinal(EVP_CIPHER_CTX *ctx, unsigned char *out, | ||
# int *outl); | ||
EVP_CipherFinal = _eay.EVP_CipherFinal | ||
EVP_CipherFinal.argtypes = [c_void_p, c_char_p, c_int_p] | ||
EVP_CipherFinal.restype = c_int | ||
|
||
#EVP_CIPHER *EVP_aes_256_cbc(void); | ||
EVP_aes_256_cbc = _eay.EVP_aes_256_cbc | ||
EVP_aes_256_cbc.argtypes = [] | ||
EVP_aes_256_cbc.restype = c_void_p | ||
|
||
#EVP_MD *EVP_sha1(void); | ||
EVP_sha1 = _eay.EVP_sha1 | ||
EVP_sha1.argtypes = [] | ||
EVP_sha1.restype = c_void_p | ||
|
||
#int EVP_CipherInit(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *type, | ||
# unsigned char *key, unsigned char *iv, int enc); | ||
EVP_CipherInit = _eay.EVP_CipherInit | ||
EVP_CipherInit.argtypes = [c_void_p, c_void_p, c_char_p, c_char_p, c_int] | ||
EVP_CipherInit.restype = c_int | ||
|
||
#int EVP_CIPHER_CTX_set_padding(EVP_CIPHER_CTX *x, int padding); | ||
EVP_CIPHER_CTX_set_padding = _eay.EVP_CIPHER_CTX_set_padding | ||
EVP_CIPHER_CTX_set_padding.argtypes = [c_void_p, c_int] | ||
EVP_CIPHER_CTX_set_padding.restype = c_int | ||
|
||
# Structures required for ctypes | ||
|
||
EVP_MAX_IV_LENGTH = 16 | ||
EVP_MAX_BLOCK_LENGTH = 32 | ||
AES_BLOCK_SIZE = 16 | ||
|
||
class EVP_CIPHER(Structure): | ||
_fields_ = [ | ||
("nid", c_int), | ||
("block_size", c_int), | ||
("key_len", c_int), | ||
("iv_len", c_int), | ||
("flags", c_ulong), | ||
("init", c_voidp), | ||
("do_cipher", c_voidp), | ||
("cleanup", c_voidp), | ||
("set_asn1_parameters", c_voidp), | ||
("get_asn1_parameters", c_voidp), | ||
("ctrl", c_voidp), | ||
("app_data", c_voidp) | ||
] | ||
|
||
class EVP_CIPHER_CTX(Structure): | ||
_fields_ = [ | ||
("cipher", POINTER(EVP_CIPHER)), | ||
("engine", c_voidp), | ||
("encrypt", c_int), | ||
("buflen", c_int), | ||
("oiv", c_ubyte * EVP_MAX_IV_LENGTH), | ||
("iv", c_ubyte * EVP_MAX_IV_LENGTH), | ||
("buf", c_ubyte * EVP_MAX_BLOCK_LENGTH), | ||
("num", c_int), | ||
("app_data", c_voidp), | ||
("key_len", c_int), | ||
("flags", c_ulong), | ||
("cipher_data", c_voidp), | ||
("final_used", c_int), | ||
("block_mask", c_int), | ||
("final", c_ubyte * EVP_MAX_BLOCK_LENGTH) ] | ||
|
||
|
||
class AES256Cipher(object): | ||
def __init__(self, key, iv, salt='12345678'): | ||
self.key_data = create_string_buffer(key) | ||
self.iv = create_string_buffer(iv) | ||
self.encryptor = self.decryptor = None | ||
self.salt = create_string_buffer(salt.encode('utf8')) | ||
|
||
self.encryptor = EVP_CIPHER_CTX() | ||
self._init_cipher(byref(self.encryptor), 1) | ||
|
||
self.decryptor = EVP_CIPHER_CTX() | ||
self._init_cipher(byref(self.decryptor), 0) | ||
|
||
def _init_cipher(self, ctypes_cipher, crypt_mode): | ||
""" crypt_mode parameter is a flag deciding whether the cipher should be | ||
used for encryption (1) or decryption (0) """ | ||
EVP_CIPHER_CTX_init(ctypes_cipher) | ||
EVP_CipherInit(ctypes_cipher, EVP_aes_256_cbc(), self.key_data, self.iv, c_int(crypt_mode)) | ||
EVP_CIPHER_CTX_set_padding(ctypes_cipher, c_int(1)) | ||
|
||
def _process_data(self, ctypes_cipher, data): | ||
# Guard against str passed in when using python3 | ||
if sys.version_info[0] > 2 and isinstance(data, str): | ||
data = data.encode('utf8') | ||
length = c_int(len(data)) | ||
buf_length = c_int(length.value + AES_BLOCK_SIZE) | ||
buf = create_string_buffer(buf_length.value) | ||
|
||
final_buf = create_string_buffer(AES_BLOCK_SIZE) | ||
final_length = c_int(0) | ||
|
||
EVP_CipherUpdate(ctypes_cipher, buf, byref(buf_length), create_string_buffer(data), length) | ||
EVP_CipherFinal(ctypes_cipher, final_buf, byref(final_length)) | ||
|
||
return string_at(buf, buf_length) + string_at(final_buf, final_length) | ||
|
||
|
||
def encrypt(self, data): | ||
return self._process_data(byref(self.encryptor), data) | ||
|
||
def decrypt(self, data): | ||
return self._process_data(byref(self.decryptor), data) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
from chef.exceptions import ChefUnsupportedEncryptionVersionError, ChefDecryptionError | ||
from chef.aes import AES256Cipher, EVP_MAX_IV_LENGTH | ||
from chef.utils import json | ||
from chef.data_bag import DataBagItem | ||
|
||
import os | ||
import sys | ||
import hmac | ||
import base64 | ||
import chef | ||
import hashlib | ||
import binascii | ||
import itertools | ||
import six | ||
from six.moves import filterfalse, zip_longest | ||
|
||
|
||
class EncryptedDataBagItem(DataBagItem): | ||
"""An Encrypted Chef data bag item object. | ||
|
||
Encrypted Databag Items behave in the same way as :class:`DatabagItem` | ||
except the keys and values are encrypted as detailed in the Chef docs: | ||
https://docs.chef.io/data_bags.html#encrypt-a-data-bag-item | ||
|
||
Refer to the :class:`DatabagItem` documentation for usage. | ||
""" | ||
SUPPORTED_ENCRYPTION_VERSIONS = (1,2) | ||
AES_MODE = 'aes_256_cbc' | ||
|
||
def __getitem__(self, key): | ||
if key == 'id': | ||
return self.raw_data[key] | ||
else: | ||
return create_decryptor(self.api.encryption_key, self.raw_data[key]).decrypt() | ||
|
||
def __setitem__(self, key, value): | ||
if key == 'id': | ||
self.raw_data[key] = value | ||
else: | ||
self.raw_data[key] = create_encryptor(self.api.encryption_key, value, self.api.encryption_version).to_dict() | ||
|
||
def create_encryptor(key, data, version): | ||
try: | ||
return { | ||
1: EncryptorVersion1(key, data), | ||
2: EncryptorVersion2(key, data) | ||
}[version] | ||
except KeyError: | ||
raise ChefUnsupportedEncryptionVersionError(version) | ||
|
||
class EncryptorVersion1(object): | ||
VERSION = 1 | ||
|
||
def __init__(self, key, data): | ||
self.plain_key = key.encode('utf8') | ||
self.key = hashlib.sha256(key.encode('utf8')).digest() | ||
self.data = data | ||
self.iv = binascii.hexlify(os.urandom(int(EVP_MAX_IV_LENGTH/2))) | ||
self.encryptor = AES256Cipher(key=self.key, iv=self.iv) | ||
self.encrypted_data = None | ||
|
||
def encrypt(self): | ||
if self.encrypted_data is None: | ||
data = json.dumps({'json_wrapper': self.data}) | ||
self.encrypted_data = self.encryptor.encrypt(data) | ||
return self.encrypted_data | ||
|
||
def to_dict(self): | ||
return { | ||
"encrypted_data": base64.standard_b64encode(self.encrypt()).decode('utf8'), | ||
"iv": base64.standard_b64encode(self.iv).decode('utf8'), | ||
"version": self.VERSION, | ||
"cipher": "aes-256-cbc" | ||
} | ||
|
||
class EncryptorVersion2(EncryptorVersion1): | ||
VERSION = 2 | ||
|
||
def __init__(self, key, data): | ||
super(EncryptorVersion2, self).__init__(key, data) | ||
self.hmac = None | ||
|
||
def encrypt(self): | ||
self.encrypted_data = super(EncryptorVersion2, self).encrypt() | ||
self.hmac = (self.hmac if self.hmac is not None else self._generate_hmac()) | ||
return self.encrypted_data | ||
|
||
def _generate_hmac(self): | ||
raw_hmac = hmac.new(self.plain_key, base64.standard_b64encode(self.encrypted_data), hashlib.sha256).digest() | ||
return raw_hmac | ||
|
||
def to_dict(self): | ||
result = super(EncryptorVersion2, self).to_dict() | ||
result['hmac'] = base64.standard_b64encode(self.hmac).decode('utf8') | ||
return result | ||
|
||
def get_decryption_version(data): | ||
if 'version' in data: | ||
if str(data['version']) in map(str, EncryptedDataBagItem.SUPPORTED_ENCRYPTION_VERSIONS): | ||
return data['version'] | ||
else: | ||
raise ChefUnsupportedEncryptionVersionError(data['version']) | ||
else: | ||
return 1 | ||
|
||
def create_decryptor(key, data): | ||
version = get_decryption_version(data) | ||
if version == 1: | ||
return DecryptorVersion1(key, data['encrypted_data'], data['iv']) | ||
elif version == 2: | ||
return DecryptorVersion2(key, data['encrypted_data'], data['iv'], data['hmac']) | ||
|
||
class DecryptorVersion1(object): | ||
def __init__(self, key, data, iv): | ||
self.key = hashlib.sha256(key.encode('utf8')).digest() | ||
self.data = base64.standard_b64decode(data) | ||
self.iv = base64.standard_b64decode(iv) | ||
self.decryptor = AES256Cipher(key=self.key, iv=self.iv) | ||
|
||
def decrypt(self): | ||
value = self.decryptor.decrypt(self.data) | ||
# After decryption we should get a string with JSON | ||
try: | ||
value = json.loads(value.decode('utf-8')) | ||
except ValueError: | ||
raise ChefDecryptionError("Error decrypting data bag value. Most likely the provided key is incorrect") | ||
return value['json_wrapper'] | ||
|
||
class DecryptorVersion2(DecryptorVersion1): | ||
def __init__(self, key, data, iv, hmac): | ||
super(DecryptorVersion2, self).__init__(key, data, iv) | ||
self.hmac = base64.standard_b64decode(hmac) | ||
self.encoded_data = data | ||
|
||
def _validate_hmac(self): | ||
encoded_data = self.encoded_data.encode('utf8') | ||
|
||
expected_hmac = hmac.new(self.key, encoded_data, hashlib.sha256).digest() | ||
valid = len(expected_hmac) ^ len(self.hmac) | ||
for expected_char, candidate_char in zip_longest(expected_hmac, self.hmac): | ||
if sys.version_info[0] > 2: | ||
valid |= expected_char ^ candidate_char | ||
else: | ||
valid |= ord(expected_char) ^ ord(candidate_char) | ||
|
||
return valid == 0 | ||
|
||
def decrypt(self): | ||
if self._validate_hmac(): | ||
return super(DecryptorVersion2, self).decrypt() | ||
else: | ||
raise ChefDecryptionError("Error decrypting data bag value. HMAC validation failed.") | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
chef_server_url 'http://chef:4000' | ||
client_key '../client.pem' | ||
node_name "test_1" | ||
|
||
data_bag_encrypt_version '2' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
1FNnCAvhritIPDerwUYKFNFZ8NaAIWyCXV43hqSVDeGk5pDt253E+RlUHlL7H/3HEFo/gnZWsk9Y5bEyOP7tUQSnT8enCbFqtvyBpiVep+4BYHss2aWBqqsm7aiPXa+BQHagmVHySleU+sFdLcNOASNMLiUB6azk8Xme1Gris8Awavrn/s5vRB7Bsl7xl84nSmu7Lg3C6Vezyye6K4ZmJOA1p0QPSMVGEJC5RkwAmA+W6G5MilBDMdxxN7mxy49WRSFLT35xFQNJOJ+Rvk53FJrhOCmiHkVNumF2MuhIpLsbrqpcdsU5UIxibjd2Dt+yz7/qytCsGSyZkVws09MgAH5icjZYV6DL8Y9CRa39KEyHl5DjHmWiRiuoFTc6oiUa0QAh08X64jz8OvcTWCJD9Fi5PdNkJblDMp9g6vvn/UPTos2s0KjzkLKdRbLrJovCSs52kkhTzfYXOYt4rmi5mQbdtcr2vsXFs+CT68Yfs56RFA2BA/+KLdaNzHFeH/Wl3h/hrciQfpAW62jnttBGr7sMV0pevXQTr2npPWq0fZHWO4gxkrL729najiDPOEeA2TeHV6+h6znZNYvfpNIRPIOMDLG7bdq2+/G7OvuE7u15qHYzWlJpvouhLA55upDK6CK1ONQw14JIK4+s9Dt2gYpV//G7MqnFMsnq3Y9ptt4= |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should check
self.encryption_key
instead - asencryption_key
is not defined