后端 · 2024年 12月 30日 0

如何对摘要算法进行加盐

在前面讲到哈希算法时,我们说,存储用户的哈希口令时,要加盐存储,目的就在于抵御彩虹表攻击。

我们回顾一下哈希算法:

digest = hash(input)

正是因为相同的输入会产生相同的输出,我们加盐的目的就在于,使得输入有所变化:

digest = hash(salt + input)

这个salt可以看作是一个额外的“认证码”,同样的输入,不同的认证码,会产生不同的输出。因此,要验证输出的哈希,必须同时提供“认证码”。

Hmac算法就是一种基于密钥的消息认证码算法,它的全称是Hash-based Message Authentication Code,是一种更安全的消息摘要算法。

Hmac算法总是和某种哈希算法配合起来用的。例如,我们使用MD5算法,对应的就是HmacMD5算法,它相当于“加盐”的MD5:

HmacMD5 ≈ md5(secure_random_key, input)

因此,HmacMD5可以看作带有一个安全的key的MD5。使用HmacMD5而不是用MD5加salt,有如下好处:

  • HmacMD5使用的key长度是64字节,更安全;
  • Hmac是标准算法,同样适用于SHA-1等其他哈希算法;
  • Hmac输出和原有的哈希算法长度一致。

可见,Hmac本质上就是把key混入摘要的算法。验证此哈希时,除了原始的输入数据,还要提供key。

为了保证安全,我们不会自己指定key,而是通过Java标准库的KeyGenerator生成一个安全的随机的key。下面是使用HmacMD5的代码:

import javax.crypto.*;
import java.util.HexFormat;

public class Main {
    public static void main(String[] args) throws Exception {
        KeyGenerator keyGen = KeyGenerator.getInstance("HmacMD5");
        SecretKey key = keyGen.generateKey();
        // 打印随机生成的key:
        byte[] skey = key.getEncoded();
        System.out.println(HexFormat.of().formatHex(skey));
        Mac mac = Mac.getInstance("HmacMD5");
        mac.init(key);
        mac.update("HelloWorld".getBytes("UTF-8"));
        byte[] result = mac.doFinal();
        System.out.println(HexFormat.of().formatHex(result));
    }
}

和MD5相比,使用HmacMD5的步骤是:

  1. 通过名称HmacMD5获取KeyGenerator实例;
  2. 通过KeyGenerator创建一个SecretKey实例;
  3. 通过名称HmacMD5获取Mac实例;
  4. SecretKey初始化Mac实例;
  5. Mac实例反复调用update(byte[])输入数据;
  6. 调用Mac实例的doFinal()获取最终的哈希值。

我们可以用Hmac算法取代原有的自定义的加盐算法,因此,存储用户名和口令的数据库结构如下:

usernamesecret_key (64 bytes)password
boba8c06e05f92e…5e167e0387872a57c85ef6dddbaa12f376de
alicee6a343693985…f4bec1f929ac2552642b302e739bc0cdbaac
timf27a973dfdc0…6003af57651c3a8a73303515804d4af43790

有了Hmac计算的哈希和SecretKey,我们想要验证怎么办?这时,SecretKey不能从KeyGenerator生成,而是从一个byte[]数组恢复:

import javax.crypto.*;
import javax.crypto.spec.*;
import java.util.HexFormat;

public class Main {
    public static void main(String[] args) throws Exception {
        byte[] hkey = HexFormat.of().parseHex(
                "b648ee779d658c420420d86291ec70f5" + 
                "cf97521c740330972697a8fad0b55f5c" + 
                "5a7924e4afa99d8c5883e07d7c3f9ed0" + 
                "76aa544d25ed2f5ceea59dcc122babc8");
        SecretKey key = new SecretKeySpec(hkey, "HmacMD5");
        Mac mac = Mac.getInstance("HmacMD5");
        mac.init(key);
        mac.update("HelloWorld".getBytes("UTF-8"));
        byte[] result = mac.doFinal();
        System.out.println(HexFormat.of().formatHex(result)); // 4af40be7864efaae1473a4c601b650ae
    }
}

恢复SecretKey的语句就是new SecretKeySpec(hkey, "HmacMD5")

在Java中计算哈希值时,加盐(salting)是一种常见的安全措施,用于增加哈希值的复杂性,防止彩虹表攻击。加盐的基本思想是在原始数据(通常是密码)中加入一个随机生成的字符串(即盐),然后再进行哈希计算。

以下是一个简单的示例,展示了如何在Java中使用加盐来计算哈希值:

  1. 生成盐:使用SecureRandom类生成一个随机的盐。
  2. 将盐和原始数据组合:将生成的盐与原始数据(如密码)组合在一起。
  3. 计算哈希值:使用MessageDigest类计算组合后的数据的哈希值。
  4. 存储盐和哈希值:通常会将生成的盐和计算得到的哈希值一起存储,以便后续验证。

下面是一个具体的代码示例:

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;

public class HashWithSalt {

    // 生成一个随机的盐
    public static byte[] generateSalt() {
        SecureRandom random = new SecureRandom();
        byte[] salt = new byte[16]; // 盐的长度可以调整
        random.nextBytes(salt);
        return salt;
    }

    // 使用盐和原始数据计算哈希值
    public static byte[] hashWithSalt(byte[] salt, String password) throws NoSuchAlgorithmException {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        digest.reset();
        digest.update(salt);
        byte[] hash = digest.digest(password.getBytes());
        return hash;
    }

    // 将字节数组转换为Base64编码的字符串
    public static String bytesToBase64(byte[] bytes) {
        return Base64.getEncoder().encodeToString(bytes);
    }

    // 将Base64编码的字符串转换为字节数组
    public static byte[] base64ToBytes(String base64) {
        return Base64.getDecoder().decode(base64);
    }

    public static void main(String[] args) {
        try {
            String password = "mySecretPassword";

            // 生成盐
            byte[] salt = generateSalt();
            System.out.println("Generated Salt: " + bytesToBase64(salt));

            // 计算哈希值
            byte[] hashedPassword = hashWithSalt(salt, password);
            System.out.println("Hashed Password: " + bytesToBase64(hashedPassword));

            // 存储盐和哈希值(这里假设存储为Base64编码的字符串)
            String storedSalt = bytesToBase64(salt);
            String storedHash = bytesToBase64(hashedPassword);

            // 验证过程
            String inputPassword = "mySecretPassword";
            byte[] inputSalt = base64ToBytes(storedSalt);
            byte[] inputHashedPassword = hashWithSalt(inputSalt, inputPassword);

            if (bytesToBase64(inputHashedPassword).equals(storedHash)) {
                System.out.println("Password verification successful!");
            } else {
                System.out.println("Password verification failed!");
            }
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
    }
}

关键点总结:

  1. 生成盐:使用SecureRandom生成一个随机的盐。
  2. 计算哈希值:将盐和原始数据组合后,使用MessageDigest计算哈希值。
  3. 存储和验证:将盐和哈希值一起存储,并在验证时使用相同的盐重新计算哈希值进行比较。

通过这种方式,即使两个用户使用相同的密码,由于盐的不同,他们的哈希值也会不同,从而提高了安全性。

在Java中使用MessageDigest类进行哈希计算时,digest.update(salt);这一步的作用是将盐(salt)添加到哈希计算的过程中。具体来说,这一步将盐的数据更新到MessageDigest对象的内部状态中,以便在后续的哈希计算中包含盐的信息。

以下是详细的解释:

  1. 初始化MessageDigest对象
   MessageDigest digest = MessageDigest.getInstance("SHA-256");

这行代码创建了一个MessageDigest对象,用于计算SHA-256哈希值。

  1. 重置MessageDigest对象(可选):
   digest.reset();

这行代码重置MessageDigest对象的状态,确保之前的任何数据不会影响当前的哈希计算。

  1. 更新盐到MessageDigest对象
   digest.update(salt);

这行代码将盐的数据添加到MessageDigest对象的内部状态中。这意味着在后续的哈希计算中,盐的数据会被包含进去。

  1. 更新原始数据到MessageDigest对象
   byte[] hash = digest.digest(password.getBytes());

这行代码将原始数据(如密码)的数据添加到MessageDigest对象的内部状态中,并计算最终的哈希值。

通过这种方式,盐和原始数据一起被包含在哈希计算中,从而生成一个唯一的哈希值。即使两个用户使用相同的密码,由于盐的不同,他们的哈希值也会不同,这大大提高了安全性。

示例代码解释

以下是完整的示例代码,进一步说明digest.update(salt);的作用:

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;

public class HashWithSalt {

    // 生成一个随机的盐
    public static byte[] generateSalt() {
        SecureRandom random = new SecureRandom();
        byte[] salt = new byte[16]; // 盐的长度可以调整
        random.nextBytes(salt);
        return salt;
    }

    // 使用盐和原始数据计算哈希值
    public static byte[] hashWithSalt(byte[] salt, String password) throws NoSuchAlgorithmException {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        digest.reset(); // 可选:重置MessageDigest对象的状态
        digest.update(salt); // 将盐添加到MessageDigest对象中
        byte[] hash = digest.digest(password.getBytes()); // 计算最终的哈希值
        return hash;
    }

    // 将字节数组转换为Base64编码的字符串
    public static String bytesToBase64(byte[] bytes) {
        return Base64.getEncoder().encodeToString(bytes);
    }

    // 将Base64编码的字符串转换为字节数组
    public static byte[] base64ToBytes(String base64) {
        return Base64.getDecoder().decode(base64);
    }

    public static void main(String[] args) {
        try {
            String password = "mySecretPassword";

            // 生成盐
            byte[] salt = generateSalt();
            System.out.println("Generated Salt: " + bytesToBase64(salt));

            // 计算哈希值
            byte[] hashedPassword = hashWithSalt(salt, password);
            System.out.println("Hashed Password: " + bytesToBase64(hashedPassword));

            // 存储盐和哈希值(这里假设存储为Base64编码的字符串)
            String storedSalt = bytesToBase64(salt);
            String storedHash = bytesToBase64(hashedPassword);

            // 验证过程
            String inputPassword = "mySecretPassword";
            byte[] inputSalt = base64ToBytes(storedSalt);
            byte[] inputHashedPassword = hashWithSalt(inputSalt, inputPassword);

            if (bytesToBase64(inputHashedPassword).equals(storedHash)) {
                System.out.println("Password verification successful!");
            } else {
                System.out.println("Password verification failed!");
            }
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,digest.update(salt);这一步确保了盐的数据被包含在最终的哈希计算中,从而使得每个用户的哈希值都是唯一的,即使他们使用相同的密码。