用CVI实现bcrypt加密算法
欲看图文版,请用PC浏览器下载pdf附件。索要CVI2010工程源码,请电邮14518918@qq.com。
得闲刷知乎,有一个提问说MD5不可靠,没加盐,而且算法耗时极短,很容易被暴力破解。豆包推荐用bcrypt算法,说bcrypt 是目前最通用、最稳妥、最不容易用错的密码哈希算法,也是绝大多数网站、APP 的默认选择。只比MD5稍复杂一点,而且每次加密前都会先随机生成一带盐(从生成的bcrypt密文,可以反向解析出原盐,故盐无需数据库额外开字段来存储)。而且bcrypt可以通过增大cost(成本因子,即迭代次数)值,人为拉长运算时间,可有效延缓暴力破解的效率,更安全。
我瞟了眼豆包给的bcrypt的函数及其用法,竟然和之前调用MD5的函数差不多,遂准备在豆包协助下开工。结果尝试了一整天,用豆包、千问、TRAE轮番尝试,到晚上吃了18元三两的韭菜肉饺,又体验了一把39元五次的洗剪吹,这才回家搜得S盒1024个32位原始数据,然后才实现了bcrypt加密算法不报错。如下是在CVI2010的debug模式下运行结果,cost=8时耗时1.7秒,cost=10时耗时6.0秒。release模式就开挂了,分别是0.1秒和0.3秒,可见确实是cost越大,耗时也越长,但安全性也越高。
我在CVI2010遇到的第一个问题是,CVI2010在编译S盒常量数组时报错。S盒常量数组包含 4 个 32 位的 S 盒(S1、S2、S3、S4),每个 S 盒有 256 个条目(即每个 S 盒是256×32bit的数组),总计4×256=1024个 32 位数值。
注意,bcrypt 是基于Blowfish算法的哈希函数,这里面的S盒是实现数据混淆、保证哈希不可逆的关键结构。豆包说,你可以把 S盒理解成一个“密码本”:它是一个预先定义(或动态生成)的固定长度数组,输入一个数值(索引,即密码明文),就能通过查表得到另一个完全不同的数值(输出,即密码密文)。这个 “查表替换” 的过程是非线性的(输入和输出无简单数学规律),也是 bcrypt 能抵抗暴力破解、差分攻击的核心原因。
最开始豆包给我的S盒常数数组如下,一编译就在第二个256的某个数据上反显蓝色背景并报错Too many initializers。
千问把二维数组S[4][256]改成一一维数组S[4256]也无济于事,反而是我改大容量到[5256]才不报错的。但即使是改成4个[256]的数组,也编译报错Too many initializers。然后又去问TRAE给一个算法代码,它给出来的数组,就从每行8个数,改成了每行4个数,拷贝到CVI2010,居然编译就过了。真是个人才,它咋晓得CVI编译器对常数数组的一行写几个数有规定的?
但第二个问题接踵而至,三个AI都不能给出正确的S盒常数数组一共1024个32位初值。它们都说其他AI的数值是错的,有幻觉给出重复循环数据,或者根本不能写完1024个数据就异常截断。问千问,为什么请AI输出1024个早就约定好的数据,对AI而言这么困难?千问答:这是一个非常深刻且直击大语言模型(LLM)本质的问题。简单来说,AI 并不是“算”不出这 1024 个数,而是被“输出长度限制”和“生成机制”给卡住了。首先是AI的硬件限制,1024 个32位数,加上标点符号和换行,大约需要 8000 ~ 10000 个 Token。而AI单次回复是有Token上限的,最大输出长度限制通常是 4096 或 8192 Tokens。所以当AI写到S盒的第 300 个数左右时,就已经达到了系统的“字数红线”,系统会强制截断输出。然后有些大模型在检测到即将被截断时,会试图“智能补全”,结果就是胡乱复制粘贴前面的片段,试图让代码看起来像是结束了,但这完全是错误的。其次,AI的生成机制,是“接龙”,而不是“查表”,是基于前面的所有字符预测下一个字符是什么,是一种概率性创作。所以对于我这种要求100%精确、0%创造性的任务,AI的机制天生就不适合。
好吧,那就只能换个问法,问哪里能直接下载到bcrypt的1024个Blowfish 标准常量?千问和TRAE都喜欢给github网址。草,咱没有梯子,一直报404找不到呢。还好,豆包给了要一个IETF 官方草案链接:https://www.ietf.org/proceedings/46/I-D/draft-schneier-blowfish-00.txt,说这是 Bruce Schneier 本人参与起草的 Blowfish 标准文档,附录 A 直接给出完整的 P 盒与 S 盒常量,是最权威的来源。
确实给了4个盒子,而且每行4个32位数,请注意每个16进制数后面都接了一个后缀“L”,会给CVI2010编译惹祸,后面需要删掉:
第三个问题是,AI给出的源代码,好多暗伤。我只能每次编译或运行报错,都把.h/.c文件和报错截图喂给豆包,反复修改了近10次,才最终加密验证成功。
第四个问题是,Win11的安全中心闲得慌,因为无法确认是谁发布的bcrypt_test.exe,就会阻碍我们exe的运行,故尝试在CVI中debug运行代码时,会弹窗Could not run executable,然后就被卡住了。
不虚,去windows安全中心添加文件夹监管排除项,把CVI的这个工程目录添加进去,让win11不要多管闲事即可:
整个CVI控制台程序就三个文件,bcrypt.c是实现核心函数的C文件,bcrypt.h是声明核心函数的头文件,bcrypt_test.c是调用核心函数的测试用例。
如下展示了一条完整bcrypt哈希及其格式,固定由 4 个部分组成,用$分隔,中间没有空格一共60个字节。显而易见地,bcrypt哈希中包含了成本因子和盐值:
$ 2a $ 08 $ RLGfU4zyx.E2p/TIgSTlP $ Ol5NkIx0a8PiOzjNAD1CvELX.J9KnS
$ 版本 $ 成本因子 $ 盐值 (16字节) $ 哈希结果 (23字节)
bcrypt.c的关键函数就两个:
分别是根据成本因子生成盐值,和生成哈希。注意前面提到过,bcrypt哈希值,已经包含了成本因子和盐值,所以数据库保存的bcrypt哈希值,可以作为盐值,直接参与到生成检验用bcrypt哈希值的过程中去。
/**
* @brief 生成随机盐值字符串 (格式: $2a$XX$...)
* @param salt 输出缓冲区 (至少 BCRYPT_MAXSALT 字节)
* @param cost 工作因子 (4-31)
* @return 0 成功,-1 失败
*/
int bcrypt_gensalt(char *salt, int cost);
/**
* @brief 计算 bcrypt 哈希值
* @param pass 密码字符串
* @param salt 盐值字符串 (由 bcrypt_gensalt 生成)
* @param hash 输出缓冲区 (至少 BCRYPT_HASHSIZE 字节)
* @return 0 成功,-1 失败
*/
int bcrypt(const char *pass, const char *salt, char *hash);
bcrypt.c的大概用法就两条:
在用户注册时,用户会被要求输入账号和密码,程序将先调用bcrypt_gensalt(salt, cost)生成盐值字符串salt[],其中的迭代次数cost可以是4/8/9/10/12等整数最大值31,然后将用户输入的密码password[]和盐值salt[]送入bcrypt(password, salt, hash)函数,生成哈希值字符串hash[],并作为密码密文和账号一并存入数据库。
用户登录时,用户会被要求输入账号和密码,程序将根据用户输入的账号从数据库读出密码密文hash[],该用hash[]全文或截取前29个字节作为盐值字符串salt[],连同用户输入的密码password[]一起,送入bcrypt(password, salt, computed_hash)函数,生成新的哈希值字符串computed_hash[]。只有当computed_hash[]=hash[],程序才能认为用户登录时输入的密码password[],就是用户注册时输入的那个密码。这就是 bcrypt 验证密码的核心原理。只要你用同一个密码 + 数据库里已有的完整 bcrypt 哈希去重新计算,新生成的哈希和旧哈希就是100%完全相同的。
不敢独享,源码在此:
第五个问题是,我原先数据库存的是MD5加密算法的密码密文,现在要换成bcrypt加密算法。如果不想打扰用户重新走一遍账号密码的注册流程,是否可以从数据库读出原MD5编码,作为密码明文,再生成bcrypt哈希值作为密码密文存回数据库?这样依赖,用户可以在界面输入原密码,后台先形成MD5编码再生成bcrypt编码最终与数据库保存的密文做对比,依然可以顺利登录?
豆包说,完全可以,而且这是业内最标准、最平滑、完全不影响用户体验的升级方案。而且由于原来的MD5是可暴力破解、可彩虹表破解的,而现在的bcrypt是慢哈希、抗暴力、自适应算力,即使攻击者拿到库,也无法还原密码。
所以为了实现MD5密文在数据库中平滑升级到bcrypt密文,我现在需要做两件事:
1,从数据库读出原MD5编码,作为密码明文,再生成bcrypt哈希值作为密码密文存回数据库
int CVICALLBACK on_UpdateMD5tobcrypt (int panel, int control, int event,
void *callbackData, int eventData1, int eventData2)
{
char sql[4096], buf[256], str[256];
int i, cost;
char salt[BCRYPT_MAXSALT], hash[BCRYPT_HASHSIZE];
char md5_user_password[64] = {0};
switch (event)
{
case EVENT_COMMIT:
//构造SQL指令
sprintf(sql, "SELECT UserPassword FROM mySFPPlusZR.user;");
//执行SQL指令
dataset = DP_GetDataSet(mysql, gbk, sql, LogErr);
if ((NULL == dataset) || (0 == dataset->cur_row))//一行数据库记录都没有获取到
{
//释放指针
if (NULL !=dataset)
{ DP_ArrayFree(dataset); dataset = NULL; }
MessagePopup("error", "数据库select失败");
SetWaitCursor(0); return -2;//执行数据库select失败
}
else //打印
{
//循环更新数据库的密文,从原MD5编码,换成MD5编码的bcrypt编码
for (i=0; i<dataset->cur_row; i++)
{
//1. 生成盐
cost = 8;
if (bcrypt_gensalt(salt, cost) != 0)
MessagePopup("error", "bcrypt为自己带盐失败");
//2. 生成哈希
if (bcrypt(dataset->Tables[i][0], salt, hash) != 0)
MessagePopup("error", "bcrypt生成哈希失败");
//3. update数据库,把UserPassword原MD5编码,换成UserPassword的MD5编码的bcrypt编码
DP_OpenTransaction(mysql); //开启事务
sprintf(sql, "UPDATE mySFPPlusZR.user SET UserPassword = '%s' "
"WHERE UserPassword = '%s' ", hash, dataset->Tables[i][0]); //构造sql指令
if (0 > DP_GetExcute(mysql, gbk, sql, LogErr))//执行sql指令
{
DP_Rollback(mysql); //撤销事务,指令回滚
if (NULL !=dataset)
{ DP_ArrayFree(dataset); dataset = NULL; } //释放指针
SetWaitCursor (0); return -3;
}
else
{
DP_Commit(mysql); //提交事务,指令确认
}
}
//释放指针
if (NULL !=dataset)
{ DP_ArrayFree(dataset); dataset = NULL; }
}
break;
}
return 0;
}
2,用户登录时,后台先接收密码明文,先形成MD5编码再生成bcrypt编码,最终与数据库保存的密文做对比
int CVICALLBACK on_login(int panel, int control, int event,
void *callbackData, int eventData1, int eventData2)
{
int error, i;
char sql[20000] = {0};
char buf[20000] = {0};
char *prepStmt = NULL;
char FormatSql[1000] = {0};
char user_password[256] = {0};
unsigned char md5_user_password[64] = {0};
char str[256]={0}, EVBSN[17], InnerPN[17];
char salt[BCRYPT_MAXSALT], hash[BCRYPT_HASHSIZE];
switch (event)
{
case EVENT_RIGHT_DOUBLE_CLICK:
SetCtrlVal(panelHandle_login, PANELLOGIN_STR_UserName, "Ellen");
SetCtrlVal(panelHandle_login, PANELLOGIN_STR_UserPassward, "123456");
case EVENT_COMMIT:
SetWaitCursor(1);
GetCtrlVal(panelHandle_login, PANELLOGIN_STR_UserName, user_name);
GetCtrlVal(panelHandle_login, PANELLOGIN_STR_UserPassward, user_password);
//构造SQL指令
sprintf(buf, "SELECT UserName, UserPassword FROM mySFPPlusZR.user \
WHERE UserName = \'%s\'", user_name);
//执行SQL指令
dataset = DP_GetDataSet(mysql, gbk, buf, LogErr);
if ((NULL == dataset) || (0 == dataset->cur_row))//一行数据库记录都没有获取到
{
//释放指针
if (NULL !=dataset)
{ DP_ArrayFree(dataset); dataset = NULL; }
MessagePopup("error", "用户名或密码错误");
SetWaitCursor(0); return -2;//执行数据库select失败
}
else //打印
{
sprintf (buf, "%s, ", dataset->Tables[0][0]);
strcat(str, buf);
// 生成MD5编码
get_md5(user_password, md5_user_password);
// 用MD5编码作为密码,数据库bcrypt密文作为盐,生成哈希
if (bcrypt(md5_user_password, dataset->Tables[0][1], hash) != 0)
MessagePopup("error", "bcrypt生成哈希失败");
//判断账号和bcrypt密文是否与数据库匹配
if (!strcmp(user_name, dataset->Tables[0][0]) &&
!strcmp(hash, dataset->Tables[0][1]))
{
//释放指针
if (NULL !=dataset)
{ DP_ArrayFree(dataset); dataset = NULL; }
}
else
{
//SetCtrlVal(PANEL, PANEL_STR_Operator, " ");
MessagePopup("error", "用户名或密码错误");
//释放指针
if (NULL !=dataset)
{ DP_ArrayFree(dataset); dataset = NULL; }
SetWaitCursor(0);
return -3; // data not found in database!
}
}
break;
}
return 0;
}
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)