Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/brpc/policy/mysql_auth_hash.cpp
Original file line number Diff line number Diff line change
@@ -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 <cstring>

#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<const unsigned char*>(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<char>(sha_pw[i] ^ salted_hash[i]);
}
return out;
}

} // namespace policy
} // namespace brpc
54 changes: 54 additions & 0 deletions src/brpc/policy/mysql_auth_hash.h
Original file line number Diff line number Diff line change
@@ -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 <string>

#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
117 changes: 117 additions & 0 deletions test/brpc_mysql_auth_hash_unittest.cpp
Original file line number Diff line number Diff line change
@@ -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 <gtest/gtest.h>

#include <string>

#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<char>(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<char>(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
Loading