我正在开发一个Web应用程序(使用gevent,但这并不重要),它必须在日志中写入一些机密信息。显而易见的想法是使用硬编码到我的应用程序中的公钥来加密机密信息。要读取它,需要一个私钥,而2048位RSA似乎足够安全。我选择了pycrypto(也尝试了M2Crypto,但发现对于我的目的几乎没有差异)并将日志加密实现为logging.Formatter
子类。但是,我是pycrypto和cryptoraphy的新手,我不确定我对数据的加密方式的选择是否合理。我需要PKCS1_OAEP
模块吗?或者有更友好的加密方法,而无需将数据分成小块?
所以,我所做的是:
import logging
import sys
from Crypto.Cipher import PKCS1_OAEP as pkcs1
from Crypto.PublicKey import RSA
PUBLIC_KEY = """ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDe2mtK03UhymB+SrIbJJUwCPhWNMl8/gA9d7jex0ciSuFfShDaqJ4wYWG4OOl
VqKMxPrPcZ/PMSwtc021yI8TXfgewb65H/YQw4JzzGANq2+mFT8jWRDn+xUc6vcWnXIG3OPg5DvIipGQvIPNIUUP3qE7yDHnS5xdVdFrVe2bUUXmZJ9
0xJpyqlTuRtIgfIfEQC9cggrdr1G50tXdXZjS0M1WXl5P6599oH/ykjpDFrCnh5fz9WDwUc0mNJ+11Qh+yfDp3k7AhzhRaROKLVWnfkklFaFm7LsdVX
KPjp7dPRcTb84c2OnlIjU0ykL74Fy0K3eaPvM6TLe/K1XuD3933 pupkin@pupkin"""
PUBLIC_KEY = RSA.importKey(PUBLIC_KEY)
LOG_FORMAT = '[%(asctime)-15s - %(levelname)s: %(message)s]'
# May be more, but there is a limit.
# I suppose, the algorithm requires enough padding,
# and size of padding depends on key length.
MAX_MSG_LEN = 128
# Size of a block encoded with padding. For a 2048-bit key seems to be OK.
ENCODED_CHUNK_LEN = 256
def encode_msg(msg):
res = []
k = pkcs1.new(PUBLIC_KEY)
for i in xrange(0, len(msg), MAX_MSG_LEN):
v = k.encrypt(msg[i : i+MAX_MSG_LEN])
# There are nicer ways to make a readable line from data than using hex. However, using
# hex representation requires no extra code, so let it be hex.
res.append(v.encode('hex'))
assert len(v) == ENCODED_CHUNK_LEN
return ''.join(res)
def decode_msg(msg, private_key):
msg = msg.decode('hex')
res = []
k = pkcs1.new(private_key)
for i in xrange(0, len(msg), ENCODED_CHUNK_LEN):
res.append(k.decrypt(msg[i : i+ENCODED_CHUNK_LEN]))
return ''.join(res)
class CryptoFormatter(logging.Formatter):
NOT_SECRET = ('CRITICAL',)
def format(self, record):
"""
If needed, I may encode only certain types of messages.
"""
try:
msg = logging.Formatter.format(self, record)
if not record.levelname in self.NOT_SECRET:
msg = encode_msg(logging.Formatter.format(self, record))
return msg
except:
import traceback
return traceback.format_exc()
def decrypt_file(key_fname, data_fname):
"""
The function decrypts logs and never runs on server. In fact,
server does not have a private key at all. The only key owner
is server admin.
"""
res = ''
with open(key_fname, 'r') as kf:
pkey = RSA.importKey(kf.read())
with open(data_fname, 'r') as f:
for l in f:
l = l.strip()
if l:
try:
res += decode_msg(l, pkey) + 'n'
except Exception: # A line may be unencrypted
res += l + 'n'
return res
# Unfortunately dictConfig() does not support altering formatter class.
# Anyway, in demo code I am not going to use dictConfig().
logger = logging.getLogger()
handler = logging.StreamHandler(sys.stderr)
handler.setFormatter(CryptoFormatter(LOG_FORMAT))
logger.handlers = []
logger.addHandler(handler)
logging.warning("This is secret")
logging.critical("This is not secret")
更新:感谢下面接受的答案,现在我看到:
我的解决方案目前似乎非常有效(很少的日志条目,没有性能考虑,或多或少受信任的存储)。关于安全性,我现在能做的最好的事情就是不要忘记禁止运行我的守护程序的用户写入程序的
.py
和.pyc
文件。:-)但是,如果用户遭到入侵,他仍然可能会尝试将调试器附加到我的守护进程,因此我也应该为他禁用登录。非常明显的时刻,但非常重要的时刻。当然,有些解决方案更具可扩展性。一种非常常见的技术是使用缓慢但可靠的 RSA 加密 AES 密钥,并使用非常快的 AES 加密数据。在这种情况下,数据加密是对称的,但检索 AES 密钥需要中断 RSA,或者在程序运行时从内存中获取它。使用更高级别的库和二进制日志文件格式进行流加密也是一种方法,尽管加密为流的二进制日志格式应该很容易受到日志损坏的影响,即使由于停电而突然重新启动也可能是一个问题,除非我在较低级别做一些事情(至少在每个守护进程启动时日志轮换)。
我把
.encode('hex')
改成了.encode('base64').replace('n').replace('r')
.幸运的是,base64编解码器工作正常,没有行尾。它节省了一些空间。使用不受信任的存储可能需要对记录进行签名,但这似乎是另一回事。
检查字符串是否基于捕获异常进行加密是可以的,因为除非日志被恶意用户篡改,否则引发异常的是 base64 编解码器,而不是 RSA 解密。
您似乎直接使用 RSA 加密数据。这相对较慢,并且存在只能加密一小部分数据的问题。基于"解密不起作用"来区分加密数据和明文数据也不是一个非常干净的解决方案,尽管它可能会起作用。你确实使用OAEP,这很好。您可能希望使用 base64 而不是十六进制来节省空间。
但是,加密货币很容易出错。因此,应尽可能始终使用高级加密库。任何您必须自己指定填充方案的内容都不是"高级"。不过,我不确定您是否能够在不求助于相当低级库的情况下创建一个高效的、基于行的日志加密系统。
如果您没有理由只加密日志的各个部分,请考虑只加密整个内容。
如果您真的迫切需要基于行的加密,您可以执行以下操作: 从安全的随机源创建一个随机对称 AES 密钥,并为其提供一个简短但唯一的 ID。 使用 RSA 加密此密钥,并将结果写入日志文件中以标记为前缀的行,例如"KEY", 与 ID 一起。对于每个日志行,生成一个随机 IV,使用所述 IV 在 CBC 模式下使用 AES256 加密消息(您现在每行没有任何长度限制!),并将密钥 ID、IV 和加密消息写入日志,并以标签为前缀,例如"ENC"。一段时间后,销毁对称密钥并重复(生成新密钥,写入日志)。此方法的缺点是,可以从内存中恢复对称密钥的攻击者可以读取使用该密钥加密的消息。优点是您可以使用更高级别的构建块,而且速度要快得多(在我的 CPU 上,您可以使用 AES-128 每秒加密 70,000 个 1 KB 的日志行,但使用 RSA2048 只能加密大约 3,500 个最大 256 字节的块)。顺便说一下,RSA解密真的很慢(每秒约100块)。
请注意,您没有身份验证,即您不会注意到对日志的修改。因此,我假设您信任日志存储。否则,请参阅 RFC 5848。