背景介绍

为了保障数据安全,MySQL 在 5.7 版本就支持了 InnoDB 表空间加密,之前写了一篇月报介绍过,参考InnoDB 表空间加密。文章开头也提到过,MariaDB 除了对表空间加密,也可以对 redo log 和 binlog 加密,本质上 redo log 和 binlog 中也保存着明文的数据,如果文件被拖走数据也有丢失的风险,因此在 MySQL 8.0 中也支持两种日志的加密,本文介绍 Binlog 的加密方式,建议先了解一下表空间加密,更容易理解。

使用方式

首先需要在 DB 启动的时候加载 Keyring,关于 Keyring 可以参考官方文档 或者上个小节提到的表空间加密的月报。


1. [mysqld]
2. early-plugin-load=keyring_file.so

控制是否对 Binlog 文件加密的开关是:binlog_encryption ,此开关可以动态打开或者关闭,修改会引起一次 Binlog rotate。需要用户具有 BINLOG_ENCRYPTION_ADMIN 权限。


1. mysql> set global binlog_encryption = ON;

配置完成后新的 Binlog 文件就是加密的了,加密是文件级别的,可以查看具体哪个文件被加密了:


1. mysql> show binary logs;
2. +------------------+-----------+-----------+
3. | Log_name         | File_size | Encrypted |
4. +------------------+-----------+-----------+
5. | mysql-bin.000001 |       178 | No        |
6. | mysql-bin.000002 |       178 | No        |
7. | mysql-bin.000003 |       202 | No        |
8. | mysql-bin.000004 |       714 | Yes       |
9. | mysql-bin.000005 |       178 | No        |
10. | mysql-bin.000006 |       178 | No        |
11. | mysql-bin.000007 |       856 | No        |
12. | mysql-bin.000008 |       707 | Yes       |
13. +------------------+-----------+-----------+

原理解析

同样为了支持 Key rotate,秘钥分为 master key 和 file password, 其中 master key 保存在 keyring 中,用来加密 file password, 这样每次 key rotate 的时候,只需要用新的 master key 把所有 Binlog 文件的 file password 重新加密一遍即可。

image.png

如图所示,master key 的密文是保存在 Keyring 中的,明文是固定的格式: MySQLReplicationKey_{UUID}_{SEQ_NO} , 其中 SEQ_NO 是每次 Key rotate 的时候自增的。因为由明文获得 Keyring 中的密文是不可逆的加密,因此明文简单点也不要紧,我们需要保证的是 Keyring 的安全。

filepassword 是保存在每个 Binlog 文件的头部的,文件头部新增的数据格式如下:

image.png

这部分是不加密的,一个文件是否加密是用 Magic num 来确定的,(0xFE62696E) 不加密, (0xFD62696E), 加密。每次打开一个文件的时候,都先判断 Magic num,确定是否需要解密。Version 不用多解释,数据格式高低版本兼容的时候用的到。Encryption Key Id 保存的就是 master key 的明文。File Password 就是加密过之后的 filepassword。IV 是从 OpenSSL 中随机生成的,解密算法需要 key 和 IV。

为了保证 key rotate 的崩溃恢复,在 Keyring 中的保存的不仅仅是 master key 的密文,还有 seqno, 那么保存 seqno 的明文是什么呢 ? 有以下几种:

  • MySQLReplicationKey_{UUID}
  • old_MySQLReplicationKey_{UUID}
  • new_MySQLReplicationKey_{UUID}
  • last_purged_MySQLReplicationKey_{UUID}

举个例子,Rotate 的时候需要获得一个新的 seqno,如果出现了 crash,重启的时候如何获得老的 seqno 呢 ?因此在 rotate 的时候会先把老的 seqno 放到 old_MySQLReplicationKey_{UUID} 为明文的 keyring 中。

代码解析

核心类

Binlog_encryption_ostream 类负责写入流程,继承了 Truncatable_ostream,和之前写文件的 IO_CACHE_stream 类似, m_down_ostream 是 IO_CACHE_stream 接口,加密后写到文件中,从 m_header 中获得 file password。 具体的加密和解密工作由 m_encryptor 负责。


1. class Binlog_encryption_ostream : public Truncatable_ostream {
2. public:
3. private:
4. std::unique_ptr<Truncatable_ostream> m_down_ostream;
5. std::unique_ptr<Rpl_encryption_header> m_header;
6. std::unique_ptr<Rpl_cipher> m_encryptor;
7. }

这两个类负责管理 Binlog 文件头保存的信息,V1 是目前的版本,说明官方设计代码的时候考虑到了以后数据格式的变化。


1. class Rpl_encryption_header_v1 : public Rpl_encryption_header {
2. private:
3. /* The key ID of the keyring key that encrypted the password */
4. std::string m_key_id;
5. /* The encrypted file password */
6. Key_string m_encrypted_password;
7. /* The IV used to encrypt/decrypt the file password */
8. Key_string m_iv;
9. }

Rpl_encryption 类负责管理 master key,和 keyring 交互,包括 key rotate 和崩溃恢复, 在代码中是一个单例。


1. class Rpl_encryption {
2. /* master key id 接口*/
3. struct Rpl_encryption_key {
4. std::string m_id;
5. Key_string m_value;
6. };
7. }

初始化

加密是文档级别的,在打开每个 binlog 的文件会去判断 Encryption 是不是 enable 了,如果判断需要加密,就初始化 m_pipiline_head 为 Binlog_encryption_ostream.


1. /* 照常打开 Binlog_ofile */
2. bool MYSQL_BIN_LOG::Binlog_ofile::open(
3. const char *binlog_name, myf flags, bool existing = false)) {
4. /* 正常的打开 IO_CACHE_ostream */
5. std::unique_ptr<IO_CACHE_ostream> file_ostream(new IO_CACHE_ostream);
6. if (file_ostream->open(log_file_key, binlog_name, flags)) DBUG_RETURN(true);

8. m_pipeline_head = std::move(file_ostream);

10. /* Setup encryption for new files if needed */
11. if (!existing && rpl_encryption.is_enabled()) {
12. std::unique_ptr<Binlog_encryption_ostream> encrypted_ostream(
13. new Binlog_encryption_ostream());
14. /* 把刚刚打开的 IO_CACHE_ostream 放到 Binlog_encryption_ostream::down_ostream */
15. /* 加密完成之后会继续用 down_ostream 写到文件里 */
16. if (encrypted_ostream->open(std::move(m_pipeline_head)))
17. DBUG_RETURN(true);
18. m_encrypted_header_size = encrypted_ostream->get_header_size();
19. m_pipeline_head = std::move(encrypted_ostream);
20. }
21. }

加密

加密的入口是 Binlog_encryption_ostream::write 函数,具体加密的工作是由 Rpl_cipher::encrypt 来做的,而 Rpl_cipher 需要的加密所用的 key 是由 Rpl_encryption_header 提供的。


1. bool Binlog_encryption_ostream::open(
2. std::unique_ptr<Truncatable_ostream> down_ostream) {
3. DBUG_ASSERT(down_ostream != nullptr);

5. m_header = Rpl_encryption_header::get_new_default_header();
6. /* 从 header 中产生一个 random 的 filepassword,然后用 master key 加密*/
7. const Key_string password_str = m_header->generate_new_file_password();
8. /* 取出 Aes_ctr,目前的加密方式是 Aes,是一个子类的具体实现 */
9. m_encryptor = m_header->get_encryptor();
10. }·

Binlog_encryption_ostream::write 中按照 ENCRYPT_BUFFER_SIZE = 2048 的大小块加密文件,加密后写到 IO_CACHE_ostream 中。


1. bool Binlog_encryption_ostream::write(const unsigned char *buffer,
2. my_off_t length) {
3. /*
4. Split the data in 'buffer' to ENCRYPT_BUFFER_SIZE bytes chunks and
5. encrypt them one by one.
6. */
7. while (length > 0) {
8. int encrypt_len =
9. std::min(length, static_cast<my_off_t>(ENCRYPT_BUFFER_SIZE));

11. if (m_encryptor->encrypt(encrypt_buffer, ptr, encrypt_len)) {
12. THROW_RPL_ENCRYPTION_FAILED_TO_ENCRYPT_ERROR;
13. return true;
14. }

16. if (m_down_ostream->write(encrypt_buffer, encrypt_len)) return true;

18. ptr += encrypt_len;
19. length -= encrypt_len;
20. }
21. }

解密

一个 Binlog 文件是不是加密的,是有文件头部的 magic num 决定的,当打开一个文件后,会调用函数 Basic_binlog_ifile::read_binlog_magic(),取出 magic num 后判断是否加密,以此来初始化。encryption_istream 的管理类似 Binlog_encryption_ostream,不在赘述。


1. bool Basic_binlog_ifile::read_binlog_magic() {
2. /*
3. If this is an encrypted stream, read encryption header and setup up
4. encryption stream pipeline.
5. */
6. if (memcmp(magic, Rpl_encryption_header::ENCRYPTION_MAGIC,
7. Rpl_encryption_header::ENCRYPTION_MAGIC_SIZE) == 0) {

9. std::unique_ptr<Binlog_encryption_istream> encryption_istream{
10. new Binlog_encryption_istream()};
11. if (encryption_istream->open(std::move(m_istream), m_error))
12. DBUG_RETURN(true);

14. /* Setup encryption stream pipeline */
15. m_istream = std::move(encryption_istream);

17. /* Read binlog magic from encrypted data */
18. if (m_istream->read(magic, BINLOG_MAGIC_SIZE) != BINLOG_MAGIC_SIZE) {
19. DBUG_RETURN(m_error->set_type(Binlog_read_error::BAD_BINLOG_MAGIC));
20. }

22. }
23. }

MASTER KEY ROTATE

Rotate 分为几个阶段,代码上从上面的阶段可以走到下面的阶段,在 recover_master_key 的时候会直接走到对应的的阶段去。


1. enum class Key_rotation_step {
2. START,
3. DETERMINE_NEXT_SEQNO,
4. GENERATE_NEW_MASTER_KEY,
5. REMOVE_MASTER_KEY_INDEX,
6. STORE_MASTER_KEY_INDEX,
7. ROTATE_LOGS,
8. PURGE_UNUSED_ENCRYPTION_KEYS,
9. REMOVE_KEY_ROTATION_TAG
10. };

每个阶段都做什么:

  1. START: 把现有的 seqno 放到 keyring 中,key 是 ‘old’ 字样的开头

```

  1. if (m_master_key_seqno > 0) {
  2. / We do not store old master key seqno into Keyring if it is zero. /
  3. if (set_old_master_key_seqno_on_keyring(m_master_key_seqno)) goto err1;
  4. } ```
  5. DETERMINE_NEXT_SEQNO: 循环遍历下一个 sequno 是多少,从当前的 seqno 递增。

```

  1. do {
  2. ++new_master_key_seqno;
  3. / Check if the key already exists /
  4. std::string candidate_key_id =
  5. Rpl_encryption_header::seqno_to_key_id(new_master_key_seqno);
  6. auto pair =
  7. get_key(candidate_key_id, Rpl_encryption_header::get_key_type());
  8. / If unable to check if the key already exists /
  9. if ((pair.first != Keyring_status::KEY_NOT_FOUND &&
  10. pair.first != Keyring_status::SUCCESS) ||
  11. DBUG_EVALUATE_IF("fail_to_fetch_key_from_keyring", true, false)) {
  12. Rpl_encryption::report_keyring_error(pair.first);
  13. goto err1;
  14. }
  15. / If the key already exists on keyring /
  16. candidate_key_fetch_status = pair.first;
  17. } while (candidate_key_fetch_status != Keyring_status::KEY_NOT_FOUND);
  18. // 找到之后放到 keyring 中,加上 new 关键字。
  19. if (set_new_master_key_seqno_on_keyring(new_master_key_seqno)) goto err1; ```
  20. GENERATE_NEW_MASTER_KEY:这一步会重新获得全局 Rpl_encryption 中的 master key,用来加密后面的数据

```

  1. /*
  2. Request the keyring to generate a new master key by key id
  3. "MySQLReplicationKey_{UUID}_{SEQNO}" using
  4. new master key SEQNO as SEQNO.
  5. */
  6. if (generate_master_key_on_keyring(new_master_key_seqno)) goto err1; ```
  7. REMOVE_MASTER_KEY_INDEX:把老的 seqno 移除。

```

  1. /*
  2. We did not store a master key seqno into keyring if
  3. m_master_key_seqno is 0.
  4. */
  5. if (m_master_key_seqno != 0) {
  6. if (remove_master_key_seqno_from_keyring()) goto err1;
  7. } ```
  8. STORE_MASTER_KEY_INDEX : 把新的 seqno 用正常的 key (不带关键字)存起来

```

  1. if (set_master_key_seqno_on_keyring(new_master_key_seqno)) goto err1; ```
  2. ROTATE_LOGS:rotate binlog 和 relay log, 从后往前遍历所有文件,重新加密 filepassword

```

  1. / We do not rotate and re-encrypt logs during recovery. /
  2. if (m_master_key_recovered && current_thd) {
  3. /*
  4. Rotate binary logs and re-encrypt previous existent
  5. binary logs.
  6. */
  7. if (mysql_bin_log.is_open()) {
  8. if (DBUG_EVALUATE_IF("fail_to_rotate_binary_log", true, false) ||
  9. mysql_bin_log.rotate_and_purge(current_thd, true)) {
  10. goto err2;
  11. }
  12. if (mysql_bin_log.reencrypt_logs()) return true;
  13. }
  14. / Rotate relay logs and re-encrypt previous existent relay logs. /
  15. if (flush_relay_logs_cmd(current_thd)) goto err2;
  16. if (reencrypt_relay_logs()) return true;
  17. } ```
  18. PURGE_UNUSED_ENCRYPTION_KEYS : 把带 ‘last_purged’ 的关键字 keyring 的 seqno 删除。
  19. REMOVE_KEY_ROTATION_TAG : 把第二步带 ‘new’ 关键字的 keyring 的 seqno 删除。

总结

Binlog 加密对于数据安全性非常必要,在 8.0.17 开始使用 AES-CTR 加密 binlog temp file, 网络传输中的依然是明文,需要使用网络加密来保证。

原文:http://mysql.taobao.org/monthly/2019/12/02/