由于密码被泄露的责任,存储密码可能是一个细微差别。更糟糕的是,用户倾向于跨服务重复使用密码,这使得安全地存储密码变得更加重要。
安全存储密码的目的是,即使包含密码的数据库遭到破坏,攻击者也无法破译任何用户的实际密码。这排除了将密码存储在纯文本中。
使用加密似乎是一个不错的选择,因为攻击者不知道实际的密码(因为它们是加密的)。但是,如果数据库被泄露,那么加密密钥可能[^1] 也会被泄露。使用这些密钥,攻击者将能够解密加密的密码——这使得这种存储方法很弱。
这就是散列或散列函数发挥作用的地方。
什么是哈希函数?
它们是具有以下属性的函数:
OutInhash(In) = Out"401357cf18542b4117ca59800657b64cce2a36d8ad4c56b6102a1e0b03049e97"string"hello""2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824""hella""70de66401b1399d79b843521ee726dcec1e9a8cb5708ec1520f1f3bb4b1dd984"
总而言之,哈希函数是“单向函数”。如果你只知道输出,就不可能/很难知道它的输入。这与加密不同,在给定输出和加密密钥的情况下,您可以知道输入。
由于这种单向属性,存储密码的散列值是一个好主意,因为如果它们的散列被泄露(通过数据库泄漏),攻击者将不知道原始密码(这是散列函数的输入) .事实上,唯一知道散列函数输入的“实体”是最初生成密码的最终用户。从安全的角度来看,这正是我们想要的。
由于属性编号 (5),有无限数量的输入可以产生相同的哈希输出。但是,在给定特定输出的情况下,也很难找到这些无限数量的输入中的任何一个!
什么是加盐以及为什么单独散列还不够好 - 人类的问题
很多人倾向于使用常用密码比如“密码”、“12345”等。由于相同输入的哈希值永远不会改变(见上面的属性(4)),我们可以预先计算常用密码的哈希值,然后检查针对这些预先计算的哈希泄露数据库数据。
"12345""5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5""12345"
解决这个问题的方法是在对密码进行散列之前(在注册过程中)添加一些随机字符串,称为“盐”,然后我们将该随机字符串附加到计算的散列中,然后再将其存储到数据库中.举个例子:
"12345""12345""ab$45""ih&g3""12345ab$45""12345ih&g3""2bb12bb768eb669f0e4b9df29e22a00467eb513c275ccfff1013288facac7889""b63400702c6f012aeaa57b5dc7eefaaaf3207cc6b68917911c410015ac0659b2"
"12345ab$45""12345ih&g3"
在我们将这些哈希值存储在数据库中之前,我们必须将盐附加到它们:
"2bb12bb768eb669f0e4b9df29e22a00467eb513c275ccfff1013288facac7889.ab$45""b63400702c6f012aeaa57b5dc7eefaaaf3207cc6b68917911c410015ac0659b2.ih&g3"
我们将盐附加到散列的原因是,在验证过程中,我们必须使用与最初相同的盐。所以我们必须把它存储在某个地方。即使盐被泄露,这也不是安全问题,因为攻击者仍然需要知道/猜测用户的密码才能生成相同的哈希。
让我们看看验证过程是如何发生的:
"abcdef""ab$45""abcdefab$45""c5110931a3ae4762c1c0334d8eeba8c9c555962cf7d2750fdd732936319a058c""c5110931a3ae4762c1c0334d8eeba8c9c555962cf7d2750fdd732936319a058c.ab$45"
"12345"
选择哪个哈希函数?
即使在加盐之后,暴力攻击的问题仍然存在。攻击者可以反复猜测不同的密码(非常快速),以查看哪个密码与泄露的哈希值匹配。有两个维度决定了攻击者可以多快找到匹配项:
1、用户密码的随机性和长度。
- 哈希函数计算哈希所需的时间
如果用户使用随机且足够长的密码,攻击者猜测该确切字符串的机会就会减少。这意味着他们必须进行更多的猜测,这将需要更多的时间。这是一个非常酷的工具,它估计猜测给定密码需要多长时间。
散列函数越慢且计算成本越高,验证每个猜测所需的时间就越多。在撰写本文时(2022 年 3 月 2 日),推荐的散列技术是使用Argon2id,最小配置为 15 MiB 内存,迭代次数为 2,并行度为 1[3].
随着计算能力的提高,推荐的散列技术也会发生变化。即使算法保持不变,建议的“轮数”/每个密码哈希应该完成的“工作量”可能会增加。
示例代码
NodeJS
import * as argon2 from "argon2";
import * as crypto from "crypto";
const hashingConfig = { // based on OWASP cheat sheet recommendations (as of March, 2022)
parallelism: 1,
memoryCost: 64000, // 64 mb
timeCost: 3 // number of itetations
}
async function hashPassword(password: string) {
let salt = crypto.randomBytes(16);
return await argon2.hash(password, {
...hashingConfig,
salt,
})
}
async function verifyPasswordWithHash(password: string, hash: string) {
return await argon2.verify(hash, password, hashingConfig);
}
hashPassword("somePassword").then(async (hash) => {
console.log("Hash + salt of the password:", hash)
console.log("Password verification success:", await verifyPasswordWithHash("somePassword", hash));
});
以上产生以下输出:
Hash + salt of the password: $argon2i$v=19$m=15000,t=3,p=1$tgSmiYOCjQ0im5U6NXEvPg$xKC4V31JqIK2XO91fnMCfevATq1rVDjIRX0cf/dnbKY
Password verification success: true
如果你运行上面的程序,每次都会产生不同的哈希,因为每次都会重新生成盐。
Go语言
package main
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"log"
"strings"
"golang.org/x/crypto/argon2"
)
type params struct {
memory uint32
iterations uint32
parallelism uint8
saltLength uint32
keyLength uint32
}
func main() {
p := ¶ms{
memory: 64 * 1024, // 64 MB
iterations: 3,
parallelism: 1,
saltLength: 16,
keyLength: 32,
}
encodedHash, err := generateHashFromPassword("somePassword", p)
if err != nil {
log.Fatal(err)
}
fmt.Println("Hash + salt of the password:")
fmt.Println(encodedHash)
match, err := verifyPassword("somePassword", encodedHash)
if err != nil {
log.Fatal(err)
}
fmt.Printf("\nPassword verification success: %v\n", match)
}
func generateHashFromPassword(password string, p *params) (encodedHash string, err error) {
salt, err := generateRandomBytes(p.saltLength)
if err != nil {
return "", err
}
hash := argon2.IDKey([]byte(password), salt, p.iterations, p.memory, p.parallelism, p.keyLength)
// Base64 encode the salt and hashed password.
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
b64Hash := base64.RawStdEncoding.EncodeToString(hash)
// Return a string using the standard encoded hash representation.
encodedHash = fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, p.memory, p.iterations, p.parallelism, b64Salt, b64Hash)
return encodedHash, nil
}
func generateRandomBytes(n uint32) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
if err != nil {
return nil, err
}
return b, nil
}
func verifyPassword(password, encodedHash string) (match bool, err error) {
// Extract the parameters, salt and derived key from the encoded password
// hash.
p, salt, hash, err := decodeHash(encodedHash)
if err != nil {
return false, err
}
// Derive the key from the other password using the same parameters.
otherHash := argon2.IDKey([]byte(password), salt, p.iterations, p.memory, p.parallelism, p.keyLength)
// Check that the contents of the hashed passwords are identical. Note
// that we are using the subtle.ConstantTimeCompare() function for this
// to help prevent timing attacks.
if subtle.ConstantTimeCompare(hash, otherHash) == 1 {
return true, nil
}
return false, nil
}
func decodeHash(encodedHash string) (p *params, salt, hash []byte, err error) {
vals := strings.Split(encodedHash, "$")
if len(vals) != 6 {
return nil, nil, nil, errors.New("the encoded hash is not in the correct format")
}
var version int
_, err = fmt.Sscanf(vals[2], "v=%d", &version)
if err != nil {
return nil, nil, nil, err
}
if version != argon2.Version {
return nil, nil, nil, errors.New("incompatible version of argon2")
}
p = ¶ms{}
_, err = fmt.Sscanf(vals[3], "m=%d,t=%d,p=%d", &p.memory, &p.iterations, &p.parallelism)
if err != nil {
return nil, nil, nil, err
}
salt, err = base64.RawStdEncoding.Strict().DecodeString(vals[4])
if err != nil {
return nil, nil, nil, err
}
p.saltLength = uint32(len(salt))
hash, err = base64.RawStdEncoding.Strict().DecodeString(vals[5])
if err != nil {
return nil, nil, nil, err
}
p.keyLength = uint32(len(hash))
return p, salt, hash, nil
}
蟒蛇
import argon2
argon2Hasher = argon2.PasswordHasher(
time_cost=3, # number of iterations
memory_cost=64 * 1024, # 64mb
parallelism=1, # how many parallel threads to use
hash_len=32, # the size of the derived key
salt_len=16 # the size of the random generated salt in bytes
)
password = "somePassword"
hash = argon2Hasher.hash(password)
print("Hash + salt of password", hash)
verifyValid = argon2Hasher.verify(hash, password)
print("Password verification success:", verifyValid)
爪哇
import de.mkammerer.argon2.Argon2;
import de.mkammerer.argon2.Argon2Factory;
public class PasswordHashing {
public static void main(String[] args) {
// salt 32 bytes
// Hash length 64 bytes
Argon2 argon2 = Argon2Factory.create(
Argon2Factory.Argon2Types.ARGON2id,
16,
32);
char[] password = "somePassword".toCharArray();
String hash = argon2.hash(3, // Number of iterations
64 * 1024, // 64mb
1, // how many parallel threads to use
password);
System.out.println("Hash + salt of the password: "+hash);
System.out.println("Password verification success: "+ argon2.verify(hash, password));
}
}
脚注:
"5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5""12345"
3.https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html^
** 写于SuperTokens的人们——希望你喜欢!我们始终在我们的Discord服务器上可用。如果您有任何问题或需要任何帮助,请加入我们。**