github.com/XiaoMi/Gaea@v1.2.5/backend/direct_connection_cn.md (about) 1 # backend 包的直连 direct_connection 2 3 ## 代码说明 4 5 ### 第一步 初始交握,传送讯息方向为 MariaDB 至 Gaea 6 7 参考 [官方文档](https://mariadb.com/kb/en/connection/) ,有以下内容 8 9 <img src="./assets/image-20220315221559157.png" alt="image-20220315221559157" style="zoom:100%;" /> 10 11 根据官方文档,使用范例说明 12 13 | 内容 | 演示范例 | 14 | ------------------------------- | ------------------------------------------------------------ | 15 | int<1> protocol version | 协定 Protocol 版本为 10 | 16 | string<NUL> server version | 数据库的版本号 version 为<br /><br />[]uint8{<br />53, 46, 53, 46, 53,<br />45, 49, 48, 46, 53,<br />46, 49, 50, 45, 77,<br />97, 114, 105, 97, 68,<br />66, 45, 108, 111, 103<br />}<br /><br />对照 ASCII 表为<br />5.5.5-10.5.12-MariaDB-log | 17 | int<4> connection id | 连接编号为<br /><br />[]uint8{16, 0, 0, 0}<br />先反向排列为 []uint8{0, 0, 0, 16}<br /><br />最后求得的连接编号为 []uint32{16} | 18 | string<8> scramble 1st part | 第一部份的 Scramble,Scramble 总共需要组成 20 bytes,<br />第一个部份共 8 bytes,其值为 []uint8{81, 64, 43, 85, 76, 90, 97, 91} | 19 | string<1> reserved byte | 数值为 0 | 20 | int<2> server capabilities | 第一部份的功能标志 capability,数值为 []uint8{254, 247} | 21 | int<1> server default collation | 数据库编码 charset 为 33,经<br />以下 [文档](https://mariadb.com/kb/en/supported-character-sets-and-collations/) 查询<br />或者是 命令 SHOW CHARACTER SET LIKE 'utf8'; 查询,<br />charset 的数值为 utf8_general_ci | 22 | int<2> status flags | 服务器状态为 []uint8{2, 0}<br />进行反向排列为[]uint8{0, 2},再转成二进制为<br />[]uint{0b000000000, 0b00000010}.<br /><br />对照 Gaea/mysql/constants.go 后,得知目前服务器的状况为<br />Autocommit (ServerStatusAutocommit) | 23 | int<2> server capabilities | 延伸的功能标志 capability,数值为 []uint8{255, 129}. | 24 25 先对 功能标志 capability 进行计算 26 27 ``` 28 先把所有的功能标志 capability 的数据收集起来,包含延伸部份 29 30 数值分别为 []uint8{254, 247, 255, 129} 31 并反向排列 32 数值分别为 []uint8{129, 255, 247, 254} 33 全部 十进制 转成 二进制,为 []uint8{10000001, 11111111, 11110111, 11111110} (转成十进制数值为 2181036030) 34 35 再用 [文档](https://mariadb.com/kb/en/connection/) 进行对照 36 比如,功能标志 capability 的第一个值为 0,意思为 CLIENT_MYSQL 值为 0,代表是由服务器发出的讯息 37 ``` 38 39 接续上表 40 41 | 项目 | 内容 | 42 | ---- | ------------------------------------------------------------ | 43 | 公式 | if (server_capabilities & PLUGIN_AUTH)<br/> int<1> plugin data length <br/>else<br/> int<1> 0x00 | 44 | 范例 | 跳过 1 个 byte | 45 46 接续上表 47 48 | 项目 | 内容 | 49 | ---- | ---------------- | 50 | 公式 | string<6> filler | 51 | 范例 | 跳过 6 个 bytes | 52 53 接续上表 54 55 | 项目 | 内容 | 56 | ---- | ------------------------------------------------------------ | 57 | 公式 | if (server_capabilities & CLIENT_MYSQL)<br/> string<4> filler <br/>else<br/> int<4> server capabilities 3rd part .<br /> MariaDB specific flags /* MariaDB 10.2 or later */ | 58 | 范例 | 跳过 4 个 bytes | 59 60 接续上表 61 62 | 项目 | 内容 | 63 | ---- | ------------------------------------------------------------ | 64 | 公式 | if (server_capabilities & CLIENT_SECURE_CONNECTION)<br/> string<n> scramble 2nd part . Length = max(12, plugin data length - 9)<br/> string<1> reserved byte | 65 | 范例 | scramble 一共要 20 个 bytes,第一部份共 8 bytes,所以第二部份共有 20 - 8 = 12 bytes,该数值为 []uint8{34, 53, 36, 85, 93, 86, 117, 105, 49, 87, 65, 125} | 66 67 接续上表 68 69 | 项目 | 内容 | 70 | ---- | ------------------------------------------------------------ | 71 | 公式 | if (server_capabilities & PLUGIN_AUTH)<br/> string<NUL> authentication plugin name | 72 | 范例 | 之后的资料都不使用 | 73 74 合拼所有 Scramble 的资料 75 76 ``` 77 第一部份 Scramble 为 []uint8{81, 64, 43, 85, 76, 90, 97, 91} 78 第二部份 Scramble 为 []uint8{34, 53, 36, 85, 93, 86, 117, 105, 49, 87, 65, 125} 79 80 两部份 Scramble 合拼后为 []uint8{81, 64, 43, 85, 76, 90, 97, 91, 34, 53, 36, 85, 93, 86, 117, 105, 49, 87, 65, 125} 81 ``` 82 83 ### 第二步 计算用于验证密码的验证码 Auth 84 85 参考 [官方文档](https://dev.mysql.com/doc/internals/en/secure-password-authentication.html) ,有整个完整验证码的计算公式说明 86 87 验证码的公式如下 88 89 ``` 90 SHA1( 密码 ) XOR SHA1( "服务器所提供的乱数" <接合> SHA1( SHA1( 密码 ) ) ) 91 其中 92 stage1 = SHA1( 密码 ) 93 stage1Hash = SHA1( stage1 ) = SHA1( SHA1( 密码 ) ) 94 scramble = SHA1( scramble <接合> SHA1( stage1Hash ) ) // 第一次修改 scramble 的数值 95 scramble = stage1 XOR scramble // 第二次修改 scramble 的数值 96 ``` 97 98 假设 99 100 - 输入密码参数 password 为 12345 101 - 数据库服务器回传的乱数 scramble为 []uint8{81, 64, 43, 85, 76, 90, 97, 91, 34, 53, 36, 85, 93, 86, 117, 105, 49, 87, 65, 125},这是用十进位的方式来表示 102 如果用十六进位来表示的数值为 []uint8{51, 40, 2B, 55, 4c, 5a, 61, 5b, 22, 35, 24, 55, 5d, 56, 75, 69, 31, 57, 41, 7d},显示的数值为 51402B554c5A615b223524555d5675693157417d 103 104 计算 stage1 为 stage1 = SHA1( 密码 ),使用 bash 进行计算 105 106 ```bash 107 # 使用 Linux Bash 去计算和验证 stage1 108 $ echo -n 12345 | sha1sum | head -c 40 # 把 密码 12345 转成 stage1 109 8cb2237d0679ca88db6464eac60da96345513964 # 此为 stage1 的值 110 ``` 111 112 计算 stage1Hash 的数值,公式为 stage1Hash = SHA1( stage1 ) = SHA1( SHA1( 密码 ) ) 113 114 ```bash 115 # 使用 Linux Bash 去计算和验证 stage1Hash 116 117 $ echo -n 12345 | sha1sum | xxd -r -p | sha1sum | head -c 40 118 00a51f3f48415c7d4e8908980d443c29c69b60c9 # 此为 stage1hash 的值 119 120 $ echo -n 8cb2237d0679ca88db6464eac60da96345513964 | xxd -r -p | sha1sum | head -c 40 121 00a51f3f48415c7d4e8908980d443c29c69b60c9 # 此为 stage1hash 的值 122 ``` 123 124 计算 "服务器所提供的乱数" 和 SHA1( SHA1( 密码 ) ) 的接合值 125 126 ```bash 127 # scramble 的值为 51402B554c5A615b223524555d5675693157417d,为连接的前半段 128 # stage1Hash 的值为 00a51f3f48415c7d4e8908980d443c29c69b60c9,为连接的后半段 129 130 # 计算 "服务器所提供的乱数" <接合> SHA1( SHA1( 密码 ) ) 的接合值 131 $ echo -n 51402B554c5A615b223524555d5675693157417d 00a51f3f48415c7d4e8908980d443c29c69b60c9 | sed "s/ //g" 132 51402B554c5A615b223524555d5675693157417d00a51f3f48415c7d4e8908980d443c29c69b60c9 # 合拼后的接合值 133 ``` 134 135 计算第一次重写的 scramble,公式为 136 137 scramble = SHA1( 接合值) = SHA1( scramble <接合> SHA1( stage1Hash ) ) 138 139 ```bash 140 # 使用 Linux Bash 去计算和验证 第一次重写 scramble 141 $ echo -n 51402B554c5A615b223524555d5675693157417d00a51f3f48415c7d4e8908980d443c29c69b60c9 | xxd -r -p | sha1sum | head -c 40 142 0ca0f764a59d1cdb10a87f0155d61aa54be1c71a # 此为第一次修改的 scramble 143 ``` 144 145 计算第二次重写的 scramble,公式为 scramble = stage1 XOR scramble 146 147 ```bash 148 # 使用 Linux Bash 去计算和验证 第二次重写 scramble 149 $ stage1=0x8cb2237d0679ca88db6464eac60da96345513964 # 之前计算出来的 stage1 的数值 150 $ scramble=0x0ca0f764a59d1cdb10a87f0155d61aa54be1c71a # 第一次修改 scramble 的数值 151 $ echo $(( $stage1^$scramble )) 152 -7792437067003134338 # 错误答案,精度不足 153 154 $ stage1=0x8cb2237d0679ca88db6464eac60da96345513964 155 $ scramble=0x0ca0f764a59d1cdb10a87f0155d61aa54be1c71a 156 $ printf "0x%X" $(( (($stage1>>40)) ^ (($scrambleFirst>>40)) )) 157 0xFFFFFFFFFF93DBB3 # 错误答案,精度不足 158 159 # stage1 和 第一次修改的 scramble 分成四段执行 XOR 160 $ printf "0x%X" $(( ((0x8cb2237d06)) ^ ((0x0ca0f764a5)) )) 161 $ printf "%X" $(( ((0x79ca88db64)) ^ ((0x9d1cdb10a8)) )) 162 $ printf "%X" $(( ((0x64eac60da9)) ^ ((0x7f0155d61a)) )) 163 $ printf "%X" $(( ((0x6345513964)) ^ ((0xa54be1c71a)) )) 164 0x8012D419A3E4D653CBCC1BEB93DBB3C60EB0FE7E # 正确答案 165 166 # scramble 为 []uint8{ 80, 12, D4, 19, A3, E4, D6, 53, CB, CC, 1B, EB, 93, DB, B3, C6, 0E, B0, FE, 7E} // 十六进位 167 # 用十进位表示为 168 # scramble 为 []uint8{128, 18, 212, 25, 163, 228, 214, 83, 203, 204, 27, 235, 147, 219, 179, 198, 14, 176, 254, 126} // 十进位 (跟代码产出的答案相同) 169 ``` 170 171 下图为代码执行的结果,结果和用 Bash 推算的相同 172 173 <img src="./assets/image-20220318183833245.png" alt="image-20220318183833245" style="zoom:70%;" /> 174 175 ### 第三步 回应交握,传送讯息方向为 Gaea 至 MariaDB 176 177 参考 [官方文档](https://mariadb.com/kb/en/connection/) ,有以下内容 178 179 <img src="./assets/image-20220318083633693.png" alt="image-20220318083633693" style="zoom:100%;" /> 180 181 根据官方文档,使用范例说明,先对 Gaea 要处理的功能标志 capability 进行计算, 182 183 | capability 项目 | 二进位 | 十进位 | 184 | ---------------------------- | ------------------ | ------ | 185 | mysql.ClientProtocol41 | 0b0000001000000000 | 512 | 186 | mysql.ClientSecureConnection | 0b1000000000000000 | 32768 | 187 | mysql.ClientLongPassword | 0b0000000000000001 | 1 | 188 | mysql.ClientTransactions | 0b0010000000000000 | 8192 | 189 | mysql.ClientLongFlag | 0b0000000000000100 | 4 | 190 | | | | 191 | 总合 | | | 192 | Gaea 支持的 capability | 0b1010001000000101 | 41477 | 193 194 计算 Gaea 和 MariaDB 双方共同支持的 capability 195 196 ``` 197 在前面第一步里,dc 对象的 capability 为 0b10000001111111111111011111111110 (转成十进制数值为 2181036030),很明显地,这个 capability 并不支持 mysql.ClientLongPassword 198 199 进行 Gaea支持的capability 和 dc.capability 进行 AND 操作 200 Gaea支持的capability & dc.capability = []uint32{41477} & []uint32{2181036030} = []uint32{41476} 201 ``` 202 203 <img src="./assets/image-20220319002738908.png" alt="image-20220319002738908" style="zoom:70%;" /> 204 205 | 内容 | 演示范例 | 206 | --------------------------------- | ------------------------------------------------------------ | 207 | int<4> client capabilities | 经由上述计算为 []uint32{41476},但是传输过程中,会反向排列,所以传送的资料为 []uint8{4, 162, 0, 0}<br /><img src="./assets/image-20220319113026919.png" alt="image-20220319113026919" style="zoom:50%;" /> | 208 | int<4> max packet size | 官方文档有提到写入的值都为 0,传送的数值为 []uint8{0, 0, 0, 0} | 209 | int<1> client character collation | 在 [官方文档](https://mariadb.com/kb/en/supported-character-sets-and-collations/) 里有说明,以这个例子为 46 ,意思为 utf8mb4_bin | 210 | string<19> reserved | 全部写入为 0 的数值,传送的数值为 []uint8{<br /> 0, 0, 0, 0, 0,<br /> 0, 0, 0, 0, 0,<br /> 0, 0, 0, 0, 0,<br /> 0, 0, 0, 0,<br /> } | 211 212 根据官方文档,使用范例说明 213 214 | 项目 | 内容 | 215 | ---- | ------------------------------------------------------------ | 216 | 公式 | if not (server_capabilities & CLIENT_MYSQL)<br/> int<4> extended client capabilities <br/>else<br/> string<4> reserved | 217 | 范例 | **CLIENT_MYSQL** 意思为这封包是否为客户端的封包,目前为 True,<br />所以 **not (server_capabilities & CLIENT_MYSQL)** 的数值为 False,<br />全部写入为 0 的数值,传送的数值为 []uint8{<br /> 0, 0, 0, 0,<br /> } | 218 219 接续上表 220 221 | 项目 | 内容 | 222 | ---- | ------------------------------------------------------------ | 223 | 公式 | string<NUL> username | 224 | 范例 | 写入登录数据库的用户的名称 xiaomi,但最后再多写一个 0 作为中断结尾,<br />写入的资料为 []uint8{120, 105, 97, 111, 109, 105, 0} | 225 226 使用 Bash 进行验证 227 228 ```bash 229 $ echo -n xiaomi | od -td1 230 0000000 120 105 97 111 109 105 231 0000006 232 ``` 233 234 接续上表 235 236 | 项目 | 内容 | 237 | ---- | ------------------------------------------------------------ | 238 | 公式 | if (server_capabilities & PLUGIN_AUTH_LENENC_CLIENT_DATA)<br/> string<lenenc> authentication data <br/>else if (server_capabilities & CLIENT_SECURE_CONNECTION)<br/> int<1> length of authentication response<br/> string<fix> authentication response (length is indicated by previous field) <br/>else<br/> string<NUL> authentication response null ended | 239 | 范例 | 目前 Gaea 支持 CLIENT_SECURE_CONNECTION,<br /><br />前面已经计算出来,scramble 为 []uint8{128, 18, 212, 25, 163, 228, 214, 83, 203, 204, 27, 235, 147, 219, 179, 198, 14, 176, 254, 126}<br /><br />但是要先告知 MariaDB 服务器 scramble 的长度为 20<br />所以回传的资料为 []uint8{20, 128, 18, 212, 25, 163, 228, 214, 83, 203, 204, 27, 235, 147, 219, 179, 198, 14, 176, 254, 126} | 240 241 接续上表 242 243 | 项目 | 内容 | 244 | ---- | ------------------------------------------------------------ | 245 | 公式 | if (server_capabilities & CLIENT_CONNECT_WITH_DB)<br/> string<NUL> default database name | 246 | 范例 | 目前 Gaea 在直连 dc 要处理的 capabilities 如下<br />mysql.ClientProtocol41<br/>mysql.ClientSecureConnection<br/>mysql.ClientTransactions<br/>mysql.ClientLongFlag<br /><br />先略过此步骤 | 247 248 接续上表 249 250 | 项目 | 内容 | 251 | ---- | ------------------------------------------------------------ | 252 | 公式 | if (server_capabilities & CLIENT_PLUGIN_AUTH)<br/> string<NUL> authentication plugin name | 253 | 范例 | 目前 Gaea 在直连 dc 要处理的 capabilities 如下<br />mysql.ClientProtocol41<br/>mysql.ClientSecureConnection<br/>mysql.ClientTransactions<br/>mysql.ClientLongFlag<br /><br />先略过此步骤 | 254 255 接续上表 256 257 | 项目 | 内容 | 258 | ---- | ------------------------------------------------------------ | 259 | 公式 | if (server_capabilities & CLIENT_CONNECT_ATTRS)<br/> int<lenenc> size of connection attributes<br/> while packet has remaining data<br/> string<lenenc> key<br/> string<lenenc> value | 260 | 范例 | 目前 Gaea 在直连 dc 要处理的 capabilities 如下<br />mysql.ClientProtocol41<br/>mysql.ClientSecureConnection<br/>mysql.ClientTransactions<br/>mysql.ClientLongFlag<br /><br />先略过此步骤 | 261 262 ## 测试说明 263 264 > 以下会说明在写测试时考量的点 265 266 ### 匿名函数的考量 267 268 在以下代码内有一个 测试函数 t.Run("测试数据库后端连线初始交握后的回应", func(t *testing.T) 269 270 此测试函数内含 匿名函数 customFunc 271 272 以下代码,匿名函数 customFunc 内的变量将会取 dc 对象的内存位置,考量后,觉得可以这样写 273 274 主要是担心 错误的变数或者是错误的数值 会进入 匿名函数 275 276 ```go 277 // 交握第二步 Step2 278 t.Run("测试数据库后端连线初始交握后的回应", func(t *testing.T) { 279 var connForSengingMsgToMariadb = mysql.NewConn(mockGaea.GetConnWrite()) 280 dc.conn = connForSengingMsgToMariadb 281 dc.conn.StartWriterBuffering() 282 283 customFunc := func() { 284 err := dc.writeHandshakeResponse41() 285 require.Equal(t, err, nil) 286 err = dc.conn.Flush() 287 require.Equal(t, err, nil) 288 err = mockGaea.GetConnWrite().Close() 289 require.Equal(t, err, nil) 290 } 291 292 fmt.Println(mockGaea.CustomSend(customFunc).ArrivedMsg(mockMariaDB)) 293 }) 294 ``` 295 296 ## 验证 297 298 使用 Linux 命令 或者是 网站 去计算 Sha1sum 时,计算出来的结果为 16 进位,只不过 IDE 工具在取中断点时会显示为 10 进位,以下使用 mysql 包里的 CalcPassword 函数中的 stage1 变量为例 299 300 ### 使用工具和网站把密码转换成验证码 301 302 使用 Linux 命令 去产生 stage1 的 sha1sum 验证码 303 304 <img src="./assets/image-20220314214316673.png" alt="image-20220314214316673" style="zoom:80%;" /> 305 306 使用 [网站](https://coding.tools/tw/sha1) 去计算 stage1 的 sha1sum 验证码 307 308 <img src="./assets/image-20220314215924425.png" alt="image-20220314215924425" style="zoom:80%;" /> 309 310 ### 使用中断点去观察 stage1 变量 311 312 使用中断点去取出相对应 stage1 的 sha1sum 验证码 313 314 <img src="./assets/image-20220314220921338.png" alt="image-20220314220921338" style="zoom:100%;" /> 315 316 ### 确认 Sha1sum 验证码的数值 317 318 使用下表去对照检查 "中断点" 和 "Linux 命令" 产生的 stage1 验证码,确认其值为正确的 319 320 | 数组位置 | 二进位 | 十进位 | 十六进位 | 321 | :------: | :------: | :----: | :------: | 322 | 0 | 10001100 | 140 | 8c | 323 | 1 | 10110010 | 178 | b2 | 324 | 2 | 00100011 | 35 | 23 | 325 | 3 | 01111101 | 125 | 7d | 326 | 4 | 00000110 | 6 | 06 | 327 | 5 | 01111001 | 121 | 79 | 328 | 6 | 11001010 | 202 | ca | 329 | 7 | 10001000 | 136 | 88 | 330 | 8 | 11011011 | 219 | db | 331 | 9 | 01100100 | 100 | 64 | 332 | 10 | 01100100 | 100 | 64 | 333 | 11 | 11101010 | 234 | ea | 334 | 12 | 11000110 | 198 | c6 | 335 | 13 | 00001101 | 13 | 0d | 336 | 14 | 10101001 | 169 | a9 | 337 | 15 | 01100011 | 99 | 63 | 338 | 16 | 01000101 | 69 | 45 | 339 | 17 | 01010001 | 81 | 51 | 340 | 18 | 00111001 | 57 | 39 | 341 | 19 | 01100100 | 100 | 64 |