当前位置: 首页 > 科技观察

如何正确进行密码验证?_1

时间:2023-03-16 12:54:18 科技观察

翻译|男爵评论|孙淑娟梁策网络安全问题日趋严重。即使是大型知名企业也面临敏感用户数据泄露的问题。这些问题可能包括未经授权访问数据库和泄露日志等。此外,我们还经常遇到零日漏洞(Zero-DayVulnerabilities),这些都对用户自身的安全和企业声誉造成负面影响。本文将介绍如何使用密码认证实现用户认证的数据存储。1.身份验证身份验证是用户确认他是所提供标识符的所有者的过程。最明显和熟悉的身份验证过程是密码身份验证。用户进入登录页面,输入用户名和密码,然后登录。以下部分介绍如何在服务器上实现身份验证。认证过程可以用一个图来表示:服务器收到请求后,服务器会根据数据库中存储的值(注册时保存的)来校验用户的数据,判断用户是否可以通过认证。如果检查成功,通常会在服务器上创建一个会话,并将其标识符作为cookie在响应中返回。那么,用户注册时如何保存认证数据呢?1.以明文形式存储密码在这种情况下,数据库中的数据将以开放数据的形式存储。任何有权访问数据库的人都可以获得用户密码,例如数据库管理员、支持人员或开发人员。此外,系统中始终存在漏洞风险,可能允许入侵者访问数据库并执行下载和转储。理想情况下,每个服务都应该有自己唯一的密码,这样就没有在服务中泄露身份验证数据的风险。但是我们使用的服务如此之多,不可能记住所有的密码。一种解决方案是密码管理器,但很少有人使用它们,用户更喜欢一个或多个可以随处使用的密码。当一项服务的数据遭到破坏时,使用该密码的其他服务也会受到影响,因此强烈建议不要以纯文本形式保存密码,以保护用户免受此类问题的影响。2.密码散列散列算法是一种根据用户密码计算数字摘要的特定函数。该函数的工作原理是足够快地从密码中获取哈希值,以至于无法在足够的时间内完成反向转换。哈希函数包括MD5、SHA-1、SHA-256、SH3-512等。使用这些函数,我们保存到数据库中的不是密码本身,而是使用哈希函数从密码中计算出的数字摘要值。例如,在Java中,使用如下操作获取密码的哈希值:Stringpassword="pa$$word";MessageDigestmd=MessageDigest.getInstance("SHA-256");byte[]digest=md.digest(password.getBytes());Stringhash=newBigInteger(1,digest).toString(16);作为转换的结果,我们将得到以下值,可以将其保存在数据库中:6a158d9847a80e99511b2a7866233e404b305fdb7c953a30deb65300a57a0655这个变体已经好很多了,但它仍然有缺点。其中之一是具有相同密码的用户将具有相同的哈希值。如果入侵者获得了对数据库的访问权,他就可以将数据用于自己的目的,而暴力破解密码的可能性是相当危险的。您可以使用流行的密码和哈希创建一个数据库(或使用现有数据库),这样您就可以快速恢复用户密码的值。这就是不推荐此选项的原因。3.使用唯一盐(Salt)的密码散列针对之前解决方案的痛点,我们可以为每个用户使用唯一的盐。salt是与密码连接的随机值,从结果中派生出哈希函数。字符串密码="pa$$word";字符串盐="b0f57dccf7133f7ef3acb09641e5f7a3";MessageDigestmd=MessageDigest.getInstance("SHA-256");byte[]digest=md.digest((password+salt).getBytes());Stringhash=newBigInteger(1,digest).toString(16)可以这样生成随机盐:Randomrandom=newSecureRandom();byte[]saltBytes=newbyte[16];random.nextBytes(saltBytes);字符串盐=newBigInteger(1,saltBytes).toString(16);这样,我们一次解决了几个问题。首先,具有相同密码的用户将具有不同的盐值,因此哈希函数的值也不同。由于无法应用预先计算好的哈希表,入侵者将更难获得密码。4.特殊算法PBKDF2、BCrypt、SCrypt的最佳选择是使用为散列密码开发的特殊算法。这些算法是自适应的,可以故意减慢计算时间,使暴力攻击更加困难。我们以BCrypt算法(实现是SpringSecurity的一部分)为例:Stringpassword="pa$$word";字符串盐=BCrypt.gensalt();Stringhash=BCrypt.hashpw(密码,盐);Theresultisthefollowinghash希值:$2a$10$alXdzX7lkEp52xiKS7YfuelpoFqz6AsvyBwIEz/BbWghdkmwGqYoy$2a$-thehashalgorithmidentifier10-numberofhashingrounds(2^10=1024)alXdzX7lkEp52xiKS7Yfue-saltlpoFqz6AsvyBwIEz/BbWghdkmwGqYoy-hash为了计算这个函数,我们使用了1024轮哈希。随着时间的推移和计算能力的增长,我们可以增加这个值来保持计算复杂度。验证用户身份时,只需调用一个方法来检查发送的密码和存储在数据库中的哈希值://plaintext-由用户发送//hash-从DBbooleancheckpw=BCrypt.checkpw(plaintext,hash);5、使用PAKE通过PAKE(PasswordAuthenticationKeyAgreement)系列协议,可以在不传输密码的情况下验证用户是否知道密码。该算法经过专门设计,因此不会传输密码本身,但会传输使用密码计算出的一些值。如果攻击者获得了对数据库的访问权限,即使他可以窃听客户端和服务器之间的通道,他也无法恢复原始密码值。我们以PAKE的SRP-6a为例。登录过程分两步进行,经过数学计算以证明客户端输入的密码而不传输密码。RFC5054中详细描述了协议规范。为了保存认证数据,需要计算v和s的值,其中v是验证者,s是盐。我们已经知道salt的计算方法,verifier的计算方法是:x=H(salt,password)v=g^x(modN)H-哈希函数(SHA-1,SHA256等)。g,N-可以从RFC5054.AppendixA中选择的常量。重要的是要注意,所选择的常量和哈希函数在服务器和客户端上必须相同。Salt和verifier值可以在客户端和服务端进行计算。如果值是在客户端计算的,我们根本不会在通信通道上传输密码,但我们也无法在服务器上检查密码策略(长度、通配符数量等)。因此,这些支票也需要传送给客户。例如,您可以使用NimbusSRP库:Stringpassword="pa$$word";SRP6CryptoParamsconfig=SRP6CryptoParams.getInstance(256,"SHA-1");SRP6VerifierGeneratorverifierGenerator=newSRP6VerifierGenerator(config);byte[]saltBytes=verifierGenerator.generateRandomSalt(16);Stringverifier=verifierGenerator.generateVerifier(saltBytes,password.getBytes()).toString(16);Stringsalt=newBigInteger(saltBytes).toString(16);结果:salt:6bb9db1c839bdc59ecbcd0ee12488462verifier:f28aed4372b1312ccdd6e281c7270be503bac99bff845c41da8189eadf9e4497这些值必须保存在数据库中,稍后在客户端身份验证过程中使用。该协议最大的优点是密码不会以任何方式传输到服务器,无法从验证者值中恢复出原始密码。此外,验证者仅在注册期间传输(如果在客户端计算)并且仅用于身份验证期间的计算。该协议本身可以抵抗MITM攻击,这意味着如果有人不小心在服务器上启用了所有用户请求的日志记录,并且这些日志随后被泄露,密码也不会被泄露。此数据按会话计算,不能用于重新输入。2.结语正确使用现代用户认证方式可以大大降低认证数据泄露的可能性,但认证只是网络安全领域的一方面。此外,日志请求和日志存储也是值得人们关注的问题。原文链接:https://dzone.com/articles/password-authentication-how-to-do-it-correctly译者介绍baron,社区编辑,拥有九年手机安全/SOC底层安全开发经验,是擅长TrustZone/TEE安全产品的设计与开发。