暂无图片
暂无图片
1
暂无图片
暂无图片
暂无图片

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

原创 大大刺猬 2024-09-27
790

导读

上一章我们讲了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] [MY-012226] [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

大概如下图:
image.png
虽然图看着丑, 但意思就是这样的.
或者借用Mayank Prasad的图如下:
image.png

keyring file

现在来具体瞧瞧, 先看瞧瞧keyring file格式, 该格式是二进制的. 无法直接查看.
image.png
看了下源码, 复杂到离谱. 但好歹有大佬解析过的. 我们就直接看格式吧.

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

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)

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

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 | -------------------------------------------------------------------------- */

具体内容如下:

对象 大小(字节) 描述
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])

image.png
那怎么校验呢? 先别急.
官方为了支持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]

image.png

看来我们成功解析到了tablespace_key.
getimgdata.gif

解析加密后的数据文件

既然tablespace_key已经获取到了, 那就该解析数据了. 加密的格式和压缩页的格式是一样的. 那就只需要把解压换成解密就行了(就换一个汉字). 先看看长什么样子.

f.seek(4*16384,0) data = f.read(16384) struct.unpack('>BBHHH',data[26:34]) data[:200]

image.png
看起来是个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]

image.png
看到眼熟的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'\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属性…)
image.png

看起来我们是解析成功的了.
getimgdata2.gif

总结

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

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论