登陆服务 | 二、设计

2021 10 27, Wed

之前写到写登陆服务有一些关键的需求,中间有很多密码学上的概念,也有很多安全上的考量。在写以前先整理清楚会比较好。

下面说的攻击通常指的是针对服务器、数据库的攻击。

登陆中的密码

密码是登陆中的核心,是我证明我是我的手段之一。但是很多时候密码会有很多坑爹的问题。

密码的存储:加密?不,哈希

很早以前密码都是明文存储在数据库里面,这当然是非常差的手段,一个站点被突破,用户的所有站点都可能有威胁。后来大部分站点就不再存储明文密码,但是现在有的博客里面会讲要把密码加密储存,在登陆验证时解密比较,这也是一个比较大的误区。

在解决这个误区前我们需要先理解加密是什么,密码学上除了加解密还有哪些验证的手段,为什么加密解密这个方法不是特别好。

编码 vs 加密解密 vs 签名验签 vs 哈希

密码学通常有两类函数:加密和哈希。从哈希延伸出来还有签名和加盐的哈希。除此之外还有我们常见的编码,有时候也会有人称之为是加密。

无论如何这些方法都是一些函数,函数意即映射,这是高中大家都学过的。

可逆过程

加密和编码都是可逆过程,唯一的区别在于参数数量。

加结密的意思是,有一对方法以及一组密钥,方法可以利用密钥在两个集合间建立一一对应的映射关系,被保护的集合中元素被称为明文,另一方是密文,明文向密文的映射叫做加密,密文向明文的映射叫解密。加解密必须知道解密的key是什么才能从密文映射回原文,从而形成对一个集合内元素的保护。加密的key和解密的key完全一样的,我们称之为对称加密,解密密钥不一样的我们称之为是非对称加密。

而编码指的是有一对方法,可以在两个集合的元素中建立一一对应的关系,一般从短的、不利于存储传输、不利于阅读的集合向长的、易于存储、易于阅读记忆的集合映射的关系叫编码或序列化,反之则称为解码、反序列化。编解码只需要元素在函数定义的集合范围内,就可以获得对应的映射关系。

不可逆过程

和可逆过程相对,密码学也有不可逆的映射过程,也就是哈希,意为从一个大集合到另一个小集合的单向映射,比如说无限集到有限集的映射。能实际应用的哈希一般都是向易于传输的值的映射,映射的结果称为 指纹 ( Fingerprint ) 摘要 ( Digest ) 、哈希值……或者干脆就叫哈希,同一个哈希函数计算同一个值的摘要总是不变的,但是同一个哈希函数对两个不一样的值可能产生同样的哈希结果,这种情况称为碰撞。

其中有一种情况叫密码哈希函数,特指满足了一定条件的哈希函数:

  • 对任意数据可以轻松算出摘要
  • 难以用摘要还原数据
  • 难以找到两段摘要一样的数据
  • 数据的任意变化会导致摘要明显变化,最好和老摘要毫不相关

下面说的哈希大体上都是说密码哈希函数。

由于哈希的这些性质,我们可以方便的用哈希函数和摘要验证一块非常大的数据有没有被修改过,只要和数据一起或者从另一个信道传送摘要,然后在另一侧用同一个哈希函数重新计算并比较哈希就好了。

由哈希延伸,还有数字签名和盐的概念。

数字签名近似于是一个哈希函数,但是多出来了一个固定的输入,和加解密一样叫密钥,并且可以用配套的密钥验证签名的有效性,即消息是否完整,及是否是同一套密钥生成的签名。非对称密钥的话私钥签名公钥验签,公钥可以公开,对称密钥就需要提前把密钥交换好,私钥和对称的密钥都只能给可信的人保管。数字签名用来验证消息的完整性和来源的可靠性,可以有效阻止攻击者通过公开的信息假传圣旨。

加盐也是额外的增加一个公开的输入,这个输入称之为盐,是一个随机的输入,盐可以以随意的形式混杂进明文中,然后经过哈希函数得出一个指纹。加盐的作用主要是防止攻击者利用彩虹表推算哈希函数的原文。

唯一解:KDF

编码的问题

各种编码都有自己的特点,而且即使把常用编码都尝试一遍也不会太费事情。

加解密的问题

加解密的方法最大的问题在于密钥如何存储。假设攻击者已经可以访问到数据库,那么攻击者八成是可以得到加解密的密钥的,假如我们要再次加密密钥,就会陷入无限套娃的问题。

直接哈希的问题

只是哈希一遍密码会遇到彩虹表的问题,也就是攻击者可以把常用密码,甚至一定长度内的字符串序列全部哈希一遍,得到一张摘要到密码的映射关系,也就是彩虹表,攻击者只要在彩虹表中查询摘要,就可以得到哈希的原文。

签名不适合

签名的问题在于密钥相对攻击者是透明的,被攻破的情况下攻击者可以用私钥再打一张新的彩虹表。

KDF:bcrypt、argon2、……

KDF是唯一适合存储密钥的方法,也可以看成是稍微复杂一点的哈希函数。其中包含一类方法是通过加盐和增加哈希时间这两种手段,来提高发现哈希碰撞的成本。对每个密码加不同的随机的盐提高了打表的难度,增加哈希时间提高了打单个表的时间成本。

密码的传输和对比

前两天刚刚听同事说要把密码编码以后传给服务器防止别人窥探,同样的版本还有把密码哈希以后给服务器和把密码加密以后给服务器。

不正确的方法

编码以后传输

读完上面可以理解编码解码并不能防止偷窥,也不能阻止篡改。

加密以后传输

加密的理念是好的,从过程来看似乎一般也没问题。但是,しかし!密钥怎么保存,无论放在哪里,客户端环境通常都可以认为是不可信的,尤其是前端,所以你无法在登陆的客户端上安全的保存密钥并且保护它不会被修改。给密钥再次加密就类似于上面无限套娃的问题。

哈希以后传输

哈希的问题从几方面来考虑。

假设开发者希望服务器直接存储比较哈希值,那么攻击者在攻破服务器后取得哈希以后,就可以直接作为密码使用。

假设开发者希望服务器可以得到原文,那么参考哈希的定义,是几乎不可能做到的。

假设开发者希望服务器再次哈希以后存储到数据库,那么会增大哈希碰撞的概率也不可取。

唯一解

唯一一种正确的方法是通过可信信道传送明文,通过KDF函数哈希过后存储到数据库,登陆时再次通过KDF哈希后比较新老摘要。

密码策略

复杂度

密码不应该太过于简单,这是一个常识。通常我们会要求密码有一定的长度,比如说最少六位密码,或者最少八位密码。同时可以为密码设置一定的规则,比如说在小写、大写、符号、数字中包含三类字符。

除此之外现在还有弱密码数据库,如果弱密码出现在数据库中,也可以多提醒用户一下。

定期修改

有的站点,比如说微软曾经有段时间,强制用户定期修改密码。这曾经被认为是增加安全性的措施,但是实际上因为用户在这种情况下会选择更简单的密码,导致安全性总体上下滑的非常厉害。所以不建议强制定期修改密码。

登陆过程

模糊化错误内容

通常情况下,如果一个过程失败了,我们会告诉用户出现了什么具体的问题,然后让用户稍后重试。但是登陆不应该这么做。登陆过程中,任何用户输入导致的失败都应该提示同样的错误提示:“用户名或密码错误”。

重试

登陆过程不应该能无限重试,需要加一个次数的限制器,最好有验证码。

重设密码、修改信息

无法登陆时重设密码

无法登陆时重设密码一般也叫找回账户阿什么的。通常的形式是用户输入一个邮箱,发送邮件后用户通过邮件中的链接重设密码。

通过重设密码的部分一般没有问题。问题在于输入邮箱并提交后,无论邮箱是否存在,都应该提示用户静候邮件,以防止有人穷举用户邮箱。

登陆后修改包括密码在内的信息

有的开发者会假设用户登陆后,就应该处于一个被信任的状态,可以任意修改自己的所有信息。

但是要考虑用户登陆以后有可能离开机器,所以这个假设不能成立。

最好的办法就是修改什么信息,就需要先认证这个信息或者密码。比如说修改密码的同时需要提供旧密码,修改邮箱的同时需要先从邮箱获取到验证码。