从源码分析 MySQL 身份验证插件的实现细节
最近在分析ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)
这个报错的常见原因。
在分析的过程中,不可避免会涉及到 MySQL 身份验证的一些实现细节。
加之之前对这一块就有很多疑问,包括:
- 一个明文密码,是如何生成 mysql.user 表中的 authentication_string?
- 在进行身份验证时,客户端是否会直接发送明文密码给 MySQL 服务端?
- MySQL 8.0 为什么要将默认的身份认证插件调整为 caching_sha2_password,mysql_native_password 有什么问题嘛?
所以,就从代码层面对 MySQL 身份验证插件(主要是 mysql_native_password)的一些实现细节进行了分析。
本文主要包括以下几部分:
- 服务端是如何对明文密码进行加密的?
- 服务端是如何进行客户端身份验证的?
- 客户端是如何处理明文密码的?会直接发送明文密码给服务端么?
- 服务端是如何验证客户端密码是否正确的?
- 为什么 MySQL 8.0 要将默认的身份认证插件调整为 caching_sha2_password?
服务端是如何对明文密码进行加密的?
在 mysql_native_password 中,对明文密码进行加密是在 my_make_scrambled_password_sha1
函数中实现的。
// sql/auth/password.cc
void my_make_scrambled_password_sha1(char *to, const char *password,
size_t pass_len) {
uint8 hash_stage2[SHA1_HASH_SIZE];
/* Two stage SHA1 hash of the password. */
compute_two_stage_sha1_hash(password, pass_len, (uint8 *)to, hash_stage2);
/* convert hash_stage2 to hex string */
*to++ = PVERSION41_CHAR;
octet2hex(to, (const char *)hash_stage2, SHA1_HASH_SIZE);
}
// sql/auth/password.cc
inline static void compute_two_stage_sha1_hash(const char *password,
size_t pass_len,
uint8 *hash_stage1,
uint8 *hash_stage2) {
/* Stage 1: hash password */
compute_sha1_hash(hash_stage1, password, pass_len);
/* Stage 2 : hash first stage's output. */
compute_sha1_hash(hash_stage2, (const char *)hash_stage1, SHA1_HASH_SIZE);
}
实现其实非常简单:
使用 OpenSSL 库中的函数对输入的密码进行 SHA-1 哈希,生成 hash_stage1。
对生成的 hash_stage1 进行二次 SHA-1 哈希,生成 hash_stage2。
将 hash_stage2 转换为十六进制表示。
最后生成的字符串即我们在mysql.user
中看到的authentication_string
。
相同的功能用下面这段 Python 代码很容易就能实现出来。
import hashlib
def compute_sha1_hash(data):
sha1 = hashlib.sha1()
sha1.update(data)
return sha1.digest()
password = "123456".encode('utf-8')
hash_stage1 = compute_sha1_hash(password)
hash_stage2 = compute_sha1_hash(hash_stage1)
print('*%s'%hash_stage2.hex().upper())
密码是123456
,最后打印的结果是 *6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9
。
同mysql.user
中的authentication_string
的值完全一样。
mysql> create user u1@'%' identified with mysql_native_password by '123456';
Query OK, 0 rows affected (0.04 sec)
mysql> select user,host,authentication_string from mysql.user where user='u1';
+------+------+-------------------------------------------+
| user | host | authentication_string |
+------+------+-------------------------------------------+
| u1 | % | *6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9 |
+------+------+-------------------------------------------+
1 row in set (0.00 sec)
有木有一种很简单的感觉?
服务端是如何进行客户端身份验证的?
在 mysql_native_password 中,对客户端进行身份验证是在 native_password_authenticate
函数中实现的。
static int native_password_authenticate(MYSQL_PLUGIN_VIO *vio,
MYSQL_SERVER_AUTH_INFO *info) {
uchar *pkt;
int pkt_len;
MPVIO_EXT *mpvio = (MPVIO_EXT *)vio;
DBUG_TRACE;
// 生成盐值(Salt)。
if (mpvio->scramble[SCRAMBLE_LENGTH])
generate_user_salt(mpvio->scramble, SCRAMBLE_LENGTH + 1);
// 将盐值发送给客户端
if (mpvio->write_packet(mpvio, (uchar *)mpvio->scramble, SCRAMBLE_LENGTH + 1))
return CR_AUTH_HANDSHAKE;
// 读取客户端的响应,其中pkt用来存储响应包的内容,pkt_len是包的长度。
if ((pkt_len = mpvio->read_packet(mpvio, &pkt)) < 0) return CR_AUTH_HANDSHAKE;
DBUG_PRINT("info", ("reply read : pkt_len=%d", pkt_len));
...
// 如果响应包的长度为0,则意味着客户端没有指定密码
if (pkt_len == 0) {
info->password_used = PASSWORD_USED_NO;
return mpvio->acl_user->credentials[PRIMARY_CRED].m_salt_len != 0
? CR_AUTH_USER_CREDENTIALS
: CR_OK;
} else
info->password_used = PASSWORD_USED_YES;
bool second = false;
// 如果响应包的长度等于盐值的长度,则会验证密码是否正确。
if (pkt_len == SCRAMBLE_LENGTH) {
if (!mpvio->acl_user->credentials[PRIMARY_CRED].m_salt_len ||
check_scramble(pkt, mpvio->scramble,
mpvio->acl_user->credentials[PRIMARY_CRED].m_salt)) {
second = true;
// 如果验证失败,则会验证第二个密码是否设置且正确。
// 在 MySQL 8.0 中,一个账户可以设置两个密码。
if (!mpvio->acl_user->credentials[SECOND_CRED].m_salt_len ||
check_scramble(pkt, mpvio->scramble,
mpvio->acl_user->credentials[SECOND_CRED].m_salt)) {
return CR_AUTH_USER_CREDENTIALS;
} else {
if (second) {...}
return CR_OK;
}
} else {
return CR_OK;
}
}
my_error(ER_HANDSHAKE_ERROR, MYF(0));
return CR_AUTH_HANDSHAKE;
}
该函数的主要作用如下:
通过
generate_user_salt
生成一个 20 位的盐值(Salt)。"盐值"(Salt)是密码学中一个常用的概念。它是一个随机生成的数据块,通常与密码一同进行哈希。
相同的密码,由于盐值的不同,生成的哈希值也会不同。
引入盐值可有效防止彩虹表攻击和碰撞攻击,提高密码的安全性。
将盐值发送给客户端。客户端会基于盐值对明文密码进行加密(具体的加密细节后面会介绍),然后将加密后的结果返回给服务端。
读取客户端的响应。
如果响应包的长度等于盐值的长度,则会调用
check_scramble
验证客户端返回的加密密码是否与数据库中存储的加密密码相匹配(具体的匹配细节后面会介绍)。
客户端是如何处理明文密码的?
这里以 JDBC 驱动为例,客户端在接受到 MySQL 服务端发送的盐值后,会调用Security
类中的scramble411
方法对明文密码进行加密。
下面我们看看具体的实现细节。
// src/main/core-impl/java/com/mysql/cj/protocol/Security.java
public static byte[] scramble411(byte[] password, byte[] seed) {
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException ex) {
throw new AssertionFailedException(ex);
}
byte[] passwordHashStage1 = md.digest(password);
md.reset();
byte[] passwordHashStage2 = md.digest(passwordHashStage1);
md.reset();
md.update(seed);
md.update(passwordHashStage2);
byte[] toBeXord = md.digest();
int numToXor = toBeXord.length;
for (int i = 0; i < numToXor; i++) {
toBeXord[i] = (byte) (toBeXord[i] ^ passwordHashStage1[i]);
}
return toBeXord;
}
该方法的主要作用如下:
使用 SHA-1 算法对明文密码(password)进行哈希,生成 passwordHashStage1。
对生成的 passwordHashStage1 再次使用 SHA-1 算法进行哈希,生成 passwordHashStage2。
调用
md.update
方法将 seed(服务端发送的盐值)和 passwordHashStage2 添加到消息摘要中。调用
md.digest
获取最终的摘要值。将摘要值中的每个字节与 passwordHashStage1 对应位置的字节进行异或运算。
这么做,主要为了增加密码处理的复杂性,使得密码在传输过程中较难被破解。
简单来说,就是客户端基于服务端发送的盐值对明文密码进行加密,最后将加密后的结果发送给服务端,并不会直接发送明文密码。
服务端是如何验证客户端密码是否正确的?
在 mysql_native_password 中,验证客户端密码是否正确是在check_scramble_sha1
函数中实现的。
static bool check_scramble_sha1(const uchar *scramble_arg, const char *message,
const uint8 *hash_stage2) {
uint8 buf[SHA1_HASH_SIZE];
uint8 hash_stage2_reassured[SHA1_HASH_SIZE];
/* create key to encrypt scramble */
compute_sha1_hash_multi(buf, message, SCRAMBLE_LENGTH,
(const char *)hash_stage2, SHA1_HASH_SIZE);
/* encrypt scramble */
my_crypt((char *)buf, buf, scramble_arg, SCRAMBLE_LENGTH);
/* now buf supposedly contains hash_stage1: so we can get hash_stage2 */
compute_sha1_hash(hash_stage2_reassured, (const char *)buf, SHA1_HASH_SIZE);
return (memcmp(hash_stage2, hash_stage2_reassured, SHA1_HASH_SIZE) != 0);
}
函数中的 scramble_arg 是客户端返回的加密密码,message 是盐值,hash_stage2 是 authentication_string 的二进制表示。
该函数的具体实现如下:
调用
compute_sha1_hash_multi
计算 message 和 hash_stage2 的 SHA-1 哈希值,对应客户端实现中的3、4步。将步骤 1 中的结果与客户端返回的加密密码进行异或运算。
因为
XOR(s1, XOR(s1, s2)) == s2
,所以最后得到的结果实际上就是客户端实现中的 passwordHashStage1。调用
compute_sha1_hash
对 passwordHashStage1 进行一次 SHA-1 哈希,生成 hash_stage2_reassured。判断 authentication_string 的二进制表示是否与 hash_stage2_reassured 相同。
如果相同,则意味着客户端输入的密码是正确的,否则是错误的。
看上去有点复杂,但实际上它跟客户端的实现类似。
为什么 MySQL 8.0 要将默认的密码认证插件调整为 caching_sha2_password
在 MySQL 8.0 中,默认的密码认证插件由 mysql_native_password 调整为了 caching_sha2_password。
官方为什么要做这个调整呢?
主要原因还是因为 mysql_native_password 不够安全。
不够安全主要体现在以下两点:
SHA-1 自身不再安全。这主要是指 SHA-1 存在碰撞漏洞,即两个不同的输入可以产生相同的哈希值。
容易引起彩虹表攻击。
在 mysql_native_password 中,对于同一个明文密码,会生成一个确定的加密密码。
如
123456
对应的加密密码永远是*6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9
,这就很容易引起彩虹表攻击。
彩虹表(Rainbow Table)是一种密码破解技术,其核心思想是事先计算并存储大量可能的密码和其对应的哈希值。这样,当攻击者获取到加密系统中存储的哈希值时,就可以直接查找对应的明文密码,而无需进行逐一尝试的破解。
所以只要获取到 mysql.user 表 authentication_string 字段的内容,再加上事先构建的彩虹表,破解出明文密码并不是一件难事。
这里顺便介绍个黑科技,在 MySQL 8.0 之前,因为 mysql.user 表使用的是 MyISAM 存储引擎,所以,只要有主机登陆权限,就能通过 vim 查看 authentication_string 字段的内容。
总结
1. mysql.user 中的 authentication_string 字段存储的是HEX(SHA1(SHA1(password)))
。
2. 服务端对客户端进行身份验证的流程图如下:
服务端在对客户端进行身份验证时,会首先发送一个 20 字节的盐值,客户端接受到这个盐值后,会返回一个通过以下公式计算的加密密码。
SHA1(password) XOR SHA1(seed <concat> SHA1(SHA1(password)))
3. 因为 mysql_native_password 容易引起彩虹表攻击,且 SHA-1 本身就不够安全,所以在 MySQL 8.0 中,默认的是身份验证插件由 mysql_native_password 调整为了 caching_sha2_password。
实际上,caching_sha2_password 底层使用的加密算法(SHA-256)早在 sha256_password 这个认证插件( MySQL 5.6 中引入的)中就使用了。虽然 sha256_password 足够安全,但因为认证速度比较慢,性能不理想,所以在线上用得并不多。
4. caching_sha2_password 在 sha256_password 的基础上,新增了一个内存缓存,用于存储哈希密码,以加快认证速度。
5. 为了方便大家理解 mysql_native_password 的实现细节,我写了个 Python 程序,完整地呈现了 mysql_native_password 与客户端交互的整个流程,感兴趣的童鞋可参考:https://github.com/slowtech/dba-toolkit/blob/master/mysql/mysql_native_password.py
参考
Native Authentication:https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_authentication_methods_native_password_authentication.html
WL#9591: Caching sha2 authentication plugin:https://dev.mysql.com/worklog/task/?id=9591
WL#10774: Remove old_passwords, PASSWORD(), other deprecated auth features:https://dev.mysql.com/worklog/task/?id=10774
从源码分析 MySQL 身份验证插件的实现细节的更多相关文章
- Kettle 4.2源码分析第二讲--Kettle插件结构体系简介
1. 插件体系结构 1.1. 插件技术原理 1.1.1. 插件概念说明 插件是一种遵循统一的预定义接口规范编写出来的程序,应用程序在运行时通过接口规范对插件进行调用,以扩展应用程序的功能.在英 ...
- 源码分析MySQL mysql_real_query函数
目录 目录 1 1. 前言 1 2. 调用路径 2 3. MAX_PACKET_LENGTH宏 2 4. DBUG_RETURN宏 3 5. COM_QUERY枚举值 3 6. mysql_query ...
- 源码分析-mysql
问题: mysql GROUP BY 返回结果 各个字段所在行
- MyBatis 源码分析 - 插件机制
1.简介 一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展.这样的好处是显而易见的,一是增加了框架的灵活性.二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作.以 My ...
- 插件开发之360 DroidPlugin源码分析(四)Activity预注册占坑
请尊重分享成果,转载请注明出处: http://blog.csdn.net/hejjunlin/article/details/52258434 在了解系统的activity,service,broa ...
- Flask框架 (四)—— 请求上下文源码分析、g对象、第三方插件(flask_session、flask_script、wtforms)、信号
Flask框架 (四)—— 请求上下文源码分析.g对象.第三方插件(flask_session.flask_script.wtforms).信号 目录 请求上下文源码分析.g对象.第三方插件(flas ...
- MyCat源码分析系列之——前后端验证
更多MyCat源码分析,请戳MyCat源码分析系列 MyCat前端验证 MyCat的前端验证指的是应用连接MyCat时进行的用户验证过程,如使用MySQL客户端时,$ mysql -uroot -pr ...
- MySQL源码分析以及目录结构 2
原文地址:MySQL源码分析以及目录结构作者:jacky民工 主要模块及数据流经过多年的发展,mysql的主要模块已经稳定,基本不会有大的修改.本文将对MySQL的整体架构及重要目录进行讲述. 源码结 ...
- MySQL源码分析以及目录结构
原文地址:MySQL源码分析以及目录结构作者:jacky民工 主要模块及数据流经过多年的发展,mysql的主要模块已经稳定,基本不会有大的修改.本文将对MySQL的整体架构及重要目录进行讲述. 源码结 ...
- [Abp vNext 源码分析] - 7. 权限与验证
一.简要说明 在上篇文章里面,我们在 ApplicationService 当中看到了权限检测代码,通过注入 IAuthorizationService 就可以实现权限检测.不过跳转到源码才发现,这个 ...
随机推荐
- #POWERBI_指标监控(第二部分,周期内下降天数及日期明细)
在指标监控的第一部分文章中,我们已经讲了,如何用DAX去查询一段周期内连续下降或者上升指标. 需要复习的同学可以点击下方链接: https://www.cnblogs.com/simone331/p/ ...
- Python socket实现ftp文件下载服务
简要 使用Python socket和多线程实现一个FTP服务下载.下面的示例是固定下载某一个任意格式文件. 仅仅为了展示如果使用socket和多线程进行文件下载 服务端代码 import socke ...
- osx12.6设置全屏
首先安装VMWARE TOOLS就是unlocker208下的tool的darwin.iso 然后进入终端命令,这里要说一下,好多资料说按COMMAND键,反正我是没有进去,直接在vmware菜单里选 ...
- Emit 实体绑定源码开源,支持类以及匿名类绑定(原创)
动态实体绑定 主要有以下两种 1.表达式树构建委托 2.Emit构建委托 根据我的经验 Emit 代码量可以更少可以很好实现代码复用 Emit实践开源项目地址跳转 https://www.cnblog ...
- [数据分析与可视化] 基于Python绘制简单动图
动画是一种高效的可视化工具,能够提升用户的吸引力和视觉体验,有助于以富有意义的方式呈现数据可视化.本文的主要介绍在Python中两种简单制作动图的方法.其中一种方法是使用matplotlib的Anim ...
- P3870 [TJOI2009] 开关(线段树)
P3870 [TJOI2009] 开关 思路:可以用线段树来维护区间中亮灯的个数,区间修改用加上懒标记就好 #include <bits/stdc++.h> #define LL long ...
- 实战攻防演练-利用Everything搜索软件进行内网后渗透利用
前言 Everything是一款很出名的文件搜索工具,基于文件.文件夹名称的快速搜索的轻量级的软件,而早在几年前就有很多apt组织利用everything来进行文件查找等,前几年在T00ls上也有人发 ...
- MVVM前后端分离:web接口规范
大前端前提下,开发采用前后端分离的方式,前端和后端主要通过接口进行分离, 后端开发接口,前端使用接口,前后端接口开发告一段落以后,接口联调差不多就进入开发尾声,准备送测了. 那么,对接口的约束和规范就 ...
- "拍牌神器"是怎样炼成的(二)--- 键鼠模拟之AutoIt
不同于上一篇的WinAPI方法,这次让我们来看另一个更简单.有效的键鼠模拟方案,即通过COM组件AutoItX实现键鼠模拟. AutoIt AutoIt是一个免费软件,它使用一种类似BASIC的脚本语 ...
- 如何深度学习Python?
安装必要软件:首先需要安装Anaconda或Miniconda等科研计算环境,并创建虚拟环境以便管理不同项目所需库和版本.可以按照如下步骤进行操作: 下载并安装 Anaconda 或 Minicond ...