[MYSQL] mysql数据加密原理和解析

导读

上一章我们讲了mysql压缩原理(含lz4压缩格式)并解析, 细心的同学应该发现旁边就是加密的相关代码. 那本章就来讲讲mysql加密和解析.

理论上, 看完本篇文章, 就能通过 keyring文件解析ibd文件了. 仅考虑社区版的keyring插件

mysql加密

低版本是使用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']

master_key轮换

有时候一个key用久了, 就觉得不安全, 想换一个也是可以的. mysql支持轮转key

ALTER INSTANCE ROTATE MASTER KEY;

mysql加密原理解析

mysql的加密实际上是分为两部分的, keyring file里面存储了一系列master_key, 然后使用master_key加密tablespace_key(加密之后的tablespace_key放在fsp), tablespace_key才是用来加密数据page的

这种设计应该是为了支持轮转key

大概如下图:

虽然图看着丑, 但意思就是这样的.

或者借用Mayank Prasad的图如下:

keyring file

现在来具体瞧瞧, 先看瞧瞧keyring file格式, 该格式是二进制的. 无法直接查看.

看了下源码, 复杂到离谱. 但好歹有大佬解析过的. 我们就直接看格式吧.

其实也能猜到大概, 但做亦或那里就难发现了..

keyring_file由一系列master_key组成. 格式如下:

描述信息, 比如版本之类的

该master_key占的总大小

加密算法类型, 通常为AES

user_id,没发现有啥用…

fsp保存的key_id和这个呼应上了,就取这个的key

给tablespace_key加密的key, 得先和obfuscate_str做亦或

虽然看起来有丢丢复杂, 但实际上就一丢丢信息… 我们可以使用如下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('

我这里只有一个key, 如果做过rotate的, 或者给其它实例使用过的, 那么就会存在多个. 比如:

keyring格式整体比较简单, 就是得和一个常量做亦或比较坑人.

encryption_metadata

在解析得到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 |
    --------------------------------------------------------------------------
  */

具体内容如下:

空了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

我们还是使用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'x00x0f': # 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('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/

https://github.com/ddcw/ibd2sql

https://github.com/mysql/mysql-server

阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.shuli.cc/?p=19999,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?