This the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

信息安全

1 - 单向散列

原文链接:单向散列函数

NAME

1. 定义

单向散列函数(one-way hash function)是指对不同的输入值,通过单向散列函数进行计算,得到固定长度的输出值。这个输入值称为消息(message),输出值称为散列值(hash value)。

NAME

单向散列函数也被称为消息摘要函数(message digest function)、哈希函数或者杂凑函数。输入的消息也称为原像(pre-image)。输出的散列值也称为消息摘要(message digest)或者指纹(fingerprint),相当于该消息的身份证。

单向散列函数有多种实现算法,常见的有:MD5SHA-1SHA-2SHA-3

2. 特性

通过上面的定义,我们对单向散列函数的了解还是模糊的。下面介绍单向散列函数的特性,加深一下印象。

2.1 散列值长度固定

无论消息的长度有多少,使用同一算法计算出的散列值长度总是固定的。比如 MD5 算法,无论输入多少,产生的散列值长度总是 128 比特(16字节)。

然而比特是计算机能够识别的单位,而我们人类更习惯于使用十六进制字符串来表示(一个字节占用两位十六进制字符)。

NAME

2.2 消息不同其散列值也不同

使用相同的消息,产生的散列值一定相同。

使用不同的消息,产生的散列值也不相同。哪怕只有一个比特的差别,得到的散列值也会有很大区别。

这一特性也叫做抗碰撞性,对于抗碰撞性弱的算法,我们不应该使用。

NAME

2.3 具备单向性

只能通过消息计算出散列值,无法通过散列值反算出消息。

NAME

2.4 计算速度快

计算散列值的速度快。尽管消息越长,计算散列值的时间也越长,但也会在短时间内完成。

3. 常见算法

MD5 与 SHA-1 算法已被攻破,不应该被用于新的用途;SHA-2 与 SHA-3 还是安全的,可以使用。

SHA-2包括:SHA-224、SHA-256、SHA-384、SHA-512、SHA-512/224、SHA-512/256。

SHA-3包括:SHA3-224、SHA3-256、SHA3-384、SHA3-512。

算法名称散列值长度是否安全
MD5128不安全
SHA-1160不安全
SHA-224224安全
SHA-256256安全
SHA-384384安全
SHA-512512安全
SHA-512/224224安全
SHA-512/256256安全
SHA3-224224安全
SHA3-256256安全
SHA3-384384安全
SHA3-512512安全

4. 应用场景

单向散列函数并不能确保信息的机密性,它是一种保证信息完整性的密码技术。下面来看它的应用场景。

4.1 用户密码保护

用户在设置密码时,不记录密码本身,只记录密码的散列值,只有用户自己知道密码的明文。校验密码时,只要输入的密码正确,得到的散列值一定是一样的,表示校验正确。

为了防止彩虹表破解,还可以为密码进行加盐处理,只要验证密码时,使用相同的盐即可完成校验。

NAME

使用散列值存储密码的好处是:即使数据库被盗,也无法将密文反推出明文是什么,使密码保存更安全。

4.2 接口验签

为了保证接口的安全,可以采用签名的方式发送。

发送者与接收者要有一个共享秘钥。当发送者向接收者发送请求时,参数中附加上签名(签名由共享秘钥 *+* 业务参数,进行单向散列函数加密生成)。接收者收到后,使用相同的方式生成签名,再与收到的签名进行比对,如果一致,验签成功。

这样即可以验证业务参数是否被篡改,又能验明发送者的身份。

NAME

4.3 文件完整性校验

文件被挂载到网站时,同时也附上其散列值和算法,比如 Tomcat 官网。

NAME

用户下载后,计算其散列值,对比结果是否相同,从而校验文件的完整性。

4.4 云盘秒传

当我们将自己喜欢的视频放到网盘上时,发现只用了几秒的时间就上传成功了,而这个文件有几个G大小,是怎么做到的呢?

其实这个“秒传”功能可以利用单向散列函数来实现。

当我们上传一个文件时,云盘客户端会先为该文件生成一个散列值。拿着这个散列值去数据库中匹配,如果匹配到,说明该文件已经在云服务器存在。只需将该散列值与用户进行关联,便可完成本次“上传”。

这样,一个文件在云服务器上只会存一份,大大节约了云服务器的空间。

NAME

5. 代码实现

JDK的 java.security.MessageDigest 类为我们提供了消息摘要算法,用于 MD5和SHA的散列值生成。下面代码做了简单的封装,便于直接使用。

public class MDUtil {

    /**
     * MD5 加密
     *
     * @param data 要加密的数据
     * @return 32位十六进制字符串
     */
    public static String MD5(byte[] data) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] bytes = md.digest(data);
            return bytesToHexString(bytes);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return "";
    }

    /**
     * MD5 加密
     *
     * @param data 要加密的数据
     * @return 32位十六进制字符串
     */
    public static String MD5(String data) {
        return MD5(data.getBytes());
    }

    /**
     * SHA-1 加密
     *
     * @param data 要加密的数据
     * @return 40位十六进制字符串
     */
    public static String SHA1(byte[] data) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            byte[] bytes = md.digest(data);
            return bytesToHexString(bytes);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return "";
    }

    /**
     * SHA-1 加密
     *
     * @param data 要加密的数据
     * @return 40位十六进制字符串
     */
    public static String SHA1(String data) {
        return SHA1(data.getBytes());
    }

    /**
     * SHA-224 加密
     *
     * @param data 要加密的数据
     * @return 56位十六进制字符串
     */
    public static String SHA224(byte[] data) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-224");
            byte[] bytes = md.digest(data);
            return bytesToHexString(bytes);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return "";
    }

    /**
     * SHA-224 加密
     *
     * @param data 要加密的数据
     * @return 56位十六进制字符串
     */
    public static String SHA224(String data) {
        return SHA224(data.getBytes());
    }

    /**
     * SHA-256 加密
     *
     * @param data 要加密的数据
     * @return 64位十六进制字符串
     */
    public static String SHA256(byte[] data) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] bytes = md.digest(data);
            return bytesToHexString(bytes);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return "";
    }

    /**
     * SHA-256 加密
     *
     * @param data 要加密的数据
     * @return 64位十六进制字符串
     */
    public static String SHA256(String data) {
        return SHA256(data.getBytes());
    }

    /**
     * SHA-384 加密
     *
     * @param data 要加密的数据
     * @return 96位十六进制字符串
     */
    public static String SHA384(byte[] data) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-384");
            byte[] bytes = md.digest(data);
            return bytesToHexString(bytes);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return "";
    }

    /**
     * SHA-384 加密
     *
     * @param data 要加密的数据
     * @return 96位十六进制字符串
     */
    public static String SHA384(String data) {
        return SHA384(data.getBytes());
    }

    /**
     * SHA-512 加密
     *
     * @param data 要加密的数据
     * @return 128位十六进制字符串
     */
    public static String SHA512(byte[] data) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-512");
            byte[] bytes = md.digest(data);
            return bytesToHexString(bytes);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return "";
    }

    /**
     * SHA-512 加密
     *
     * @param data 要加密的数据
     * @return 128位十六进制字符串
     */
    public static String SHA512(String data) {
        return SHA512(data.getBytes());
    }

    /**
     * 将字节数组转换为十六进制字符串
     *
     * @param bytes 字节数组
     * @return 十六进制字符串
     */
    private static String bytesToHexString(byte[] bytes) {
        StringBuilder hexValue = new StringBuilder();
        for (byte b : bytes) {
            int val = b & 0xFF;
            if (val < 16) {
                hexValue.append("0");
            }
            hexValue.append(Integer.toHexString(val));
        }
        return hexValue.toString();
    }

}

下面分别使用这些算法计算“123456”的散列值:

public static void main(String[] args) {
    System.out.println("MD5\t\t" + MDUtil.MD5("123456"));
    System.out.println("SHA-1\t" + MDUtil.SHA1("123456"));
    System.out.println("SHA-224\t" + MDUtil.SHA224("123456"));
    System.out.println("SHA-256\t" + MDUtil.SHA256("123456"));
    System.out.println("SHA-384\t" + MDUtil.SHA384("123456"));
    System.out.println("SHA-512\t" + MDUtil.SHA512("123456"));
}

输出结果:

MD5      e10adc3949ba59abbe56e057f20f883e
SHA-1    7c4a8d09ca3762af61e59520943dc26494f8941b
SHA-224  f8cdb04495ded47615258f9dc6a3f4707fd2405434fefc3cbf4ef4e6
SHA-256  8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92
SHA-384  0a989ebc4a77b56a6e2bb7b19d995d185ce44090c13e2984b7ecc6d446d4b61ea9991b76a4c2f04b1b4d244841449454
SHA-512  ba3253876aed6bc22d4a6ff53d8406c6ad864195ed144ab5c87621b6c233b548baeae6956df346ec8c17f5ea10f35ee3cbc514797ed7ddd3145464e2a0bab413

我用的是Java8,还不支持 SHA-3,所以上面代码只封装了MD5、SHA-1和SHA-2。

NAME

从Java9开始支持SHA-3

NAME

2 - 对称加密

原文链接:对称加密

NAME

加密、解密和密钥

加密(Encrypt)是从明文生成密文的步骤,解密(Decrypt)是从密文还原成明文的步骤,而这两个步骤都需要用到密钥(Key)。这和我们现实中,用钥匙上锁和开锁是一样的。

NAME

什么是对称加密

对称加密(Symmetric Cryptography)是密码学中的一类加密算法,这类算法在加密和解密时,使用相同的密钥。

对称加密又称为共享密钥加密,其最大的缺点是,对称加密的安全性依赖于密钥,一旦泄露,就意味着任何人都能解密消息。

对称加密的优点是加密速度快,所以在很多场合被使用。

常见算法

本节介绍对称加密的一些常见算法,包括DES、3DES和AES。

DES算法

DES(Data Encryption Standard,中文:数据加密标准),是一种对称加密算法。该算法在1976年被美国联邦政府的国家标准局确定为联邦资料处理标准(FIPS),并于1977年被发布,随后在国际上广泛流传开来。然而,随着计算机的进步,DES 已经能够被暴力破解,所以该算法已经不安全了。

DES是一种分组密码(Block Cipher,或者叫块加密),即将明文按64比特进行分组加密,每组生成64位比特的密文。它的密钥长度为56比特(从规格上来说,密钥长度是64比特,但由于每隔7比特会设置一个用于错误检查的比特,因此实际长度为56比特)。

NAME

3DES算法

三重数据加密算法(Triple Data Encryption Algorithm,缩写为TDEA),简称3DES(Triple-DES),是DES的增强版,相当于对每组数据应用了三次DES算法。

由于DES算法的密钥长度过短,容易被暴力破解,为了解决这一问题,设计出了该算法。它使用简单的方法,通过增加DES密钥长度的方式来避免类似攻击,而不是一种全新的密码算法。

该算法在每次应用DES时,使用不同的密钥,所以有三把独立密钥。这三把密钥组成一起,是一个长度为168(56 + 56 + 56)比特的密钥,所以3DES算法的密钥总长度为168比特。

3DES的加密过程,并不是进行三次DES加密(加密→加密→加密),而是以密钥1、密钥2、密钥3的顺序,进行加密解密加密的过程。

NAME

3DES的解密过程和加密正好相反,是以密钥3、密钥2、密钥1的顺序,进行解密加密解密的操作。

NAME

AES算法

AES(Advanced Encryption Standard),即高级加密标准,是取代DES算法的一种新的对称加密算法。AES算法是从全世界的企业和密码学家,提交的对称密码算法中竞选出来的,最终 Rijndael 加密算法胜出,所以AES又称为 Rijndael 加密算法。

AES也是一种分组密码,它的分组长度为128比特,密钥长度可以为128比特、192比特或256比特。

NAME

分组密码的模式

上面介绍的DES、3DES和AES都属于分组密码,它们只能加密固定长度的明文。如果需要加密更长的明文,就需要对分组密码进行迭代,而分组密码的迭代方法称为分组密码的模式(Model)。简而一句话:分组密码的模式,就是分组密码的迭代方式。

分组密码有很多种模式,这里主要介绍以下几种:ECB、CBC、CFB、OFB、CTR。

明文分组与密文分组

在下面对模式的介绍时,会用到两个术语,这里先介绍一下:

在分组密码中,我们称每组的明文为明文分组,每组生成的密文称为密文分组

若将所有的明文分组合并起来就是完整的明文(先忽略填充),将所以的密文分组合并起来就是完整的密文。

ECB模式

ECB(Electronic CodeBook)模式,即电子密码本模式。该模式是将明文分组,加密后直接成为密文分组,分组之间没有关系。

NAME

ECB模式是所有模式中最简单的一种,该模式的明文分组与密文分组是一一对应的关系,若明文分组相同,其密文分组也一定相同。因此,ECB模式也是最不安全的模式。

CBC模式

CBC(Cipher Block Chaining)模式,即密码分组链接模式。该模式首先将明文分组与前一个密文分组进行XOR运算,然后再进行加密。只有第一个明文分组特殊,需要提前为其生成一个与分组长度相同的比特序列,进行XOR运算,这个比特序列称为初始化向量(Initialization Vector),简称IV

NAME

CFB模式

CFB(Cipher FeedBack)模式,即密文反馈模式。该模式首先将前一个密文分组进行加密,再与当前明文分组进行XOR运算,来生成密文分组。同样CFB模式也需要一个IV。

NAME

OFB模式

OFB(Output FeedBack)模式,即输出反馈模式。该模式会产生一个密钥流,即将密码算法的前一个输出值,做为当前密码算法的输入值。该输入值再与明文分组进行XOR运行,计算得出密文分组。该模式需要一个IV,进行加密后做为第一个分组的输入。

NAME

CTR模式

CTR(CounTeR)模式,即计数器模式。该模式也会产生一个密钥流,它通过递增一个计数器来产生连续的密钥流。对该计数器进行加密,再与明文分组进行XOR运算,计算得出密文分组。

NAME

分组密码的填充

在分组密码中,当数据长度不符合分组长度时,需要按一定的方式,将尾部明文分组进行填充,这种将尾部分组数据填满的方法称为填充(Padding)。

No Padding

即不填充,要求明文的长度,必须是加密算法分组长度的整数倍。

... | DD DD DD DD DD DD DD DD | DD DD DD DD DD DD DD DD |

ANSI X9.23

在填充字节序列中,最后一个字节填充为需要填充的字节长度,其余字节填充0

... | DD DD DD DD DD DD DD DD | DD DD DD DD 00 00 00 04 |

ISO 10126

在填充字节序列中,最后一个字节填充为需要填充的字节长度,其余字节填充随机数

... | DD DD DD DD DD DD DD DD | DD DD DD DD 81 A6 23 04 |

PKCS#5和PKCS#7

在填充字节序列中,每个字节填充为需要填充的字节长度

... | DD DD DD DD DD DD DD DD | DD DD DD DD 04 04 04 04 |

ISO/IEC 7816-4

在填充字节序列中,第一个字节填充固定值80,其余字节填充0。若只需填充一个字节,则直接填充80

... | DD DD DD DD DD DD DD DD | DD DD DD DD 80 00 00 00 |

... | DD DD DD DD DD DD DD DD | DD DD DD DD DD DD DD 80 |

Zero Padding

在填充字节序列中,每个字节填充为0

... | DD DD DD DD DD DD DD DD | DD DD DD DD 00 00 00 00 |

Java代码实现

Java在底层已经封装好了对称加密的实现, 我们只需要使用即可。现在介绍几个重要的类:

SecureRandom类

SecureRandom类是一个强安全的随机数生成器(Random Number Generator,简称:RNG),加密相关的推荐使用此随机数生成器。

我们可以通过构造方法生成一个实例,或者向构造方法传递一个种子来创建实例。

SecureRandom random = new SecureRandom();

KeyGenerator类

KeyGenerator类是对称密码的密钥生成器,需要指定加密算法,来生成相应的密钥。

Java中支持的算法:

  • AES (128)
  • DES (56)
  • DESede (168)
  • HmacSHA1
  • HmacSHA256

下面是一些标准算法的介绍:

NAME

生成密钥代码如下:

/**
 * 通过密码和算法获取 Key 对象
 *
 * @param key       密钥
 * @param algorithm 算法,例如:AES (128)、DES (56)、DESede (168)、HmacSHA1、HmacSHA256
 * @return 密钥 Key
 * @throws Exception
 */
private static Key getKey(byte[] key, String algorithm) throws Exception {
    // 通过算法获取 KeyGenerator 对象
    KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);
    // 使用密钥做为随机数,初始化 KeyGenerator 对象
    keyGenerator.init(new SecureRandom(key));
    // 生成 Key
    return keyGenerator.generateKey();
}

Cipher类

Cipher类提供了加密和解密的功能。该类需要指定一个转换(Transformation)来创建一个实例,转换的命名方式:算法名称/工作模式/填充方式

下面是Java支持的转换:

  • AES/CBC/NoPadding (128)
  • AES/CBC/PKCS5Padding (128)
  • AES/ECB/NoPadding (128)
  • AES/ECB/PKCS5Padding (128)
  • DES/CBC/NoPadding (56)
  • DES/CBC/PKCS5Padding (56)
  • DES/ECB/NoPadding (56)
  • DES/ECB/PKCS5Padding (56)
  • DESede/CBC/NoPadding (168)
  • DESede/CBC/PKCS5Padding (168)
  • DESede/ECB/NoPadding (168)
  • DESede/ECB/PKCS5Padding (168)
  • RSA/ECB/PKCS1Padding (1024, 2048)
  • RSA/ECB/OAEPWithSHA-1AndMGF1Padding (1024, 2048)
  • RSA/ECB/OAEPWithSHA-256AndMGF1Padding (1024, 2048)

下面是一些标准的模式:

NAME

下面是一些标准的填充:

NAME

加密代码如下:

private static final String DES_ALGORITHM = "DES";
private static final String DES_TRANSFORMATION = "DES/ECB/PKCS5Padding";

/**
 * DES 加密
 *
 * @param data 原始数据
 * @param key  密钥
 * @return 密文
 */
private static byte[] encryptDES(byte[] data, byte[] key) throws Exception {
    // 获取 DES Key
    Key secretKey = getKey(key, DES_ALGORITHM);

    // 通过标准转换获取 Cipher 对象, 由该对象完成实际的加密操作
    Cipher cipher = Cipher.getInstance(DES_TRANSFORMATION);
    // 通过加密模式、密钥,初始化 Cipher 对象
    cipher.init(Cipher.ENCRYPT_MODE, secretKey);
    // 生成密文
    return cipher.doFinal(data);
}

解密代码如下:

private static final String DES_ALGORITHM = "DES";
private static final String DES_TRANSFORMATION = "DES/ECB/PKCS5Padding";

/**
 * DES 解密
 *
 * @param data 密文
 * @param key  密钥
 * @return 原始数据
 */
private static byte[] decryptDES(byte[] data, byte[] key) throws Exception {
    // 获取 DES Key
    Key secretKey = getKey(key, DES_ALGORITHM);
    // 通过标准转换获取 Cipher 对象, 由该对象完成实际的加密操作
    Cipher cipher = Cipher.getInstance(DES_TRANSFORMATION);
    // 通过解密模式、密钥,初始化 Cipher 对象
    cipher.init(Cipher.DECRYPT_MODE, secretKey);
    // 生成原始数据
    return cipher.doFinal(data);
}