diff --git a/src/brpc/policy/mysql_auth_hash.cpp b/src/brpc/policy/mysql_auth_hash.cpp new file mode 100644 index 0000000000..11711ecfb3 --- /dev/null +++ b/src/brpc/policy/mysql_auth_hash.cpp @@ -0,0 +1,61 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "brpc/policy/mysql_auth_hash.h" + +#include + +#include "butil/sha1.h" + +namespace brpc { +namespace policy { + +std::string MysqlNativePasswordScramble(const butil::StringPiece& salt, + const butil::StringPiece& password) { + if (password.empty()) { + return std::string(); + } + if (salt.size() != kMysqlNativePasswordResponseLen) { + return std::string(); + } + + const size_t kHashLen = butil::kSHA1Length; + + unsigned char sha_pw[kHashLen]; + butil::SHA1HashBytes( + reinterpret_cast(password.data()), + password.size(), sha_pw); + + unsigned char sha_sha_pw[kHashLen]; + butil::SHA1HashBytes(sha_pw, kHashLen, sha_sha_pw); + + unsigned char joined[kHashLen * 2]; + memcpy(joined, salt.data(), kHashLen); + memcpy(joined + kHashLen, sha_sha_pw, kHashLen); + + unsigned char salted_hash[kHashLen]; + butil::SHA1HashBytes(joined, sizeof(joined), salted_hash); + + std::string out(kHashLen, '\0'); + for (size_t i = 0; i < kHashLen; ++i) { + out[i] = static_cast(sha_pw[i] ^ salted_hash[i]); + } + return out; +} + +} // namespace policy +} // namespace brpc diff --git a/src/brpc/policy/mysql_auth_hash.h b/src/brpc/policy/mysql_auth_hash.h new file mode 100644 index 0000000000..c385609406 --- /dev/null +++ b/src/brpc/policy/mysql_auth_hash.h @@ -0,0 +1,54 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// Clean-room implementation of the MySQL native_password ("mysql41") +// authentication scramble, written from MySQL's public protocol +// documentation and not derived from any GPL-licensed source. +// +// Algorithm: +// scramble = SHA1(password) XOR SHA1( salt || SHA1( SHA1(password) ) ) +// +// Reference (public MySQL docs): +// https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_authentication_methods_native_password_authentication.html + +#ifndef BRPC_POLICY_MYSQL_AUTH_HASH_H +#define BRPC_POLICY_MYSQL_AUTH_HASH_H + +#include + +#include "butil/strings/string_piece.h" + +namespace brpc { +namespace policy { + +// The mysql_native_password scramble output is always 20 bytes (one +// SHA-1 digest worth) when the password is non-empty. +static const size_t kMysqlNativePasswordResponseLen = 20; + +// Computes the mysql_native_password scramble for the given password +// and the 20-byte salt that the server sent in its handshake packet. +// +// Returns a 20-byte raw response on success. Returns an empty string +// when |password| is empty (per spec: the wire response for a blank +// password is zero bytes) or when |salt| has the wrong length. +std::string MysqlNativePasswordScramble(const butil::StringPiece& salt, + const butil::StringPiece& password); + +} // namespace policy +} // namespace brpc + +#endif // BRPC_POLICY_MYSQL_AUTH_HASH_H diff --git a/test/brpc_mysql_auth_hash_unittest.cpp b/test/brpc_mysql_auth_hash_unittest.cpp new file mode 100644 index 0000000000..8ba71f4147 --- /dev/null +++ b/test/brpc_mysql_auth_hash_unittest.cpp @@ -0,0 +1,117 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include + +#include + +#include "brpc/policy/mysql_auth_hash.h" +#include "butil/strings/string_piece.h" + +namespace { + +// Convert a hex string ("9f14...") to a raw bytes std::string. +std::string FromHex(const std::string& hex) { + std::string out; + out.resize(hex.size() / 2); + for (size_t i = 0; i < out.size(); ++i) { + char b[3] = {hex[2 * i], hex[2 * i + 1], '\0'}; + out[i] = static_cast(strtol(b, nullptr, 16)); + } + return out; +} + +TEST(MysqlAuthHashTest, KnownVector_PasswordPassword_AsciiSalt) { + // password = "password" + // salt = "0123456789ABCDEFGHIJ" (20 ASCII bytes) + // Expected scramble derived from the public-spec formula + // SHA1(p) XOR SHA1(salt || SHA1(SHA1(p))). + const std::string salt = "0123456789ABCDEFGHIJ"; + const std::string password = "password"; + const std::string expected = + FromHex("9f14d8530c26444b47bf2ff8860de84dbfd85c88"); + + const std::string actual = brpc::policy::MysqlNativePasswordScramble( + butil::StringPiece(salt), butil::StringPiece(password)); + ASSERT_EQ(expected.size(), + brpc::policy::kMysqlNativePasswordResponseLen); + ASSERT_EQ(expected, actual); +} + +TEST(MysqlAuthHashTest, KnownVector_PasswordSecret_BinarySalt) { + // password = "secret" + // salt = bytes 0x01..0x14 (20 binary bytes, exercises non-ASCII salt) + std::string salt; + salt.reserve(20); + for (int i = 1; i <= 20; ++i) { + salt.push_back(static_cast(i)); + } + const std::string password = "secret"; + const std::string expected = + FromHex("b32bb3a583e1340c0a1108d58b1be49781ad8c2f"); + + const std::string actual = brpc::policy::MysqlNativePasswordScramble( + butil::StringPiece(salt), butil::StringPiece(password)); + ASSERT_EQ(expected, actual); +} + +TEST(MysqlAuthHashTest, EmptyPasswordReturnsEmptyString) { + // Per MySQL spec, a blank password produces a zero-byte wire response, + // never a 20-byte zero block. + const std::string salt(20, 'A'); + const std::string actual = brpc::policy::MysqlNativePasswordScramble( + butil::StringPiece(salt), butil::StringPiece("")); + EXPECT_TRUE(actual.empty()); +} + +TEST(MysqlAuthHashTest, BadSaltLengthReturnsEmptyString) { + // The handshake salt is always 20 bytes. Anything else is a protocol + // error; we surface that as an empty result so callers fail closed. + const std::string short_salt(19, 'A'); + const std::string long_salt(21, 'A'); + + EXPECT_TRUE(brpc::policy::MysqlNativePasswordScramble( + butil::StringPiece(short_salt), butil::StringPiece("pw")).empty()); + EXPECT_TRUE(brpc::policy::MysqlNativePasswordScramble( + butil::StringPiece(long_salt), butil::StringPiece("pw")).empty()); +} + +TEST(MysqlAuthHashTest, DeterministicAcrossCalls) { + const std::string salt(20, '\x42'); + const std::string password = "hunter2"; + + const std::string a = brpc::policy::MysqlNativePasswordScramble( + butil::StringPiece(salt), butil::StringPiece(password)); + const std::string b = brpc::policy::MysqlNativePasswordScramble( + butil::StringPiece(salt), butil::StringPiece(password)); + EXPECT_EQ(a, b); + EXPECT_EQ(a.size(), brpc::policy::kMysqlNativePasswordResponseLen); +} + +TEST(MysqlAuthHashTest, DifferentSaltsProduceDifferentOutputs) { + const std::string salt1(20, '\x01'); + const std::string salt2(20, '\x02'); + const std::string password = "hunter2"; + + const std::string a = brpc::policy::MysqlNativePasswordScramble( + butil::StringPiece(salt1), butil::StringPiece(password)); + const std::string b = brpc::policy::MysqlNativePasswordScramble( + butil::StringPiece(salt2), butil::StringPiece(password)); + EXPECT_NE(a, b); +} + +} // namespace