上一章我们讲了mysql压缩原理(含lz4压缩格式)并解析, 细心的同学应该发现旁边就是加密的相关代码. 那本章就来讲讲mysql加密和解析.
理论上, 看完本篇文章, 就能通过 keyring文件解析ibd文件了. 仅考虑社区版的keyring插件
低版本是使用plugin, 高版本使用Components.(花里胡哨的). 本次使用Plugin的方式安装keyring. 参考如下:
# 配置文件添加如下信息:
early-plugin-load=keyring_file.so
keyring_file_data=/usr/local/mysql/keyring/keyring2
# 重启mysql实例
systemctl restart mysqld_3314
注: 这个keyring2(名字随便取)文件别整丢了, 不然数据就gg了. 我测试的时候,换了个新名字(生成新的master_key)之后, 旧的表就无法读取了. 会报错:2024-09-27T02:23:25.097676Z 9 ERROR InnoDB Encryption information in datafile: ./db1/t20240926.ibd can't be decrypted, please confirm that keyring is loaded. 做校验的时候,没注意, 坑了我一手......
本次演示解析如下表
create table db1.t20240926(id int primary key, name varchar(200)) encryption='y';
insert into db1.t20240926 values(1,'ddcw');
insert into db1.t20240926 values(2,'ddcw');
-- 给已有的表设置加密
alter table db1.t1 encryption='y';
general tablespace也是支持加密的. 虽然使用场景少
ALTER [UNDO] TABLESPACE tablespace_name
NDB only:
{ADD | DROP} DATAFILE 'file_name'
[INITIAL_SIZE [=] size]
[WAIT]
InnoDB and NDB:
[RENAME TO tablespace_name]
InnoDB only:
[AUTOEXTEND_SIZE [=] 'value']
[SET {ACTIVE | INACTIVE}]
[ENCRYPTION [=] {'Y' | 'N'}]
InnoDB and NDB:
[ENGINE [=] engine_name]
Reserved for future use:
[ENGINE_ATTRIBUTE [=] 'string']
有时候一个key用久了, 就觉得不安全, 想换一个也是可以的. mysql支持轮转key
ALTER INSTANCE ROTATE MASTER KEY;
mysql的加密实际上是分为两部分的, keyring file里面存储了一系列master_key, 然后使用master_key加密tablespace_key(加密之后的tablespace_key放在fsp), tablespace_key才是用来加密数据page的
这种设计应该是为了支持轮转key
大概如下图:
虽然图看着丑, 但意思就是这样的.
或者借用Mayank Prasad的图如下:
现在来具体瞧瞧, 先看瞧瞧keyring file格式, 该格式是二进制的. 无法直接查看.
看了下源码, 复杂到离谱. 但好歹有大佬解析过的. 我们就直接看格式吧.
其实也能猜到大概, 但做亦或那里就难发现了..
keyring_file由一系列master_key组成. 格式如下:
对象 | 大小(字节) | 描述 |
---|---|---|
header | 24 | 描述信息, 比如版本之类的 |
total_length | 8 | 该master_key占的总大小 |
key_id_length | 8 | key_id长度 |
key_type_length | 8 | 加密算法类型, 通常为AES |
user_id_length | 8 | user_id,没发现有啥用... |
key_length | 8 | key的长度 |
key_id | fsp保存的key_id和这个呼应上了,就取这个的key | |
key_type | 加密算法的类型 | |
user_id | 没dio用 | |
key | 32(通常是) | 给tablespace_key加密的key, 得先和obfuscate_str做亦或 |
.... | n | 重复master_key |
EOF | 3 | EOF |
.... | ... | .. |
虽然看起来有丢丢复杂, 但实际上就一丢丢信息... 我们可以使用如下python代码来解析
import struct
from Crypto.Cipher import AES
keyring_filename = '/usr/local/mysql/keyring/keyring2'
filename = '/data/mysql_3314/mysqldata/db1/t20240926.ibd'
def read_keyring(data):
offset = 24
kd = {}
xor_str = '*305=Ljt0*!@$Hnm(*-9-w;:'.encode()
while True:
if data[offset:offset+3] == b'EOF':
break
total_length, key_id_length, key_type_length, user_id_length, key_length = struct.unpack_from('<QQQQQ', data, offset) # 注意是小端字节序...
offset += 40
key_id = data[offset:offset+key_id_length].decode()
offset += key_id_length
key_type = data[offset:offset+key_type_length].decode()
offset += key_type_length
user_id = data[offset:offset+user_id_length]
offset += user_id_length
key = data[offset:offset+key_length]
keyt = bytes([key[i] ^ xor_str[i%24] for i in range(len(key))])
offset += key_length
kd[key_id] = {'key':keyt,'key_type':key_type}
if offset % 8 != 0:
offset += 8 - (offset % 8)
return kd
with open(keyring_filename,'rb') as f:
keyring_data = f.read()
kd = read_keyring(keyring_data)
print(kd)
我这里只有一个key, 如果做过rotate的, 或者给其它实例使用过的, 那么就会存在多个. 比如:
keyring格式整体比较简单, 就是得和一个常量做亦或比较坑人.
在解析得到master_key之后, 我们就可以解析fsp去获取tablespace_key了. 先看看fsp中记录的encryption_metadata格式吧. 总大小是115字节. 在我们之前解析sdi的时候有见到过(当时年轻,不知其含义)
#MAGIC_SIZE=3 KEY_LEN=32 SERVER_UUID_LEN=36
#(MAGIC_SIZE + sizeof(uint32) + (KEY_LEN * 2) + SERVER_UUID_LEN + sizeof(uint32))
INFO_SIZE = 3+4+32*2+36+4
INFO_MAX_SIZE = INFO_SIZE + 4
#SDI_OFFSET = 38+112+40*256 + INFO_MAX_SIZE
SDI_VERSION = 1
/* Encryption info to be filled in following format
--------------------------------------------------------------------------
| Magic bytes | master key id | server uuid | tablespace key|iv | checksum |
--------------------------------------------------------------------------
*/
具体内容如下:
对象 | 大小(字节) | 描述 |
---|---|---|
magic | 3 | 版本关键字 |
master_key_id | 4 | master_key_id. |
server_uuid | 36 | server_uuid |
key_info | 32*2 | tablespace_key+iv |
checksum | 4 | 使用crc32c校验的 |
null | 4 | 空了4字节,不造干嘛的 |
5.7.11引入的加密功能, 具体的magci对应如下
KEY_MAGIC_V1[] = "lCA"; // 5.7.11
KEY_MAGIC_V2[] = "lCB"; // 5.7.12+
KEY_MAGIC_V3[] = "lCC"; // 8.0.5+
keyring中的master_id实际上是encryption_metadata中的uuid+master_id.
master_key = kd['INNODBKey'+'-'+server_uuid+'-'+str(master_id)]['key']
熟悉aes的cbc模式的小伙伴可能会疑惑,iv不是要求16字节么, 这里是使用的32字节啊.... (实际上是取的32字节中的前16字节. 小坑).
我们再使用代码解析下吧. 这里的crc32是使用的crc32算法, 可参考之前坏块校验
## 解析keyring的代码我就省略了, 上面有的.
kd = read_keyring(keyring_data)
f = open(filename,'rb')
fsp = f.read(16384)
#struct.unpack('>BBHHH',fsp[26:34])
data = fsp[10390:10390+115]
print(data[:3]) # lCC
master_id = struct.unpack('>L',data[3:7])[0]
server_uuid = data[7:7+36].decode()
master_key = kd['INNODBKey'+'-'+server_uuid+'-'+str(master_id)]['key']
ase = AES.new(master_key,AES.MODE_ECB)
print('MASTER_KEY:',master_key)
key_info = ase.decrypt(data[43:43+32*2])
print('KEY:',key_info[:32])
print('IV:',key_info[32:48])
那怎么校验呢? 先别急.
官方为了支持rotate, 使用了keyring,里面保存多个key,那么就得确保里面的key能够解析fsp的tablespace_key. 所以整了个校验位.... 我们来校验下.
# crc32c的导入参考: https://github.com/ddcw/ddcw/tree/master/python/check_innodb_file 我这里就省略了.
calculate_crc32c(key_info) # 小坑,是校验的整个key_info(不是key+iv). mysql到处给我埋坑....
struct.unpack('>L',fsp[10390:10390+115][-8:-4])[0]
看来我们成功解析到了tablespace_key.
既然tablespace_key已经获取到了, 那就该解析数据了. 加密的格式和压缩页的格式是一样的. 那就只需要把解压换成解密就行了(就换一个汉字). 先看看长什么样子.
f.seek(4*16384,0)
data = f.read(16384)
struct.unpack('>BBHHH',data[26:34])
data[:200]
看起来是个index page. 而且数据全是加密的. 那就开始解密吧.
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
backend = default_backend()
cipher = Cipher(algorithms.AES(key_info[:32]), modes.CBC(key_info[32:48]),backend=backend)
decryptor = cipher.decryptor()
dedata = data[:38] + decryptor.update(data[38:])
dedata[:200]
看到眼熟的infimum了. 那就说明我们基本上解析对了. 但我们再拼接为sql瞅瞅.
我们还是使用ibd2sql来解析.
wget https://github.com/ddcw/ibd2sql/archive/refs/heads/main.zip
unzip main.zip
cd ibd2sql-main
vim ibd2sql/ibd2sql.py添加如下逻辑
from ibd2sql import encrypt
....
# 之前压缩页那再来个elif (我们没有提前解析fsp的encryption_metadata, 所以得把fd也搞过去.)
elif data[24:26] == b'\x00\x0f': # 15: 加密页
FIL_PAGE_VERSION,FIL_PAGE_ALGORITHM_V1,FIL_PAGE_ORIGINAL_TYPE_V1,FIL_PAGE_ORIGINAL_SIZE_V1,FIL_PAGE_COMPRESS_SIZE_V1 = struct.unpack('>BBHHH',data[26:34])
data = data[:24] + struct.pack('>H',FIL_PAGE_ORIGINAL_TYPE_V1) + b'\x00'*8 + data[34:38] + encrypt.decrypt(self.f,data[38:])
然后再把上面解密的代码整合一下得到encrypt.
# vim ibd2sql/encrypt.py
import struct
from Crypto.Cipher import AES
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
keyring_filename = '/usr/local/mysql/keyring/keyring2'
def read_keyring(data):
offset = 24
kd = {}
xor_str = '*305=Ljt0*!@$Hnm(*-9-w;:'.encode()
while True:
if data[offset:offset+3] == b'EOF':
break
total_length, key_id_length, key_type_length, user_id_length, key_length = struct.unpack_from('<QQQQQ', data, offset)
offset += 40
key_id = data[offset:offset+key_id_length].decode()
offset += key_id_length
key_type = data[offset:offset+key_type_length].decode()
offset += key_type_length
user_id = data[offset:offset+user_id_length]
offset += user_id_length
key = data[offset:offset+key_length]
keyt = bytes([key[i] ^ xor_str[i%24] for i in range(len(key))])
offset += key_length
kd[key_id] = {'key':keyt,'key_type':key_type}
if offset % 8 != 0:
offset += 8 - (offset % 8)
return kd
with open(keyring_filename,'rb') as f:
keyring_data = f.read()
def decrypt(f,bdata):
f.seek(0,0)
kd = read_keyring(keyring_data)
fsp = f.read(16384)
data = fsp[10390:10390+115]
master_id = struct.unpack('>L',data[3:7])[0]
server_uuid = data[7:7+36].decode()
master_key = kd['INNODBKey'+'-'+server_uuid+'-'+str(master_id)]['key']
ase = AES.new(master_key,AES.MODE_ECB)
key_info = ase.decrypt(data[43:43+32*2])
backend = default_backend()
cipher = Cipher(algorithms.AES(key_info[:32]), modes.CBC(key_info[32:48]),backend=backend)
decryptor = cipher.decryptor()
return decryptor.update(bdata)
直接解析加密的ibd文件 (作者又没加encrypt属性...)
看起来我们是解析成功的了.
mysql的加密数据是使用keyring来实rotate的. 即keyring文件中的master_key来加密fsp中的tablespace_key, 而数据页的加密实际上是使用tablespace_key来加密的. 如果加密文件丢了/损坏/替换了, 数据就恢复不了了. 加密主要是使用aes算法.(ecb模式和cbc模式都用了).
不建议使用数据库层的加密,比较耗费cpu.
解析的时候由于keyring替换了一次, 导致做校验的时候一直没通过, 找了很久原因. 最终看了下日志, 有[MY-012226] [InnoDB] Encryption information in datafile
才发现原因的..
可以根据文中的步骤来测试, 也可以等下个版本ibd2sql更新了再去测试.
参考:
https://dev.mysql.com/blog-archive/mysql-innodb-transparent-tablespace-encryption/
https://mysql.wisborg.dk/2019/01/28/automatic-decryption-of-mysql-binary-logs-using-python/
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。