C/C++实现生成BCrypt密文
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$ 的密文的。
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 两个函数。
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);