PostgreSQL学习之SCRAM-SHA-256加密认证
PostgreSQL数据库保存用户密码的方式为加密保存(准确的说是保存用户的密码与随机数的hash值),加密算法为MD5和SCRAM-SHA-256两种,保存位置为系统表pg_authid。
PostgreSQL数据库保存用户密码的方式为加密保存(准确的说是保存用户的密码与随机数的hash值),加密算法为MD5和SCRAM-SHA-256两种,保存位置为系统表pg_authid。
SCRAM-SHA-256加密及认证流程图:
SCRAM-SHA-256加密
SCRAM-SHA-256$4096:kgMvWNAau3NfgfcUIY89CA==$KQXDM5+0dCYCYFIjP4VcIXvrD5sqneAPImEjnR42+IY=:yt950PbOeGkvBe75WN706nGAdDOhU/2jbzxf+iRjLFU=
密文格式解析:
SCRAM-SHA-256:加密算法名称
4096:循环轮数,加密过程中算法执行次数
kgMvWNAau3NfgfcUIY89CA==:随机数
KQXDM5+0dCYCYFIjP4VcIXvrD5sqneAPImEjnR42+IY=:stored_key
yt950PbOeGkvBe75WN706nGAdDOhU/2jbzxf+iRjLFU=:server_key
加密过程解析:
实现scram-sha-256加密的函数主要是pg_be_scram_build_secret,首先获取一个随机数,该随机数会随同密码密文一同保存(如上),然后进入加密函数scram_build_secret,这里最主要是下面四步:
/* Calculate StoredKey and ServerKey */
if (scram_SaltedPassword(password, salt, saltlen, iterations,
salted_password) < 0 ||
scram_ClientKey(salted_password, stored_key) < 0 ||
scram_H(stored_key, SCRAM_KEY_LEN, stored_key) < 0 ||
scram_ServerKey(salted_password, server_key) < 0)
{
#ifdef FRONTEND
return NULL;
#else
elog(ERROR, "could not calculate stored key and server key");
#endif
}
第一步用password做密码加密随机数,然后再执行4096轮次的hmac计算生成salted_password;
第二步用第一步生成的salted_password加密字符串“Client Key”;
第三步对第二步生成结果进行hash计算,生成stored_key;
第四步用第一步生成的salted_password对字符串"Server Key"进行加密生成server_key。
SCRAM-SHA-256认证
PostgreSQL数据库实现scram-sha-256认证是基于SASL协议的,SASL协议提供一个认证框架,在此框架下可嵌入各种认证实现。scram-sha-256认证交互流程如下:
1、服务端检测到此会话需要scram-sha-256认证,则会发送PG认证请求,请求的认证类型为SASL(10),该消息仅包括字符串"SCRAM-SHA-256";
2、客户端响应PG消息SASLInitialResponse message,其包括SASL认证机制:字符串"SCRAM-SHA-256"和认证数据(SASL authentication data),认证数据主要包括一个即时生成的随机数,即client_nonce;
3、服务端继续发送PG认证请求,认证类型为SASL continue (11),其包含的SASL认证数据如下(抓包):
r=z6tIfBaGWR1WYKPSDrqeWzLe8zdb/ceKb4HbkGt8t4Kwze74,s=rrWb/0K3P1DyS08PYevJYQ==,i=4096
其中r=包括客户端随机数(client_nonce)和服务端随机数(server_nonce);
s=为存储的被scram-sha-256加密的密码密文中的随机数(salt);
i=为存储的被scram-sha-256加密的密码密文中的轮数;
4、客户端继续响应SASL响应消息,其响应SASL数据如下(抓包):
c=biws,r=z6tIfBaGWR1WYKPSDrqeWzLe8zdb/ceKb4HbkGt8t4Kwze74,p=igHrqaPMTMR9pHbUlj328W00DD+fIq+1g3qGr9X/ZQ0=
r=还是随机数
p=是client_proof,其计算过程如下:
if (scram_SaltedPassword(state->password, state->salt, state->saltlen,
state->iterations, state->SaltedPassword) < 0 ||
scram_ClientKey(state->SaltedPassword, ClientKey) < 0 ||
scram_H(ClientKey, SCRAM_KEY_LEN, StoredKey) < 0 ||
pg_hmac_init(ctx, StoredKey, SCRAM_KEY_LEN) < 0 ||
pg_hmac_update(ctx,
(uint8 *) state->client_first_message_bare,
strlen(state->client_first_message_bare)) < 0 ||
pg_hmac_update(ctx, (uint8 *) ",", 1) < 0 ||
pg_hmac_update(ctx,
(uint8 *) state->server_first_message,
strlen(state->server_first_message)) < 0 ||
pg_hmac_update(ctx, (uint8 *) ",", 1) < 0 ||
pg_hmac_update(ctx,
(uint8 *) client_final_message_without_proof,
strlen(client_final_message_without_proof)) < 0 ||
pg_hmac_final(ctx, ClientSignature, sizeof(ClientSignature)) < 0)
{
pg_hmac_free(ctx);
return false;
}
for (i = 0; i < SCRAM_KEY_LEN; i++)
result[i] = ClientKey[i] ^ ClientSignature[i];
前三步与存储加密部分的前三步完全相同(参考文章前面的加密部分),最终生成StoredKey,然后使用StoredKey做密钥加密本次认证前几次交互产生的数据和随机数,最终生成ClientSignature,最后用ClientSignature对第二步生成的ClientKey进行异或加密生成client_proof。(服务端认证的时候使用本次认证交互数据生成同样的ClientSignature,然后再对客户端的client_proof进行异或解密,解密获得ClientKey再对其进行hash计算,得出的值就是服务端本地存储的密码密文中的stored_key,这样检查此处客户端计算的stored_key与本地存储的stored_key一致则认证通过)
5、服务端校验流程:
if (pg_hmac_init(ctx, state->StoredKey, SCRAM_KEY_LEN) < 0 ||
pg_hmac_update(ctx,
(uint8 *) state->client_first_message_bare,
strlen(state->client_first_message_bare)) < 0 ||
pg_hmac_update(ctx, (uint8 *) ",", 1) < 0 ||
pg_hmac_update(ctx,
(uint8 *) state->server_first_message,
strlen(state->server_first_message)) < 0 ||
pg_hmac_update(ctx, (uint8 *) ",", 1) < 0 ||
pg_hmac_update(ctx,
(uint8 *) state->client_final_message_without_proof,
strlen(state->client_final_message_without_proof)) < 0 ||
pg_hmac_final(ctx, ClientSignature, sizeof(ClientSignature)) < 0)
{
elog(ERROR, "could not calculate client signature");
}
pg_hmac_free(ctx);
/* Extract the ClientKey that the client calculated from the proof */
for (i = 0; i < SCRAM_KEY_LEN; i++)
ClientKey[i] = state->ClientProof[i] ^ ClientSignature[i];
/* Hash it one more time, and compare with StoredKey */
if (scram_H(ClientKey, SCRAM_KEY_LEN, client_StoredKey) < 0)
elog(ERROR, "could not hash stored key");
if (memcmp(client_StoredKey, state->StoredKey, SCRAM_KEY_LEN) != 0)
return false;
第一步使用本地存储的stored_key对本次交互数据和随机数进行加密生成ClientSignature(客户端直接使用密码明文生成的ClientSignature与此处服务端使用加密后的stored_key生成的ClientSignature是一样的);
第二步使用上一步生成的ClientSignature对客户端发送过来的client_proof进行异或解密即可得到client_StoredKey,此值应该与本地存储的密文中的stored_key一致,比较结果如果一致就认证通过,否则认证失败。
为什么上述两个值比较,如果一致就能认证通过呢?那就要看这两个值分别是如何计算出来的,如果使用同样的输入经过同样的计算过程,那么虽然由不同终端计算的两个值肯定是相同的。详见文章开头的流程图。
到此服务端就已经对客户端认证通过,但后面还有一步客户端的操作,未完待续......
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)