背景描述
使用airflow时发现airflow会讲一些secret(variable
和 connection
中的一些credential
)通过fernet加密存储到meta db.之前对fernet加密过程不是很了解,于是花了点时间了解了此加密技术的细节,于是在此总结一下.
什么是Fernet
参考fernet spec关于fernet的定义
Conceptually, fernet takes a user-provided message (an arbitrary sequence of bytes), a key (256 bits), and the current time, and produces a token, which contains the message in a form that can’t be read or altered without the key.
结合cryptography package中关于fernet的说明: https://cryptography.io/en/latest/fernet/
可以将fernet定义为一种对称加密算法, 它可以将用户信息(字节序列),32字节(256 bits)的密钥以及unix时间戳(单位秒)加密成密文.同理可以使用32字节密钥,过期时间(单位秒)和给定的unix时间戳进行解密.
代码实践
python环境中需要安装cryptography
:
pip install cryptography
此包中有两个类非常关键cryptography.fernet.Fernet
以及cryptography.fernet.MultiFernet
- fernet类常规使用方式
>>> from cryptography.fernet import Fernet
>>> key = Fernet.generate_key()
>>> key
b'BT******'
>>> f = Fernet(key)
>>> res = f.encrypt(b'hello')
>>> res
b'gAAA*****'
>>> f.decrypt(res).decode()
'hello'
生成一个密钥key
然后创建Fernet
对象,即可使用此对象对bytes
类型数据进行加密解密.
加密过程中引入时间
信息.
首先是extract_timestamp(token)
函数,此函数可以从加密后的token中提取加密时的时间戳信息. 如下对一个加密后的token使用extract_timestamp
函数返回的时间戳是一致的.
>>> time.time()
1709025319.734114
>>> res = f.encrypt(b'hello')
>>> f.extract_timestamp(res)
1709025336
>>> f.extract_timestamp(res)
1709025336
>>> f.extract_timestamp(res)
1709025336
由此引入加密相关的两个函数.
encrypt(data)
和encrypt_at_time(data, current_time
后者是前者的显示传递加密时间戳
的调用方式, encrypt
默认使用当前的unix timestamp(单位:秒)
作为加密时的时间戳.
而encrypt_at_time
可以显示的指定时间戳作为参数.
>>> t1 = int(time.time())
>>> t2 = t1 + 100
>>> res1 = f.encrypt_at_time(b'hello', t1)
>>> res2 = f.encrypt_at_time(b'hello', t2)
>>> f.extract_timestamp(res2) - f.extract_timestamp(res1)
100
decrypt(token, ttl=None)
和decrypt_at_time(token, ttl, current_time)
同样后者是前者的显示指定解密时间戳
的调用方式. ttl(int类型单位秒)表示过期时间
,当解密时间戳>加密时间戳+ttl(单位秒)时,解密过程会抛出异常,如果不传递ttl值,则过期时间不考虑在解密过程中.注意如果使用decrypt_at_time
函数,ttl一定要传递值. 如下所示,解密时会抛出InvalidToken
异常.
>>> t1 = int(time.time())
>>> t2 = t1 + 100
>>> res = f.encrypt_at_time(b'hello', t1)
>>> f.decrypt_at_time(res, 50, t2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
*******
raise InvalidToken
cryptography.fernet.InvalidToken
>>> f.decrypt_at_time(res, 150, t2)
b'hello'
- MultiFernet
正如官方所说,此类实现了fernet加密的密钥轮换.MultiFernet和 Fernet对象api完全一样,出了rotate方法.
>>> from cryptography.fernet import MultiFernet
>>> f1 = Fernet(Fernet.generate_key())
>>> f2 = Fernet(Fernet.generate_key())
f = MultiFernet([f1,f2])
>>> f.decrypt(f.encrypt(b'hello')).decode()
'hello'
>>> f1.decrypt(m)
b'hello'
>>> f2.decrypt(m)
Traceback (most recent call last):
******
cryptography.exceptions.InvalidSignature: Signature did not match digest.
官方对于MultiFernet
的解释:
MultiFernet performs all encryption options using the first key in the list provided. MultiFernet attempts to decrypt tokens with each key in turn.A cryptography.fernet.InvalidToken exception is raised if the correct key is not found in the list provided.
MultiFernet
使用列表中第一个
key做加密啊,且依次使用列表中每一个key
进行解密. 如果列表中所有key都不能进行解密则抛出异常.
如上例子所示使用MultiFernet
加密之后也可以使用第一个fernet key进行解密.
- token rotate: 密钥轮换
token rotate
是MultiFernet
独有的功能.通过apirotate(token)
实现.本质上是跟新了加密密钥对加密后的token进行重加密,提高密文被攻击的难度.如下例子:
>>> from cryptography.fernet import MultiFernet
>>> f1 = Fernet(Fernet.generate_key())
>>> f2 = Fernet(Fernet.generate_key())
>>> f3 = Fernet(Fernet.generate_key())
>>> f = MultiFernet([f1,f2])
>>> res = f.decrypt(b'hello')
>>> ff = MultiFernet([f3, f1, f2])
>>> k = ff.rotate(res)
>>> ff.decrypt(k)
b'hello'
>>> f.decrypt(k)
Traceback (most recent call last):
******
cryptography.fernet.InvalidToken
可见rotate token之后再使用之前的key就会报错,即便攻击者有之前的密钥他也没法使用密钥来解rotate之后的token.
参考文章
- https://cryptography.io/en/latest/fernet/
- https://github.com/fernet/spec/