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一致,比较结果如果一致就认证通过,否则认证失败。

        为什么上述两个值比较,如果一致就能认证通过呢?那就要看这两个值分别是如何计算出来的,如果使用同样的输入经过同样的计算过程,那么虽然由不同终端计算的两个值肯定是相同的。详见文章开头的流程图。

        到此服务端就已经对客户端认证通过,但后面还有一步客户端的操作,未完待续...... 

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐