Spring Security 推荐使用 BCryptPasswordEncoder 进行密码加密及验证,这个安全性高。C++写的系统与 Spring 的系统有密文有关的交互,于是需要 C++ 的系统能生成 Spring 可校验的密文。

C++ 最有名的安全库是 OpenSSL,但 OpenSSL 上没有找到与 Spring 相同的 BCrypt 加密算法。Spring 使用的是 $2a$ 版 BCrypt,加密迭代次数是 10。[1][2][3]

Google 搜索得知,Apache 的 htpasswd 可以做 BCrypt 加密,但它默认是 $2y$ 版本[4][5],而不是 $2a$ 版本。

分析 htpasswd 的代码,发现他是可以生成 $2a$ 的密文的。

htpasswd 的有关实现:

    case ALG_BCRYPT:
rv = apr_generate_random_bytes((unsigned char*)salt, 16);
if (rv != APR_SUCCESS) {
ctx->errstr = apr_psprintf(ctx->pool, "Unable to generate random "
"bytes: %pm", &rv);
ret = ERR_RANDOM;
break;
}
if (ctx->cost == 0)
ctx->cost = BCRYPT_DEFAULT_COST;
rv = apr_bcrypt_encode(pw, ctx->cost, (unsigned char*)salt, 16,
ctx->out, ctx->out_len);
if (rv != APR_SUCCESS) {
ctx->errstr = apr_psprintf(ctx->pool, "Unable to encode with "
"bcrypt: %pm", &rv);
ret = ERR_PWMISMATCH;
break;
}
break;

他调用了 apr 库的 apr_generate_random_bytes 和 apr_bcrypt_encode 两个函数。

apr_bcrypt_encode 的实现:

static const char * const bcrypt_id = "$2y$";
APR_DECLARE(apr_status_t) apr_bcrypt_encode(const char *pw,
unsigned int count,
const unsigned char *salt,
apr_size_t salt_len,
char *out, apr_size_t out_len)
{
char setting[40];
if (_crypt_gensalt_blowfish_rn(bcrypt_id, count, (const char *)salt,
salt_len, setting, sizeof(setting)) == NULL)
return APR_FROM_OS_ERROR(errno);
if (_crypt_blowfish_rn(pw, setting, out, out_len) == NULL)
return APR_FROM_OS_ERROR(errno);
return APR_SUCCESS;
}

可以看到 bcrypt_id 是写死 $2y$,我们要用 $2a$ 就必须重新实现一版自己的 apr_bcrypt_encode。

在看 _crypt_gensalt_blowfish_rn 的实现

char *_crypt_gensalt_blowfish_rn(const char *prefix, unsigned long count,
const char *input, int size, char *output, int output_size)
{
if (size < 16 || output_size < 7 + 22 + 1 ||
(count && (count < 4 || count > 17)) ||
prefix[0] != '$' || prefix[1] != '2' ||
(prefix[2] != 'a' && prefix[2] != 'y')) {
if (output_size > 0) output[0] = '\0';
__set_errno((output_size < 7 + 22 + 1) ? ERANGE : EINVAL);
return NULL;
}
if (!count) count = 5;
output[0] = '$';
output[1] = '2';
output[2] = prefix[2];
output[3] = '$';
output[4] = '0' + count / 10;
output[5] = '0' + count % 10;
output[6] = '$';
BF_encode(&output[7], (const BF_word *)input, 16);
output[7 + 22] = '\0';
return output;
}

可以看到它是支持 $2a$ 的,因此我们直接给他传 $2a$ 是没有问题的。

通过以上思路重新实现 apr_bcrypt_encode 后,经验证是可以在 Spring 中校验通过的。
C++ 测试生成代码:

#define BCRYPT_DEFAULT_COST 10
std::string crypt_password(const std::string& pwd)
{
unsigned char salt[16];
apr_status_t rv;
rv = apr_generate_random_bytes((unsigned char*)salt, 16);
char out[128] = {};
rv = apr_bcrypt_encode(pwd.c_str(), BCRYPT_DEFAULT_COST, salt, 16, out, 128);
return out;
}

Spring 测试校验代码:

		PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String pwd = "123456";
String encPwd1 = "$2a$10$2naiSihmgRb4wfhqBr03S.eL0UfykVzuvOSkWo4i06kK3N9PZxsuu";
String encPwd2 = passwordEncoder.encode(pwd);
boolean ret = passwordEncoder.matches(pwd, encPwd1);
boolean ret2 = passwordEncoder.matches(pwd, encPwd2);

  1. Java-Security(三):加密的用法、PasswordEncoder类源码分析
  2. bcrypt Wikipedia
  3. Can someone explain how BCrypt verifies a hash?
  4. I would like to compute the bcrypt hash of my password.
  5. htpasswd – Manage user files for basic authentication
  6. yugabyte / crypt_blowfish

发表评论

您的电子邮箱地址不会被公开。