背景介绍

为了保障数据安全,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);
  7. m_pipeline_head = std::move(file_ostream);
  8. /* Setup encryption for new files if needed */
  9. if (!existing && rpl_encryption.is_enabled()) {
  10. std::unique_ptr<Binlog_encryption_ostream> encrypted_ostream(
  11. new Binlog_encryption_ostream());
  12. /* 把刚刚打开的 IO_CACHE_ostream 放到 Binlog_encryption_ostream::down_ostream */
  13. /* 加密完成之后会继续用 down_ostream 写到文件里 */
  14. if (encrypted_ostream->open(std::move(m_pipeline_head)))
  15. DBUG_RETURN(true);
  16. m_encrypted_header_size = encrypted_ostream->get_header_size();
  17. m_pipeline_head = std::move(encrypted_ostream);
  18. }
  19. }

加密

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

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));
  10. if (m_encryptor->encrypt(encrypt_buffer, ptr, encrypt_len)) {
  11. THROW_RPL_ENCRYPTION_FAILED_TO_ENCRYPT_ERROR;
  12. return true;
  13. }
  14. if (m_down_ostream->write(encrypt_buffer, encrypt_len)) return true;
  15. ptr += encrypt_len;
  16. length -= encrypt_len;
  17. }
  18. }

解密

一个 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) {
  8. std::unique_ptr<Binlog_encryption_istream> encryption_istream{
  9. new Binlog_encryption_istream()};
  10. if (encryption_istream->open(std::move(m_istream), m_error))
  11. DBUG_RETURN(true);
  12. /* Setup encryption stream pipeline */
  13. m_istream = std::move(encryption_istream);
  14. /* Read binlog magic from encrypted data */
  15. if (m_istream->read(magic, BINLOG_MAGIC_SIZE) != BINLOG_MAGIC_SIZE) {
  16. DBUG_RETURN(m_error->set_type(Binlog_read_error::BAD_BINLOG_MAGIC));
  17. }
  18. }
  19. }

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. }
  2. 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;
  3. 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;
  4. 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. }
  5. STORE_MASTER_KEY_INDEX : 把新的 seqno 用正常的 key (不带关键字)存起来

    1. if (set_master_key_seqno_on_keyring(new_master_key_seqno)) goto err1;
  6. 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. }
  7. PURGE_UNUSED_ENCRYPTION_KEYS : 把带 ‘last_purged’ 的关键字 keyring 的 seqno 删除。

  8. REMOVE_KEY_ROTATION_TAG : 把第二步带 ‘new’ 关键字的 keyring 的 seqno 删除。

总结

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