数据加密的本质就是对原始信息内容进行混淆和隐藏,使其难于理解,或难遇反向解密,或者利用现有的技术无法在短时间内破解,从而达到保障信息不被泄露的目的。目前在后台架构中,常用的有对称加密算法和飞对称加密算法。

加密应用场景:敏感信息加密,比如,证件号码,银行卡账号,邮件信息,聊天记录cookie信息,私钥保护等等。

常见的对称加密算法有:

常用算法介绍

  • DES: Data Encryption Standard,即数据加密标准,是一种历史比较悠久的加密算法,上世纪70年代就在使用,是由IBM公司设计的算法,并被美国政府于1976年确定为国家标准局确定为联邦资料处理标准,这种加密算法再七八十年代广为流传,但随着计算机硬件的指数级发展,处理能力不断提高,DES 加密算法已经不再安全,早在 1999年,就有组织宣称使用在23小时之内成功破解了,使用64位数据块,这主要是由于DES算法的秘钥太短,只有56位,很容易通过暴力方式破解。目前,DES算法已经被AES算法所取代。

  • 3DES:三重数据加密算法,Triple DES,也叫三级DES。3DES 不算是一种新的加密算法,它相当于在DES加密算法之上做的增强。由于DES算法本身的短秘钥容易被暴力破解,所以3DES通过增加DES的秘钥长度,使用3个56位的秘钥对数据进行加密,从而能够增强安全性,延缓暴力破解的可能。但加密效率会更慢,3DES被认为是DES向AES发展的过度算法。

  • Blowfish:Blowfish 是 Bruce Schneider 于1993 年设计的。Blowfish 的采用变长1到448位的变长秘钥,给用户比较大的灵活性,相比于DES算法在保证安全性的公式,有很好的加密效率。Bruce Schneider 发明之初一方面是为了摒弃DES算法的老化,一方面是为了缓解加密算法的垄断,因为当时大部分的加密算法都被商业机构或者政府所掌握,有专利保护,所以发展受限,而该算法该算法是完全可以免费的,任何人都可以试用,所以Blowfish 算法有很广泛的应用。

  • AES:Advanced Encryption Standard 即高级加密标准,高级加密标准由美国国家标准与技术研究院(NIST)在2001年提出,ASE算法是目前相对较新,也是目前最流行的对称加密算法,速度更快,安全性更好,被认为是 DES 的替代者。AES 使用128位的加密块,使用128,192和256位长度的秘钥,都是字节的倍数,如果数据块或秘钥长度不足,算法会补齐所以易于软件和硬件的实现。而且某些处理器支持AES硬件加速,比如 Intel 推出的 AES-NI 指令集( Advanced Encryption Standard New Instructions),是专门用户针对AES加密算法硬件CPU指令集,可以让AES加解密速度大幅提升,目前AMD 和 SPARC 的 CPU 也支持AES 硬件加速,查看CPU是否支持AES指令,可以通过命令查看,如: 出现 aes 字样,表示支持。

    # grep -o aes -m1 -i /proc/cpuinfo
    aes
    

    参考:https://yq.aliyun.com/articles/64343

查看Java是否支持AES-NI加速,UseAESIntrinsics 位 true 表示支持。

$ java -XX:+PrintFlagsFinal -version | grep -i aes
bool UseAES = true {product}
bool UseAESIntrinsics = true {product}
java version "1.8.0_141"
Java(TM) SE Runtime Environment (build 1.8.0_141-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, mixed mode)

UseAES 和 UseAESIntrinsics 的介绍可以参考:http://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html。http://aub.iteye.com/blog/1133494

在互联网后台技术架构中,多数是以短文本的加密解密为主,我们测试一下对于相同的内容,不同加密算法的性能:

测试环境为:MacBook Pro,Intel CPU 2.6Ghz(支持AES-NI指令集),内存 16G。 系统环境:JDK 1.8,OSX 10.12.5

测试代码如下:

package com.github.coderxing.book.code.chapter6;

import java.security.Key;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;

/**
 * 常用对称加密算法性能测试
 */
public class CipherTest {

    private static final int LOOP = 50000;

    public static String ALGORITHM_DES = "DES";
    public static String ALGORITHM_3DES = "DESede"; // 3DES
    public static String ALGORITHM_BLOWFISH = "Blowfish";
    public static String ALGORITHM_AES = "AES";

    public static Key keyGenerator(String algorithm, int n) throws Exception {
        KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);
        keyGenerator.init(n);
        return keyGenerator.generateKey();
    }

    // 加密方法
    public static byte[] encrypt(Key key, String text, String algorithm) throws Exception {
        // ECB是分组模式,PKCS5Padding 是补全策略
        Cipher cipher = Cipher.getInstance(algorithm + "/ECB/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, key);
        return cipher.doFinal(text.getBytes());
    }

    // 解密方法
    public static byte[] decrypt(Key key, byte[] data, String algorithm) throws Exception {
        Cipher cipher = Cipher.getInstance(algorithm + "/ECB/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, key);
        return cipher.doFinal(data);
    }

    // 测试加密速度
    public static void testEncrypt(String text, String algorithm, int bit) throws Exception {
        Key key = keyGenerator(algorithm, bit);
        long startTs = System.currentTimeMillis();

        for (int i = 0; i < LOOP; i++) {
            encrypt(key, text, algorithm);
        }
        long endTs = System.currentTimeMillis();
        System.out.println(algorithm + " (" + bit + "位秘钥) 加密平均耗时 " + ((endTs - startTs) / (double) LOOP) + " ms");
    }

    public static void testDecrypt(String text, String algorithm, int bit) throws Exception {
        Key key = keyGenerator(algorithm, bit);
        long startTs = System.currentTimeMillis();

        byte[] encrypted = encrypt(key, text, algorithm);

        for (int i = 0; i < LOOP; i++) {
            decrypt(key, encrypted, algorithm);
        }
        long endTs = System.currentTimeMillis();
        System.out.println(algorithm + " (" + bit + "位秘钥) 解密平均耗时 " + ((endTs - startTs) / (double) LOOP) + " ms");
    }

    public static void main(String[] args) throws Exception {
        String passwordText = "这是我的信用卡账号 5555 5555 5555 5555";

        // DES 算法仅支持56位定长秘钥
        testEncrypt(passwordText, ALGORITHM_DES, 56);
        testDecrypt(passwordText, ALGORITHM_DES, 56);

        // 3DES 算法仅支持112位和168位秘钥
        testEncrypt(passwordText, ALGORITHM_3DES, 112);
        testDecrypt(passwordText, ALGORITHM_3DES, 112);

        testEncrypt(passwordText, ALGORITHM_3DES, 168);
        testDecrypt(passwordText, ALGORITHM_3DES, 168);

        // Blowfish 算法支持32 到 448位的变长秘钥
        testEncrypt(passwordText, ALGORITHM_BLOWFISH, 32);
        testDecrypt(passwordText, ALGORITHM_BLOWFISH, 32);

        testEncrypt(passwordText, ALGORITHM_BLOWFISH, 256);
        testDecrypt(passwordText, ALGORITHM_BLOWFISH, 256);

        testEncrypt(passwordText, ALGORITHM_BLOWFISH, 448);
        testDecrypt(passwordText, ALGORITHM_BLOWFISH, 448);

        // AES 算法支持 128,192,256 三种定长秘钥
        testEncrypt(passwordText, ALGORITHM_AES, 128);
        testDecrypt(passwordText, ALGORITHM_AES, 128);

        testEncrypt(passwordText, ALGORITHM_AES, 192);
        testDecrypt(passwordText, ALGORITHM_AES, 192);

        testEncrypt(passwordText, ALGORITHM_AES, 256);
        testDecrypt(passwordText, ALGORITHM_AES, 256);

    }
}

代码中分别对各种算法的加解密过程执行 50000,取平均执行时间,耗时结果如下:

DES (56位秘钥) 加密平均耗时 0.0081 ms DES (56位秘钥) 解密平均耗时 0.0062 ms DESede (112位秘钥) 加密平均耗时 0.00972 ms DESede (112位秘钥) 解密平均耗时 0.0082 ms DESede (168位秘钥) 加密平均耗时 0.01074 ms DESede (168位秘钥) 解密平均耗时 0.00782 ms Blowfish (32位秘钥) 加密平均耗时 0.04542 ms Blowfish (32位秘钥) 解密平均耗时 0.04456 ms Blowfish (256位秘钥) 加密平均耗时 0.04524 ms Blowfish (256位秘钥) 解密平均耗时 0.04528 ms Blowfish (448位秘钥) 加密平均耗时 0.04552 ms Blowfish (448位秘钥) 解密平均耗时 0.04522 ms AES (128位秘钥) 加密平均耗时 0.006 ms AES (128位秘钥) 解密平均耗时 0.00552 ms AES (192位秘钥) 加密平均耗时 0.00614 ms AES (192位秘钥) 解密平均耗时 0.00562 ms AES (256位秘钥) 加密平均耗时 0.00784 ms AES (256位秘钥) 解密平均耗时 0.0061 ms

以上结果是在我们的笔记本电脑上的测试结果,具体性能如何还要读者在自己的环境中实测。如果你不知道选择什么对称加密算法,推荐使用AES,无论是在性能还是在安全性上都有保障。

注意: 由于美国对软件出口的限制,默认情况下,使用上诉算法的秘钥不能找过128位,如果超过,就会抛出“Java.security.InvalidKeyException: Illegal key size or default paramet” 异常。 Oracle在其官方网站上提供了无政策限制权限文件(Unlimited Strength Jurisdiction Policy Files),需要手动下载,并体替换的掉 替换 <JDK安装目录>/jre/lib/security/ 下面的local_policy.jar和US_export_policy.jar 两个文件。 java8 对应的下载地址为:http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html。

算法分组

另外,再选择算法之后,还要选择分组模式,算法是基于固定长度(比如AES的加密数据块的大小是128位)的内容进行加密,如果要加密的数据超过了算法支持的数据块长度,就需要讲数据进行切分,分别进行加密,如果切分后不是分块长度的整数,对于。以AES算法为例,有 ECB, CBC等模式。

ECB(Electronic codebook,电子密码本)是一种基础模式,其在加解密的时候,讲原始数据按照加密算的的块大小分成若干的组,如果不足则补全,再对每个组单独进行加密解密,其过程如下图所示:

(https://zh.wikipedia.org/wiki/分组密码工作模式) 图片来自维基百科。

该模式的缺点是同样的明文快加密后会形成相同的密文块,明文块和密文块之间有明显的一一对应关系,每一块之间相互独立,互补干扰,不能保证数据的完整性,为数据篡改提供了可能,攻击者可以已知的明文来构建确定的密文,而且建立一套明文密文字典对照表,通过重放,可以对信息片段进行篡改,或者通过反查翻译明文信息。维基百科上举过一个例子(这里加引用):““梦幻之星在线:蓝色脉冲”在线电子游戏使用ECB模式的Blowfish密码。在密钥交换系统被破解而产生更简单的破解方式前,作弊者重复通过发送加密的“杀死怪物”消息包以非法的快速增加经验值”(参考https://zh.wikipedia.org/wiki/分组密码工作模式) 所以,相比于其他模式,这种模式速度快,但是所以不能很好的隐藏数据,数据保护程度不够严格,实际项目中不建议使用。

CBC模式(CBC,Cipher-block chaining)是ECB模式的改进,这种模式是应用最广的一种模式,在该模式中,每一个明文数据块再加密前,都要和前一个密文数据块进行抑或计算,然后在讲计算结果进行加密计算,这样每一位的改变都会造成整个密文的改变,这样就使得数据片段之间联系起来,起到了防止篡改的作用。第一块明文没有前置密文,所以需要提供个预设值和它进行抑或计算,这个值被称作“初始”向量,CBC模式的计算过程如下图(来自维基百科):

(https://zh.wikipedia.org/wiki/%E5%88%86%E7%BB%84%E5%AF%86%E7%A0%81%E5%B7%A5%E4%BD%9C%E6%A8%A1%E5%BC%8F)

图片来自维基百科。

另外,为了安全期间,在key不变的情况下(有些情况下,key不是由业务团队管理,key统一由安全团队分配,不能随意创建,比如一个业务应用只能有一个key),初始向量最好使用随机值,每次加密的结果都不一样,否则也会出现ECB同样的问题,如果初始向量固定的话,固定的明文输出固定的密文,也会为攻击者提提供构建明文密文对照表的可能,尤其是针对首个分组块的预测。所以,建议加解密双方约定好,使用相同的方式创建随机初始向量,比如按照用户维度,每个用户产生一个随机初始向量,或者时间维度,一段时间内使用一个初始向量,或者一个文件一个唯一随机向量,或者按照回话ID生产,确保回话内唯一等等, 从而降低初始向量的预测性,为了加强安全性,秘钥和随机向量最好分开存储,比如秘钥又安全团队管理,随机向量由业务团队负责存储管理。


import java.security.Key;
import java.security.SecureRandom;
import java.util.Base64;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/**
 * 常用对称加密算法性能测试
 *
 * 注意:如果抛出 “Java.security.InvalidKeyException: Illegal key size or default
 * paramet 异常。” 请到 Oracle 官网下载无政策限制权限文件(Unlimited Strength Jurisdiction Policy
 * Files)
 *
 * Java8
 * 对应的下载地址:http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html
 *
 * @author chenghao
 *
 */
public class CipherAES_CBC {

    public static Key genKey(String key) throws Exception {
        SecretKeySpec secretKey = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
        return secretKey;
    }

    // 生产随机初始向量
    public static IvParameterSpec genRandomIV() throws Exception {
        byte[] randomBytes = new byte[16];
        SecureRandom r = new SecureRandom();
        r.nextBytes(randomBytes);
        IvParameterSpec iv = new IvParameterSpec(randomBytes);
        return iv;
    }

    // 加密方法
    public static byte[] encrypt(Key key, String text, IvParameterSpec iv) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, key, iv);
        return cipher.doFinal(text.getBytes());
    }

    // 解密方法
    public static byte[] decrypt(Key key, byte[] data, IvParameterSpec iv) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, key, iv);
        return cipher.doFinal(data);
    }

    public static void main(String[] args) throws Exception {
        // 明文
        String text = "行用卡账号:4567 8910 1112 1314";

        // 128位16字节的秘钥,AES 只支持 128 192 和 256 位的三种秘钥,否则会报错
        String keyStr = "r3pNS{>Zpk-t=h54";
        Key key = genKey(keyStr);

        byte[] randomBytes = new byte[16];
        SecureRandom r = new SecureRandom();
        r.nextBytes(randomBytes);

        IvParameterSpec iv = new IvParameterSpec(randomBytes);

        // 加密
        byte[] encrypted = encrypt(key, text, iv);

        // 解密
        byte[] decrypted = decrypt(key, encrypted, iv);

        System.out.println("秘钥 : " + keyStr);
        System.out.println("初始向量(base64编码): " + Base64.encodeBase64String(iv.getIV()));
        System.out.println("加密后密文(base64编码): " + Base64.encodeBase64String(encrypted));
        System.out.println("解密后明文:" + new String(decrypted));

    }
}

第一次执行结果:

秘钥 : r3pNS{>Zpk-t=h54
初始向量(base64编码): zo7q8GArDVXdV5XvorDwuw==
加密后密文(base64编码): vIso8PhszZ7Fi6wBsSs9RsKVb9e3np543nuVQ2MSricJ+4LJM4rlV8rByK0C1l0m
解密后明文:行用卡账号:4567 8910 1112 1314

第二次执行结果:

秘钥 : r3pNS{>Zpk-t=h54
初始向量(base64编码): wnfY45w8tGxp5ptXobCd5g==
加密后密文(base64编码): cy26/wRva2ieEG/kfqpr46PrOh1jJ+dCtXxLbh2A4UaBln/B8ateFyvLGotFYXyX
解密后明文:行用卡账号:4567 8910 1112 1314

可见,每次加密结果都不一样,这就有效防止了信息字典攻击的威胁。

results matching ""

    No results matching ""