-
Hi, One of the recommendations on the Quick Start guide is to read credentials from an encrypted file. Does anyone have an example of how this is done? |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 3 replies
-
This is an excellent question, that has a lot of potential answers. I'm working on an example that leverages the pyca Perhaps we can convince some other folks to post their solutions here as well... 🐲 |
Beta Was this translation helpful? Give feedback.
-
Hi @WDmoose! I've got a couple of simple examples for you that leverage AES 256 using CBC mode (Cipher Block Chaining). I'm also still working on a version that leverages AES is not our only option, but it's a good symmetric algorithm to use for a development implementation discussion. Our code can change depending on the mode we use, so I'll keep these two to CBC (relatively secure with HMAC).
Example 1 - pyCrypto (DO NOT USE)If you google, "How do I AES encrypt a string with Python" you will very quickly run across a variation of this example. Personally, I don't have issues with how the code is implemented, it still encrypts and decrypts properly. Unfortunately, since this example leverages the deprecated
Running bandit against this code block should generate multiple alerts. Bandit analysis of the following code[main] INFO running on Python 3.9.9
Run started:2022-10-25 06:29:11.207569
Test results:
>> Issue: [B413:blacklist] The pyCrypto library and its module Random are no longer actively maintained and have been deprecated. Consider using pyca/cryptography library.
Severity: High Confidence: High
CWE: CWE-327 (https://cwe.mitre.org/data/definitions/327.html)
Location: aes_example.py:34:0
More Info: https://bandit.readthedocs.io/en/1.7.4/blacklists/blacklist_imports.html#b413-import-pycrypto
33 # or the pyCryptodome library: https://snyk.io/advisor/python/pycryptodome instead.
34 from Crypto import Random
35 from Crypto.Cipher import AES
--------------------------------------------------
>> Issue: [B413:blacklist] The pyCrypto library and its module AES are no longer actively maintained and have been deprecated. Consider using pyca/cryptography library.
Severity: High Confidence: High
CWE: CWE-327 (https://cwe.mitre.org/data/definitions/327.html)
Location: aes_example.py:35:0
More Info: https://bandit.readthedocs.io/en/1.7.4/blacklists/blacklist_imports.html#b413-import-pycrypto
34 from Crypto import Random
35 from Crypto.Cipher import AES
36
37 class AESCipher:
--------------------------------------------------
Code scanned:
Total lines of code: 134
Total lines skipped (#nosec): 0
Run metrics:
Total issues (by severity):
Undefined: 0
Low: 0
Medium: 0
High: 2
Total issues (by confidence):
Undefined: 0
Low: 0
Medium: 0
High: 2
Files skipped (0):
pyCrypto exampler"""Simple AES encryption example.
** ******** ********
**** /**///// **//////
**//** /** /**
** //** /******* /*********
********** /**//// ////////**
/**//////** /** /** Simple encryption example #1
/** /** /******** ********
// // //////// ////////
This is a commonly referenced solution for AES encryption found online.
This works well and is easy to implement, but unfortunately leverages
a deprecated encryption library, pyCrypto. Since this library is no
longer maintained, it is recommended that you leverage a new solution
such as pyca/cryptography or pyCryptodome.
https://snyk.io/advisor/python/pycrypto
"""
import os
from argparse import ArgumentParser, RawTextHelpFormatter
from hashlib import sha256
from base64 import b64encode, b64decode
# The pyCrypto library has been deprecated.
# This code works as a demonstration but production environments should leverage
# the cryptography library: https://snyk.io/advisor/python/cryptography
# or the pyCryptodome library: https://snyk.io/advisor/python/pycryptodome instead.
from Crypto import Random
from Crypto.Cipher import AES
class AESCipher:
"""This is a commonly referenced wrapper class for handling AES."""
def __init__(self, key):
"""Create an instance of our class and set our key."""
if isinstance(key, str):
self.key = sha256(key.encode()).digest()
else:
self.key = key
def encrypt(self, raw):
"""Encrypt the provided plain text."""
raw = self._pad(raw)
init_vector = Random.new().read(AES.block_size)
cipher = AES.new(self.key, AES.MODE_CBC, init_vector)
return b64encode(init_vector + cipher.encrypt(raw)).decode('utf-8')
def decrypt(self, enc):
"""Decrypt the provided cipher text."""
enc = b64decode(enc)
init_vector = enc[:AES.block_size]
cipher = AES.new(self.key, AES.MODE_CBC, init_vector)
return self._unpad(cipher.decrypt(enc[AES.block_size:])).decode('utf-8')
def _pad(self, incoming):
"""Pad for AES CBC."""
return str(
incoming + (AES.block_size - len(incoming) % AES.block_size) \
* chr(AES.block_size - len(incoming) % AES.block_size)
).encode()
@staticmethod
def _unpad(incoming):
"""Remove AES CBC padding."""
return incoming[:-ord(incoming[len(incoming)-1:])]
class AESCrypt:
"""This class wraps the AESCipher class and provides extended functionality."""
def __init__(self, key: str = None, key_file: str = None):
"""Construct an instance of the class and determine if we're creating or reading keys."""
if key_file and not key:
if os.path.exists(key_file):
self.key = self._read_key(key_file)
else:
raise FileNotFoundError("Encryption key file not found.")
if key and not key_file:
print("Key file not specified, defaulting to 'key.enc'.")
key_file = "key.enc"
if key:
self._create_key_file(key_str=key, key_loc=key_file)
self.key = self._read_key(key_file)
@staticmethod
def _create_key_file(key_str: str, key_loc: str):
"""Create an encoded key file using the key string provided."""
keyfile = os.path.join(os.getcwd(), key_loc)
with open(keyfile, 'wb+') as data:
data.write(AESCipher(key=key_str).key)
data.close()
@staticmethod
def _read_key(key_loc: str):
"""Read the key file and return the encoded key string."""
keyfile = os.path.join(os.getcwd(), key_loc)
with open(keyfile, 'rb') as data:
key_text = data.read()
return key_text
def decrypt(self, cipher_text: str):
"""Decrypt the provided cipher text."""
return AESCipher(key=self.key).decrypt(cipher_text)
def encrypt(self, plain_text: str):
"""Encrypt the provided plain text."""
return AESCipher(key=self.key).encrypt(plain_text)
def consume_arguments():
"""Consume any provided command line arguments."""
parser = ArgumentParser(description=__doc__, formatter_class=RawTextHelpFormatter)
parser.add_argument("-k", "--key", help="Key to use for encryption operations", default=None)
parser.add_argument("-f", "--keyfile",
help="Key file to use for encryption operations",
default=None
)
mut = parser.add_mutually_exclusive_group(required=True)
mut.add_argument("-e", "--encrypted", help="Encrypted text to decrypt", default=None)
mut.add_argument("-p", "--plain", help="Plain text to encrypt", default=None)
return parser.parse_args()
def get_crypter(key: str = None, keyfile: str = None):
"""Return the correct cipher class based upon provided inputs."""
returned = None
if keyfile:
returned = AESCrypt(key_file=args.keyfile)
if key:
# Key takes precedence
returned = AESCipher(key=sha256(args.key.encode()).digest())
return returned
if __name__ == "__main__":
args = consume_arguments()
if not args.key and not args.keyfile:
# They gave us no keys, assume they want to create a keyfile
new_key = input(
"Key file was not specified, and you provided no key.\n"
"Assuming new key file generation.\n"
"What value would you like to use for your key? "
)
if not new_key:
raise SystemExit("You must provide a key.")
new_key_file = input("Where would you like to store your key? ")
if not new_key_file:
raise SystemExit("You must provide a key file location.")
try:
crypt = AESCrypt(key=new_key, key_file=new_key_file)
print(f"Your key file was successfully saved to {new_key_file}.")
except Exception as enc_fail:
raise SystemExit(str(enc_fail)) from enc_fail
# Retrieve an instance of the appropriate encryption class
crypt = get_crypter(key=args.key, keyfile=args.keyfile)
if args.encrypted:
# Receiving an empty string back means decryption failed
print(f"Decrypted text: {crypt.decrypt(str(args.encrypted))}")
elif args.plain:
print(f"Encrypted text: {crypt.encrypt(str(args.plain))}") Example 2 - CryptodomeI like Cryptodome. It provides very similar patterns to the pyCrypto library and is easy to use. One of the examples from my research, where some of this code originates, leverages both. Falling back to Cryptodome when pyCrypto was unavailable (it's pretty old code). Since the module naming conventions match, the code is fine with either import, it just has to detect which library to use. The following example does pretty much the same thing as our first. I didn't wrap the functionality in a class, just tweaked and mixed the online code examples a bit to demonstrate how they could be leveraged. Depending on the environment, this could potentially be effective for scenarios where the key is ingested at runtime. This example also has the added benefit of leveraging a maintained library that is vetted and passes static code analysis. [main] INFO running on Python 3.9.9
Run started:2022-10-25 06:33:40.325618
Test results:
No issues identified.
Code scanned:
Total lines of code: 116
Total lines skipped (#nosec): 0
Run metrics:
Total issues (by severity):
Undefined: 0
Low: 0
Medium: 0
High: 0
Total issues (by confidence):
Undefined: 0
Low: 0
Medium: 0
High: 0
Files skipped (0): Pycryptodome exampler"""Simple AES encryption example leveraging pyCryptodome.
** ******** ********
**** /**///// **//////
**//** /** /**
** //** /******* /*********
********** /**//// ////////**
/**//////** /** /** Simple encryption example #2
/** /** /******** ********
// // //////// ////////
This solution leverages pyCryptodome to handle our AES encryption operations.
For scenarios where the secret is stored vs. known, this solution is not robust
enough as it only encodes the key using base64 when writing to the key file.
For situations where the key is a known value and provided at runtime, or where
the key storage location is secured, (where the key is also encrypted) this solution
should be sufficient.
https://snyk.io/advisor/python/pycryptodome
"""
from argparse import ArgumentParser, RawTextHelpFormatter, SUPPRESS
from base64 import b64encode, b64decode
from hashlib import sha256
from os.path import exists
from Cryptodome import Random
from Cryptodome.Cipher import AES
from Cryptodome.Util import Padding
__BLOCK_SIZE__ = 16
def get_crypt_key(key_str: str = None):
"""Generate a properly padded AES CBC key."""
if not key_str:
key_str = "FungoBat!"
if not isinstance(key_str, bytes):
key_str = key_str.encode()
return sha256(key_str).digest()
def encrypt(raw, secret=None):
"""Encode the provided plaintext using the provided key."""
raw = bytes(Padding.pad(data_to_pad=raw.encode('utf-8'), block_size=__BLOCK_SIZE__))
init_vector = Random.new().read(AES.block_size)
cipher = AES.new(get_crypt_key(secret), AES.MODE_CBC, init_vector)
return b64encode(init_vector + cipher.encrypt(raw)).decode('utf-8')
def decrypt(enc, secret=None):
"""Decode the provided ciphertext using the provided key."""
enc = b64decode(enc)
init_vector = enc[:AES.block_size]
cipher = AES.new(get_crypt_key(secret), AES.MODE_CBC, init_vector)
decoded = Padding.unpad(
padded_data=cipher.decrypt(enc[AES.block_size:]),
block_size=__BLOCK_SIZE__)
return decoded.decode()
def generate_key_file():
"""Create a keyfile based off of the inputs provided."""
new_key = input(
"Generate a key file.\n"
"Base64 encoded.\n"
"What value would you like to use for your key? "
)
if not new_key:
raise SystemExit("You must provide a key.")
new_key_file = input("Where would you like to store your key? ")
if not new_key_file:
raise SystemExit("You must provide a key file location.")
try:
# !!WARNING!! This variation only base64 encodes your key
# and should not be considered a safe method for storing
# secrets within a production environment.
with open(new_key_file, "wb") as saved:
saved.write(b64encode(new_key.encode()))
print(f"Your key file was successfully saved to {new_key_file}.")
except Exception as enc_fail:
raise SystemExit(str(enc_fail)) from enc_fail
def consume_key_file(key_file: str):
"""Consume the key file and retrieve the key."""
if not exists(key_file):
raise SystemExit("Specified key file does not exist")
with open(key_file, "rb") as reading:
returned = b64decode(reading.read()).decode()
return returned
def consume_arguments():
"""Consume any provided command line arguments."""
parser = ArgumentParser(description=__doc__,
formatter_class=RawTextHelpFormatter,
argument_default=SUPPRESS
)
parser.add_argument("text", metavar="text", type=str, nargs="?", default="")
mut = parser.add_mutually_exclusive_group(required=True)
mut.add_argument("-k", "--key", help="Key to use for encryption operations", default=None)
mut.add_argument("-f", "--keyfile",
help="Key file to use for encryption operations",
default=None
)
mut.add_argument("-g", "--generate",
help="Generate a new keyfile using a key you specify",
action="store_true",
default=False
)
wot = parser.add_mutually_exclusive_group(required=False)
wot.add_argument("-d", "--decrypt",
help="Decrypt the provided string",
action="store_true",
default=False
)
wot.add_argument("-e", "--encrypt",
help="Encrypt the provided string",
action="store_true",
default=False
)
return parser.parse_args()
if __name__ == "__main__":
# Retrieve any provided command line arguments
args = consume_arguments()
if args.generate:
generate_key_file()
else:
if args.keyfile:
# Consume the key file to retrieve their key
args.key = consume_key_file(args.keyfile)
if args.decrypt:
# Decrypt the provided text using their key
decrypted = decrypt(str(args.text), secret=get_crypt_key(args.key))
print(f"Decrypted text: {decrypted}")
elif args.encrypt:
# Encrypt the provided text using their key
encrypted = encrypt(str(args.text), secret=get_crypt_key(args.key))
print(f"Encrypted text: {encrypted}") Putting it all togetherEither of our two solutions are subsequently easy to integrate with FalconPy. A quick sample of leveraging the Cryptodome example could look something like the following. Take note that we still have to ingest a secret in order to decrypt, so for demonstration purposes I'm retrieving this from the user at runtime. ### Simple CrowdStrike API usage example
### Assume we have the methods demonstrated above imported and available.
from argparse import ArgumentParser
from falconpy import Hosts
parser = ArgumentParser(description="Just an example")
parser.add_argument("key", metavar="key", help="Key to use this program", type=str)
args = parser.parse_args()
# These are not real keys, just an example of what ciphertext may look like.
hosts = Hosts(
client_id=decrypt("LM0cSbJ1eA4QL1FgmRBav1+6097HyuWEYakY/mryiZPii4/KAjR+ZvB7SCAaj+Ul",
secret=args.key # Or retrieve from the key file location / system environment
),
client_secret=decrypt("GN011qpa71AnJGgVgZVFjH96O0Hl8H3wQyFwP45GgkS2Vj3iVtWV+GOYZjN75Duu",
secret=args.key
)
)
print(hosts.query_devices_by_filter(limit=1)) Once the cryptographic components are in place, leveraging them to authenticate is super simple. Which means with just a little coding, we can come up with some variations that are not only useful, but kinda cool. Notes for the AES Authentication sample
|
Beta Was this translation helpful? Give feedback.
Hi @WDmoose!
I've got a couple of simple examples for you that leverage AES 256 using CBC mode (Cipher Block Chaining). I'm also still working on a version that leverages
pyca/cryptography
, but since it's more complex, I went ahead and posted these since I had them already.AES is not our only option, but it's a good symmetric algorithm to use for a development implementation discussion. Our code can change depending on the mode we use, so I'll keep these two to CBC (relatively secure with HMAC).