小羊博客

  • 首页
  • 分类
    • 解决方案
    • 安全
    • 编程
    • 杂记
Sheep Blog
小羊的个人博客
  1. 首页
  2. 安全
  3. 正文

如何安全地在数据库中保存用户密码

2022-11-13 1条评论

背景

在我们设计一个Web程序时,往往需要包含用户登录注册系统,如何安全地保存用户密码是一个不可忽视的大问题。如果直接保存密码明文,或者使用一些不安全的加密算法加密后保存密文,那么一旦数据库泄露,可能会造成非常大的危害。密码在数据库中往往并不需要还原,因此,最好的方式就是使用加盐的密码hash(salted password hashing)。

什么是hash

Hash算法是一种单向的函数,可以把任意数量的数据转换成固定长度的指纹,这个过程是不可逆的,而且,只要输入发生改变,哪怕只改变1bit,输出的指纹也会有很大的不同。这种特性非常适合用来保存密码。在一个使用Hash的账号系统中,用户注册和认证的大致流程如下:

但使用hash之后,密码就一定安全了吗?答案是错误的。最常听到的Hash算法有MD5、SHA256、SHA512等,如果在网上搜索MD5,能找到一大堆可以破解的网站,而且很多常见的字符串都可以逆向出来。显然,单纯对密码进行hash还是不够的。要知道如何正确使用Hash保存密码,我们首先需要了解黑客是如何破解hash的。

如何破解hash

字典和暴力破解攻击

字典攻击就是自己构造一个包含常见密码、短语、字符串的文件集合,然后对每一条数据进行hash,并和要破解的hash值比较,如果一致,就说明破解成功了。字典攻击的有效性和字典的质量成正比,和密码的复杂度成反比。

暴力破解就是不构造字典,直接尝试所有可能字符的所有组合。显然,当长度确定且较短时,它是可行有效的;但如果密码长度不确定,或者长度很长时,这种方式会消耗很大的资源。

理论上没有任何一种Hash算法能够抵抗字典和暴力破解攻击,我们能做的只是尽可能让这种方式变得低效。比如,强制用户使用复杂密码,采用未知长度的密码,采用密码工具提供的随机密码等。

查表破解

查表破解和上述方法类似。对于特定的hash类型,可以预先计算出密码字典中每一个密码的hash,然后将hash值和密码保存在一个表里,需要破解时只需要查表即可。一个设计良好的查询表结构,即使存储了数十亿个hash,每秒钟仍然可以查询成百上千个hash。

反向查表破解

反向查表破解的前提是攻击者已经拿到了数据库,然后构造常见密码字典,对字典中的值进行hash后与数据库比较,如果有匹配的,就可以破解相对应的用户密码了。这种攻击方式很有效果,因为通常情况下很多用户都会有使用相同的密码。

彩虹表

彩虹表和查表破解类似,只是牺牲了一些破解时间,来换取更小的存储空间,单位存储空间能够存储更多的hash。但哈希表并不是简单的明文到密文的映射。

彩虹表的前身

彩虹表的前身是“预计算的哈希链集”。当面对需要破解的哈希函数H,首先定义一个约减函数R,该函数的定义域与值域需要和H相反。通过R函数可以将哈希值约减为一个与原文相同格式的值,由于H不可逆,所以不可能通过R函数直接获取到明文,R函数只是将其转换成了一个相同格式的值,这个值落在H的定义域里面。然后再对这个值进行H运算,得到一个密文;再进行R运算,又得到一个相同格式的明文……这样重复k次后,我们就得到了一条哈希链(假设k=2):

​

这条链并不需要完整记录下来,我们只需要在数据库中保存起始点和结束点即可,对于上图的这条哈希链,我们需要保存的是aaaaaa和kiebgt。之后,我们以大量随机明文作为起点,通过上述步骤计算出哈希链并将结束点存储,得到一张哈希链表。那么,如果使用这张哈希链表对密码进行破解呢?

假设我们知道密码哈希运算后的密文为920ECF10,则先对其进行一次R运算,得到kiebgt,查询哈希链表,发现存在这么一个结束点,那么我们就有足够的把握(并不是100%)认为,明文就存在于这条链上。之后,从起始点aaaaaa开始,重复哈希链的计算过程,发现sgfnyd进行H运算后得到的密文和要破解的密文一样,那么说明密码明文就是sgfnyd。

如果得到的密文不是920ECF10,而是281DAF40,那么一次R运算可能得不到存在的结束点,只需要再重复H和R运算即可,在上图的哈希链中,进行R->H->R运算后,得到了kiebgt,它是一个结束点,那么我们就可以找到起始点aaaaaa,重复哈希链表的计算过程,发现aaaaaa进行H运算后得到该密文,说明密码明文是aaaaaa。

如果进行了k次R->H运算后仍然没有找到结束点,那么我们就认为明文并不存在于这张表中,再计算也没有意义了。

每一条哈希链都代表了属性相同的一组明文,每一个明文都可以通过起始点快速地计算得出,计算次数不大于k。对于每一组明文,只需要保存起始点和结束点,储存空间只需约1/k,因此大大节约了空间。

R的问题

一个优秀的R函数应该做到以下两点:

  • 能够将值域固定在特定的范围,如长度范围、字符取值范围等
  • R必须同Hash函数一样,尽可能保证输出值在值域中均匀分布,减少碰撞的概率

什么是碰撞?发生了碰撞会出现什么结果呢?

碰撞就是R函数将不同的哈希值计算得出了相同的明文,这时就可能出现下图的情况,加粗部分是完全相同的哈希链。如果碰撞概率较大,那么可能会出现很多很多的重复部分,造成严重的冗余和浪费。

​

彩虹表的改进

彩虹表对碰撞问题进行了改进。在彩虹表中,R函数并不是一个函数,而是k个函数的集合。在生成哈希链的计算过程中,每一步都使用不同的R函数,如下图所示。这样,即使发生碰撞,只要他们不是在同一个位置,也不会造成太多的浪费,因为下一次R运算使用的函数并不相同。

​

如果碰撞发生的位置相同会怎样呢?假设都是在第i个位置发生的碰撞,那么总共要进行k此运算,剩下的计算次数是k-i次,每一次计算使用的R也是相同的,最终得到的结束点也一定是相同的,这样就会直接舍弃其中的一条链,自然不会浪费存储空间。

不过彩虹表的使用比较麻烦。首先,假设要破解的密文位于任一链条的k-1位置处,对其进行Rk运算,得到结束点,然后就可以用起始点验证其正确性。但如果并没有找到结束点,就需要假设它在k-2位置,进行两次运算……重复这个过程,直到找到包含在表中的结束点。最不利的情况下,需要将密文进行完整的R1->H->...->Rk运算后,才能得知明文是否存在于彩虹表中。

对于相同个数的明文,当k越大时,破解的期望时间就越长,但彩虹表所占用的空间就越小;相反,k越小时,彩虹表本身就越大,相应的破解时间就越短。这正是保持空间、时间二者平衡的精髓所在。极端的,令k=1,简化函数R(x)=x,这样的彩虹表就变成了通常的错误理解,即将明文、密文对应关系全部保存的表。此时由于k极小,因而得到的表的体积极大,甚至可能超出储存能力。

如果将哈希后的密文比作一把锁,暴力破解的方法就是现场制作各种各样不同齿形的钥匙,再来尝试能否开锁,这样耗时无疑很长;查表破解,是事先制作好所有齿形的钥匙,全部拿过来尝试开锁,这样虽然省去了制作钥匙的时间,但是这些钥匙实在是太多了,没法全部带在身上;彩虹表,是将钥匙按照某种规律进行分组,每组钥匙中只需要带最有特点的一个,当发现某个“特征钥匙”差一点就能开锁了,则当场对该钥匙进行简单的打磨,直到能开锁为止。这种方法是既省力又省时的。

彩虹表的防御

从上述过程可知,彩虹表是针对特定的哈希函数H制作的,如果H变了,那么哈希表也需要改变。因此防御哈希表,可以从改变H下手,这就是后面要提到的加盐。

加盐

如果两个用户使用了同样的密码,那么一定他们的密码hash也一定相同。我们可以让每一个hash随机化,同一个密码每次hash后得到不同的密码,这样就可以避免彩虹表攻击了。

具体操作就是,给每个密码加一个随机的前缀或者后缀,然后再进行哈希,这个随机的后缀或前缀就称为“盐”。检查用户输入的密码时,我们也还需要这个盐值,因此需要把它也保存在数据库中,可以单独新建一个salt列来保存,或者也可以同hash保存在一列中,中间通过特殊符号分隔。

盐不需要保密,只要盐是随机的话,查表,彩虹表都会失效。因为攻击者无法事先知道盐是什么,也就没有办法预先计算出查询表和彩虹表。如果每个用户都是使用了不同的盐,那么反向查表攻击也没法成功。

盐值的长度一定要足够长,如果太短,假设只有3位,那么可能只需要1TB的存储空间就足够破解了。一个经验规则就是盐的至少要跟hash函数输出的长度一致。

盐值不可以复用,否则可能会被逆向出来,构造出特定的彩虹表进行快速破解。正确的做法是,使用可靠安全的随机数生成器来得到,在Java中可以用java.security.SecureRandom​,在Python中可以用os.urandom​,在PHP中可以用mcrypt_create_iv​或者openssl_random_pseudo_bytes​。每一个用户、每一个密码都需要使用不同的盐,每次修改密码都要生成一个新的随机盐,永远不要重复使用盐。

要验证用户输入的密码,需要从数据库中取出盐和哈希,然后将用户输入与盐值拼接,以相同的哈希函数进行哈希,与数据库中的值进行比较,如果相同,说明密码正确。

加盐可以让攻击者无法使用查表和彩虹表的方式对大量hash进行破解,但是依然无法避免对单个hash的字典和暴力攻击。高端的显卡(GPUs)和一些定制的硬件每秒可以计算数十亿的hash,所以针对单个hash的攻击依然有效。

如何恰当地进行hash

为了避免字典和暴力攻击,我们可以采用一种称为key扩展(key stretching)的技术。这种技术的思路就是让hash变得非常缓慢,即使使用告诉GPU和特定的硬件,字典和暴力破解的速度也慢到没有实用价值。通过减慢hash的过程来防御攻击,但是hash速度依然可以保证用户使用的时候没有明显的延迟。

常见的此类算法有PBKDF2和bcrypt。这些算法采用了一个安全变量或者迭代次数作为参数。比如PBKDF2,采用对密码和盐值实用哈希函数进行特定次数的迭代,最终得到一个密文。

常见的PBKDF2算法代码实现

PHP

<?php
/*
 * Password Hashing With PBKDF2 (http://crackstation.net/hashing-security.htm)
 * Copyright (c) 2013, Taylor Hornby
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, 
 * this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation 
 * and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 * POSSIBILITY OF SUCH DAMAGE.
 */

// These constants may be changed without breaking existing hashes.
define("PBKDF2_HASH_ALGORITHM", "sha256");
define("PBKDF2_ITERATIONS", 1000);
define("PBKDF2_SALT_BYTE_SIZE", 24);
define("PBKDF2_HASH_BYTE_SIZE", 24);

define("HASH_SECTIONS", 4);
define("HASH_ALGORITHM_INDEX", 0);
define("HASH_ITERATION_INDEX", 1);
define("HASH_SALT_INDEX", 2);
define("HASH_PBKDF2_INDEX", 3);

function create_hash($password)
{
    // format: algorithm:iterations:salt:hash
    $salt = base64_encode(mcrypt_create_iv(PBKDF2_SALT_BYTE_SIZE, MCRYPT_DEV_URANDOM));
    return PBKDF2_HASH_ALGORITHM . ":" . PBKDF2_ITERATIONS . ":" .  $salt . ":" .
        base64_encode(pbkdf2(
            PBKDF2_HASH_ALGORITHM,
            $password,
            $salt,
            PBKDF2_ITERATIONS,
            PBKDF2_HASH_BYTE_SIZE,
            true
        ));
}

function validate_password($password, $correct_hash)
{
    $params = explode(":", $correct_hash);
    if(count($params) < HASH_SECTIONS)
       return false;
    $pbkdf2 = base64_decode($params[HASH_PBKDF2_INDEX]);
    return slow_equals(
        $pbkdf2,
        pbkdf2(
            $params[HASH_ALGORITHM_INDEX],
            $password,
            $params[HASH_SALT_INDEX],
            (int)$params[HASH_ITERATION_INDEX],
            strlen($pbkdf2),
            true
        )
    );
}

// Compares two strings $a and $b in length-constant time.
function slow_equals($a, $b)
{
    $diff = strlen($a) ^ strlen($b);
    for($i = 0; $i < strlen($a) && $i < strlen($b); $i++)
    {
        $diff |= ord($a[$i]) ^ ord($b[$i]);
    }
    return $diff === 0;
}

/*
 * PBKDF2 key derivation function as defined by RSA's PKCS #5: https://www.ietf.org/rfc/rfc2898.txt
 * $algorithm - The hash algorithm to use. Recommended: SHA256
 * $password - The password.
 * $salt - A salt that is unique to the password.
 * $count - Iteration count. Higher is better, but slower. Recommended: At least 1000.
 * $key_length - The length of the derived key in bytes.
 * $raw_output - If true, the key is returned in raw binary format. Hex encoded otherwise.
 * Returns: A $key_length-byte key derived from the password and salt.
 *
 * Test vectors can be found here: https://www.ietf.org/rfc/rfc6070.txt
 *
 * This implementation of PBKDF2 was originally created by https://defuse.ca
 * With improvements by http://www.variations-of-shadow.com
 */
function pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output = false)
{
    $algorithm = strtolower($algorithm);
    if(!in_array($algorithm, hash_algos(), true))
        trigger_error('PBKDF2 ERROR: Invalid hash algorithm.', E_USER_ERROR);
    if($count <= 0 || $key_length <= 0)
        trigger_error('PBKDF2 ERROR: Invalid parameters.', E_USER_ERROR);

    if (function_exists("hash_pbkdf2")) {
        // The output length is in NIBBLES (4-bits) if $raw_output is false!
        if (!$raw_output) {
            $key_length = $key_length * 2;
        }
        return hash_pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output);
    }

    $hash_length = strlen(hash($algorithm, "", true));
    $block_count = ceil($key_length / $hash_length);

    $output = "";
    for($i = 1; $i <= $block_count; $i++) {
        // $i encoded as 4 bytes, big endian.
        $last = $salt . pack("N", $i);
        // first iteration
        $last = $xorsum = hash_hmac($algorithm, $last, $password, true);
        // perform the other $count - 1 iterations
        for ($j = 1; $j < $count; $j++) {
            $xorsum ^= ($last = hash_hmac($algorithm, $last, $password, true));
        }
        $output .= $xorsum;
    }

    if($raw_output)
        return substr($output, 0, $key_length);
    else
        return bin2hex(substr($output, 0, $key_length));
}
?>

JAVA

/* 
 * Password Hashing With PBKDF2 (http://crackstation.net/hashing-security.htm).
 * Copyright (c) 2013, Taylor Hornby
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, 
 * this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation 
 * and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 * POSSIBILITY OF SUCH DAMAGE.
 */

import java.security.SecureRandom;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.SecretKeyFactory;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;

/*
 * PBKDF2 salted password hashing.
 * Author: havoc AT defuse.ca
 * www: http://crackstation.net/hashing-security.htm
 */
public class PasswordHash
{
    public static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1";

    // The following constants may be changed without breaking existing hashes.
    public static final int SALT_BYTE_SIZE = 24;
    public static final int HASH_BYTE_SIZE = 24;
    public static final int PBKDF2_ITERATIONS = 1000;

    public static final int ITERATION_INDEX = 0;
    public static final int SALT_INDEX = 1;
    public static final int PBKDF2_INDEX = 2;

    /**
     * Returns a salted PBKDF2 hash of the password.
     *
     * @param   password    the password to hash
     * @return              a salted PBKDF2 hash of the password
     */
    public static String createHash(String password)
        throws NoSuchAlgorithmException, InvalidKeySpecException
    {
        return createHash(password.toCharArray());
    }

    /**
     * Returns a salted PBKDF2 hash of the password.
     *
     * @param   password    the password to hash
     * @return              a salted PBKDF2 hash of the password
     */
    public static String createHash(char[] password)
        throws NoSuchAlgorithmException, InvalidKeySpecException
    {
        // Generate a random salt
        SecureRandom random = new SecureRandom();
        byte[] salt = new byte[SALT_BYTE_SIZE];
        random.nextBytes(salt);

        // Hash the password
        byte[] hash = pbkdf2(password, salt, PBKDF2_ITERATIONS, HASH_BYTE_SIZE);
        // format iterations:salt:hash
        return PBKDF2_ITERATIONS + ":" + toHex(salt) + ":" +  toHex(hash);
    }

    /**
     * Validates a password using a hash.
     *
     * @param   password        the password to check
     * @param   correctHash     the hash of the valid password
     * @return                  true if the password is correct, false if not
     */
    public static boolean validatePassword(String password, String correctHash)
        throws NoSuchAlgorithmException, InvalidKeySpecException
    {
        return validatePassword(password.toCharArray(), correctHash);
    }

    /**
     * Validates a password using a hash.
     *
     * @param   password        the password to check
     * @param   correctHash     the hash of the valid password
     * @return                  true if the password is correct, false if not
     */
    public static boolean validatePassword(char[] password, String correctHash)
        throws NoSuchAlgorithmException, InvalidKeySpecException
    {
        // Decode the hash into its parameters
        String[] params = correctHash.split(":");
        int iterations = Integer.parseInt(params[ITERATION_INDEX]);
        byte[] salt = fromHex(params[SALT_INDEX]);
        byte[] hash = fromHex(params[PBKDF2_INDEX]);
        // Compute the hash of the provided password, using the same salt, 
        // iteration count, and hash length
        byte[] testHash = pbkdf2(password, salt, iterations, hash.length);
        // Compare the hashes in constant time. The password is correct if
        // both hashes match.
        return slowEquals(hash, testHash);
    }

    /**
     * Compares two byte arrays in length-constant time. This comparison method
     * is used so that password hashes cannot be extracted from an on-line 
     * system using a timing attack and then attacked off-line.
     * 
     * @param   a       the first byte array
     * @param   b       the second byte array 
     * @return          true if both byte arrays are the same, false if not
     */
    private static boolean slowEquals(byte[] a, byte[] b)
    {
        int diff = a.length ^ b.length;
        for(int i = 0; i < a.length && i < b.length; i++)
            diff |= a[i] ^ b[i];
        return diff == 0;
    }

    /**
     *  Computes the PBKDF2 hash of a password.
     *
     * @param   password    the password to hash.
     * @param   salt        the salt
     * @param   iterations  the iteration count (slowness factor)
     * @param   bytes       the length of the hash to compute in bytes
     * @return              the PBDKF2 hash of the password
     */
    private static byte[] pbkdf2(char[] password, byte[] salt, int iterations, int bytes)
        throws NoSuchAlgorithmException, InvalidKeySpecException
    {
        PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, bytes * 8);
        SecretKeyFactory skf = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM);
        return skf.generateSecret(spec).getEncoded();
    }

    /**
     * Converts a string of hexadecimal characters into a byte array.
     *
     * @param   hex         the hex string
     * @return              the hex string decoded into a byte array
     */
    private static byte[] fromHex(String hex)
    {
        byte[] binary = new byte[hex.length() / 2];
        for(int i = 0; i < binary.length; i++)
        {
            binary[i] = (byte)Integer.parseInt(hex.substring(2*i, 2*i+2), 16);
        }
        return binary;
    }

    /**
     * Converts a byte array into a hexadecimal string.
     *
     * @param   array       the byte array to convert
     * @return              a length*2 character string encoding the byte array
     */
    private static String toHex(byte[] array)
    {
        BigInteger bi = new BigInteger(1, array);
        String hex = bi.toString(16);
        int paddingLength = (array.length * 2) - hex.length();
        if(paddingLength > 0)
            return String.format("%0" + paddingLength + "d", 0) + hex;
        else
            return hex;
    }

    /**
     * Tests the basic functionality of the PasswordHash class
     *
     * @param   args        ignored
     */
    public static void main(String[] args)
    {
        try
        {
            // Print out 10 hashes
            for(int i = 0; i < 10; i++)
                System.out.println(PasswordHash.createHash("p\r\nassw0Rd!"));

            // Test password validation
            boolean failure = false;
            System.out.println("Running tests...");
            for(int i = 0; i < 100; i++)
            {
                String password = ""+i;
                String hash = createHash(password);
                String secondHash = createHash(password);
                if(hash.equals(secondHash)) {
                    System.out.println("FAILURE: TWO HASHES ARE EQUAL!");
                    failure = true;
                }
                String wrongPassword = ""+(i+1);
                if(validatePassword(wrongPassword, hash)) {
                    System.out.println("FAILURE: WRONG PASSWORD ACCEPTED!");
                    failure = true;
                }
                if(!validatePassword(password, hash)) {
                    System.out.println("FAILURE: GOOD PASSWORD NOT ACCEPTED!");
                    failure = true;
                }
            }
            if(failure)
                System.out.println("TESTS FAILED!");
            else
                System.out.println("TESTS PASSED!");
        }
        catch(Exception ex)
        {
            System.out.println("ERROR: " + ex);
        }
    }

}

参考链接

  • 加盐hash保存密码的正确方式
  • 什么是彩虹表?

​

​​

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
标签: hash pbkdf2 密码学
最后更新:2022-11-13

小羊

未来不迎,当下不杂,既往不恋。

打赏 点赞
< 上一篇

文章评论

  • 林林

    欢迎互访。

    2022-11-18
    回复
  • razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
    取消回复
    文章目录
    • 背景
    • 什么是hash
    • 如何破解hash
      • 字典和暴力破解攻击
      • 查表破解
      • 反向查表破解
      • 彩虹表
    • 加盐
    • 如何恰当地进行hash
    • 常见的PBKDF2算法代码实现
      • PHP
      • JAVA
    • 参考链接

    COPYRIGHT © 2022 Sheep Blog. ALL RIGHTS RESERVED.

    Theme Kratos Made By Seaton Jiang

    豫ICP备2021033701号

    豫公网安备 41108102000493号