I've got a python script with logging. Now I want to encrypt log with AES using pycrypto.
import logging
import base64
from Crypto.Cipher import AES
aes = AES.new(cryptoKey)
logging.basicConfig(filename='example.log',level=logging.DEBUG) # file name, not custom file
logging.info('text')
I want to use base64.b64encode(aes.encrypt('#logging text#')) before write it to log . What is a most estate way to do it?
There's a bit more to encryption than mere forwarding of data. I would suggest writing your own log formatter and setting it as a root formatter - that way no matter where you log from in your app, even parts not controlled by your code, it will always go through a layer of encryption. So, something like:
import base64
import logging
from Crypto.Cipher import AES
from Crypto.Hash import SHA256
from Crypto import Random
class EncryptedLogFormatter(logging.Formatter):
# make sure that the `key` is a byte stream on Python 3.x
def __init__(self, key, fmt=None, datefmt=None):
self._key = SHA256.new(key).digest() # use SHA-256 for a proper-sized AES key
super(EncryptedLogFormatter, self).__init__(fmt=fmt, datefmt=datefmt)
def format(self, record):
message = record.msg # log message to encrypt, if any
if message: # no sense to encrypt empty log messages
# on Python 3.x encode first: message = message.encode("utf-8")
iv = Random.new().read(AES.block_size) # we'll be using CBC so generate an IV
cipher = AES.new(self._key, AES.MODE_CBC, iv)
# AES demands all blocks to be of `AES.block_size` so we have to pad the message
# you can use any padding you prefer, I think PKCS#7 is the best option
padding = AES.block_size - len(message) % AES.block_size
# pad the message...
message += chr(padding) * padding # Python 3.x: bytes([padding]) * padding
message_enc = iv + cipher.encrypt(message) # add iv and encrypt
# finally, replace our plain-text message with base64 encoded encrypted one
record.msg = base64.b64encode(message_enc).decode("latin-1")
# you can do more here, even print out your own string but we'll just
# pass it to the default formatter now that the message is encrypted
# so that it can respect other formatting options.
return super(EncryptedLogFormatter, self).format(record)
Then you can use it wherever you can change the logging formatter, i.e.:
import sys
import logging
# lets get the root logger
root = logging.getLogger()
root.handlers = [] # blank out the existing handlers
# create a new handler, file handler instead of stdout is perfectly fine
handler = logging.StreamHandler(stream=sys.stdout)
# now lets get to business
handler.setFormatter(EncryptedLogFormatter("Whatever key/pass you'd like to use",
"[%(levelname)s] %(message)s"))
# lets add it to the root logger so it gets called by the rest of the app automatically
root.addHandler(handler)
# And lets see what happens:
logging.warn("Sensitive stuff, hide me!")
# [WARNING] NDKeIav5G5DtbaSPB4Y/DR3+GZ9IwmXKzVTua1tTuDZ7uMwxBAKTXgIi0lam2dOQ
# YMMV, the IV is random so every block will be different every time
You can of course encrypt levels, timestamps, pretty much anything from the logging.LogRecord, and you can output whatever format you prefer. When the time comes to read your logs, you just need to do the reverse - see an example in this answer.
UPDATE: As per request, here's how to do the 'reverse' (i.e. decrypt the encrypted logs). First, lets create a few log entries for testing (continuing with the previous):
root.setLevel(logging.DEBUG) # let's make sure we support all levels
logging.warn("Lorem ipsum dolor sit amet.")
logging.info("Consectetur adipiscing elit.")
logging.debug("Sed do eiusmod tempor.")
Provided that the format remained the same ([%(levelname)s] %(message)s), this will result in a log like (of course, it will always be different due to the random IV):
[WARNING] LQMLkbx3YF7ra3e5ZLRj3p1mi2dwCOJe/GMfo2Xg8BBSZMDmZO75rrgoiy/6kqjf [INFO] D+ehnsq1kWQi61AsLOBkqglXla7jgc2myPFaCGcfCRe6drk9ZmNl+M3UkKPWkDiU [DEBUG] +rHEHkM2YHJCkIL+YwWI4FNqg6AOXfaBLRyhZpk8/fQxrXLWxcGoGxh9A2vO+7bq
To create a reader for such a log (file) we need to be aware of the format so we can differentiate encrypted from non-encrypted data. In this case, separating the parts is easy - each log entry is on a new line, the levels are not encrypted and the actual encrypted data is always separated by a whitespace from the actual log level. So, to put all that together we might construct something like:
import base64
from Crypto.Cipher import AES
from Crypto.Hash import SHA256
# make sure that the `key` is a byte stream on Python 3.x
def log_decryptor(key, stream): # assume the stream can be iterated line-by-line
key = SHA256.new(key).digest() # same derivation as in the EncryptedLogFormatter
for line in stream:
if not line.strip(): # empty line...
continue # ignore it!
level, stream = line.split(None, 1) # split on log level and log data
message_enc = base64.b64decode(stream.encode("latin-1")) # decode the stream
iv = message_enc[:AES.block_size] # grab the IV from the beginning
# decrypt the stream
message = AES.new(key, AES.MODE_CBC, iv).decrypt(message_enc[AES.block_size:])
padding = ord(message[-1]) # get the padding value; Python 3.x: message[-1]
if message[-padding:] != chr(padding) * padding: # verify the padding
# on Python 3.x: bytes([padding]) * padding
raise ValueError("Invalid padding encountered.")
# Python 3.x: decode the message: message[:-padding].decode("utf-8")
yield "{} {}".format(level, message[:-padding]) # yield the decrypted value
And then you can use it as a regular generator to decrypt your logs, e.g.:
logs = ["[WARNING] LQMLkbx3YF7ra3e5ZLRj3p1mi2dwCOJe/GMfo2Xg8BBSZMDmZO75rrgoiy/6kqjf",
"[INFO] D+ehnsq1kWQi61AsLOBkqglXla7jgc2myPFaCGcfCRe6drk9ZmNl+M3UkKPWkDiU",
"[DEBUG] +rHEHkM2YHJCkIL+YwWI4FNqg6AOXfaBLRyhZpk8/fQxrXLWxcGoGxh9A2vO+7bq"]
for line in log_decryptor("Whatever key/pass you'd like to use", logs):
print(line)
# [WARNING] Lorem ipsum dolor sit amet.
# [INFO] Consectetur adipiscing elit.
# [DEBUG] Sed do eiusmod tempor.
Or if you've set your log to stream to a file, you can directly decrypt such file as:
with open("path/to/encrypted.log", "r") as f:
for line in log_decryptor("Whatever key/pass you'd like to use", f):
print(line) # or write to a 'decrypted.log' for a more persistent solution
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With