前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[MYSQL] mysql数据加密原理和解析

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

原创
作者头像
大大刺猬
发布2024-09-27 17:52:32
5660
发布2024-09-27 17:52:32
举报
文章被收录于专栏:大大刺猬

导读

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

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

mysql加密

低版本是使用plugin, 高版本使用Components.(花里胡哨的). 本次使用Plugin的方式安装keyring. 参考如下:

代码语言:shell
复制
# 配置文件添加如下信息:
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. 做校验的时候,没注意, 坑了我一手......

表加密

本次演示解析如下表

代码语言:sql
复制
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也是支持加密的. 虽然使用场景少

代码语言:sql
复制
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

代码语言:sql
复制
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组成. 格式如下:

对象

大小(字节)

描述

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代码来解析

代码语言:python
代码运行次数:0
复制
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格式整体比较简单, 就是得和一个常量做亦或比较坑人.

encryption_metadata

在解析得到master_key之后, 我们就可以解析fsp去获取tablespace_key了. 先看看fsp中记录的encryption_metadata格式吧. 总大小是115字节. 在我们之前解析sdi的时候有见到过(当时年轻,不知其含义)

代码语言:python
代码运行次数:0
复制
#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对应如下

代码语言:c++
复制
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.

代码语言:python
代码运行次数:0
复制
master_key = kd['INNODBKey'+'-'+server_uuid+'-'+str(master_id)]['key']

熟悉aes的cbc模式的小伙伴可能会疑惑,iv不是要求16字节么, 这里是使用的32字节啊.... (实际上是取的32字节中的前16字节. 小坑).

我们再使用代码解析下吧. 这里的crc32是使用的crc32算法, 可参考之前坏块校验

代码语言:python
代码运行次数:0
复制
## 解析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. 所以整了个校验位.... 我们来校验下.

代码语言:python
代码运行次数:0
复制
# 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已经获取到了, 那就该解析数据了. 加密的格式和压缩页的格式是一样的. 那就只需要把解压换成解密就行了(就换一个汉字). 先看看长什么样子.

代码语言:python
代码运行次数:0
复制
f.seek(4*16384,0)
data = f.read(16384)
struct.unpack('>BBHHH',data[26:34])
data[:200]

看起来是个index page. 而且数据全是加密的. 那就开始解密吧.

代码语言:python
代码运行次数:0
复制
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来解析.

代码语言:shell
复制
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.

代码语言:python
代码运行次数:0
复制
# 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/

https://github.com/ddcw/ibd2sql

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 导读
  • mysql加密
    • 表加密
      • 表空间加密
        • master_key轮换
        • mysql加密原理解析
        • keyring file
        • encryption_metadata
        • 解析加密后的数据文件
        • ibd2sql
        • 总结
        相关产品与服务
        云数据库 MySQL
        腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档