AES加密笔记——Windows&Linux

最近由于业务需求,需要给Kafka内的报文进行加密。Kafka的上游与下游都是我们自己的系统,分析过业务场景后,决定使用对称加密算法。

对称加密算法

对称加密(也叫私钥加密)指加密和解密使用相同密钥的加密算法。在大多数的对称算法中,加密密钥和解密密钥是相同的,所以也称这种加密算法为秘密密钥算法或单密钥算法。

优点:对称加密算法的特点是算法公开、计算量小、加密速度快、加密效率高。

缺点:交易双方都使用同样钥匙,安全性得不到保证。每对用户每次使用对称加密算法时,都需要使用其他人不知道的惟一钥匙,这会使得发收信双方所拥有的钥匙数量呈几何级数增长,密钥管理成为用户的负担。对称加密算法在分布式网络系统上使用较为困难,主要是因为密钥管理困难,使用成本较高。

非对称加密算法

非对称加密算法需要两个密钥:公开密钥(publickey:简称公钥)和私有密钥(privatekey:简称私钥)。公钥与私钥是一对,如果用公钥对数据进行加密,只有用对应的私钥才能解密。

优点:算法强度复杂、安全性强。相比于对称秘钥只有一个秘钥而言,非对称密钥体制有两种密钥,其中一个是公开的,这样就可以不需要像对称密码那样传输对方的密钥了。这样安全性就大了很多。

缺点:但是由于其算法复杂,而使得加密解密速度没有对称加密解密的速度快。

什么是AES?

AES算是比较基础的对称加密算法,原理简单。

高级加密标准(AES,Advanced Encryption Standard)为最常见的对称加密算法,AES最常见的有3种方案,分别是AES-128、AES-192和AES-256,它们的区别在于密钥长度不同,AES-128的密钥长度为16bytes(128bit/8),后两者分别为24bytes和32bytes。密钥越长,安全强度越高,但伴随运算轮数的增加,带来的运算开销就会更大。

AES算法在加密过程中分为四步:

  • 字节代换
  • 行移位
  • 列混合
  • 轮密钥加

字节代换

AES的字节代换其实就是一个简单的查表操作。AES定义了一个S盒和一个逆S盒。

行移位

行移位是一个简单的左循环移位操作。当密钥长度为128比特时,状态矩阵的第0行左移0字节,第1行左移1字节,第2行左移2字节,第3行左移3字节。

列混合

列混合变换是通过矩阵相乘来实现的,经行移位后的状态矩阵与固定的矩阵相乘,得到混淆后的状态矩阵。

轮密钥加

轮密钥加是将128位轮密钥同状态矩阵中的数据进行逐位异或操作。

AES128具体实现

Windows上的首次尝试

import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

public class Encrypt1 {
    private final String password;
    private final KeyGenerator kgen;
    private final SecretKey secretKey;
    private final byte[] enCodeFormat;
    private final SecretKeySpec key;
    private Cipher cipher;

    public Encrypt1(String password) throws NoSuchAlgorithmException, NoSuchPaddingException {
        this.password = password;

        kgen = KeyGenerator.getInstance("AES");

        kgen.init(128, new SecureRandom(password.getBytes()));

        secretKey = kgen.generateKey();

        enCodeFormat = secretKey.getEncoded();

        key = new SecretKeySpec(enCodeFormat, "AES");

        cipher = Cipher.getInstance("AES");

    }
    /**
     * AES加密字符串
     *
     * @param content 加密内容
     *
     * @return 密文
     */
    public byte[] encrypt(String content) {
        try {
            byte[] byteContent = content.getBytes("utf-8");

            cipher.init(Cipher.ENCRYPT_MODE, key);

            byte[] result = cipher.doFinal(byteContent);

            return result;

        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (IllegalBlockSizeException e) {
            e.printStackTrace();
        } catch (BadPaddingException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 解密AES加密过的字符串
     *
     * @param content 解密密文
     *
     * @return 明文
     */
    public byte[] decrypt(byte[] content) {
        try {
            cipher.init(Cipher.DECRYPT_MODE, key);
            byte[] result = cipher.doFinal(content);
            return result; 

        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (IllegalBlockSizeException e) {
            e.printStackTrace();
        } catch (BadPaddingException e) {
            e.printStackTrace();
        }
        return null;
    }


    public static void main(String[] args) {

        String content = "麻溜地撸个加密程序";
        String password = "123456";
        Encrypt1 e1, e2;

        try {
            e1 = new Encrypt1(password);
            e2 = new Encrypt1(password);

            System.out.println("加密之前:" + content);

            // 加密
            byte[] encrypt = e1.encrypt(content);
            System.out.println("加密后的内容:" + new String(encrypt));

            // 解密
            byte[] decrypt = e2.decrypt(encrypt);
            System.out.println("解密后的内容:" + new String(decrypt));

        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
        }
    }
}

最初我在Windows上的电脑测试这段代码时还很好用。但是当我将相关代码部署到Linux服务器上时,解密出现了问题,在解密时抛出异常。类似下图:


解密报错.png

报错指明是由于错误的秘钥导致,但是经过详细比较,我在加密和解密时的秘钥都是采用同一个,不可能是由于使用秘钥不同导致。由于业务流程上是Windows系统上的程序充当Producer对报文进行加密然后插入Kafka消息队列,Linux上的程序作为Consumer进行消费并对之前的密文解密。第一直觉误认为在进行插入过程中byte数组产生了问题,于是Producer改进为转为16进制进行插入,在Consumer进行消费时进行检查。非常奇妙的是,消费者拿到的加密报文与生产者产生的报文完完全全相同,而且将消费者拿到的报文复制到最初测试的程序中,可以正常解密。于是可以大致断定为是环境导致的解密失败。为了确认我用相同字符串在Windows和Linux环境下用相同秘钥进行了加密,对比加密后的字符串。根据AES加密算法的原理,如果使用相同秘钥,同一个字符串加密后的密文应该是相同的。但是在上述不同操作系统之间,加密后的内容是不同的。

错误原因分析:

SecureRandom 实现随操作系统本身的內部状态不同而不同,除非调用方在调用 getInstance 方法之后又调用了 setSeed 方法;该实现在 windows 上每次生成的 key 都相同,但是在 solaris 或部分 linux 系统上则不同。
真相大白后我们进行Linux版本修正。

Linux版本的AES128实现

import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

public class Encrypt2 {
    private final String password;
    private final KeyGenerator kgen;
    private final SecretKey secretKey;
    private final byte[] enCodeFormat;
    private final SecretKeySpec key;
    private final Cipher cipher;
    private final SecureRandom secureRandom;


    public Encrypt2(String password) throws NoSuchAlgorithmException, NoSuchPaddingException {
        this.password = password;

        kgen = KeyGenerator.getInstance("AES");

        secureRandom = SecureRandom.getInstance("SHA1PRNG");

        secureRandom.setSeed(password.getBytes());

        kgen.init(128, secureRandom);

        secretKey = kgen.generateKey();

        enCodeFormat = secretKey.getEncoded();

        key = new SecretKeySpec(enCodeFormat, "AES");

        cipher = Cipher.getInstance("AES");

    }
    /**
     * AES加密字符串
     *
     * @param content 加密内容
     *
     * @return 密文
     */
    public byte[] encrypt(String content) {
        try {
            byte[] byteContent = content.getBytes("utf-8");

            cipher.init(Cipher.ENCRYPT_MODE, key);

            byte[] result = cipher.doFinal(byteContent);

            return result;

        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (IllegalBlockSizeException e) {
            e.printStackTrace();
        } catch (BadPaddingException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 解密AES加密过的字符串
     *
     * @param content 解密密文
     *
     * @return 明文
     */
    public byte[] decrypt(byte[] content) {
        try {
            cipher.init(Cipher.DECRYPT_MODE, key);// 初始化为解密模式的密码器
            byte[] result = cipher.doFinal(content);
            return result; // 明文

        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (IllegalBlockSizeException e) {
            e.printStackTrace();
        } catch (BadPaddingException e) {
            e.printStackTrace();
        }
        return null;
    }


    public static void main(String[] args) {

        String content = "麻溜地撸个加密程序";
        String password = "123456";
        Encrypt2 e1, e2;

        try {
            e1 = new Encrypt2(password);
            e2 = new Encrypt2(password);

            System.out.println("加密之前:" + content);

            // 加密
            byte[] encrypt = e1.encrypt(content);
            System.out.println("加密后的内容:" + new String(encrypt));

            // 解密
            byte[] decrypt = e2.decrypt(encrypt);
            System.out.println("解密后的内容:" + new String(decrypt));

        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
        }
    }
}

经过测试,此加密工具在Windows和Linux系统中均表现良好。对于多系统串行加密解密也没有问题。

多线程进行加密解密试验

简单地对报文进行加密解密是不能满足实际情况的,该方法是否线程安全是个还需要确定的事情。对多线程进行了如下测试。加密解密方法不变,测试方法如下:

    public static void main(String[] args) {

        String content1 = "麻溜地撸个加密程序";
        String content2 = "苟利国家生死以";
        String content3 = "岂因福祸避趋之";
        String password = "123456";
        Encrypt3 e1, e2;

        try {
            e1 = new Encrypt3(password);
            e2 = new Encrypt3(password);
            Thread thread1, thread2, thread3;

            thread1 = new Thread(() -> {
                int i = 0;
                while (true) {
                    System.out.println("线程1加密之前:" + content1 + i);

                    // 加密
                    byte[] encrypt = e1.encrypt(content1 + i++);
                    System.out.println("线程1加密后的内容:" + new String(encrypt));

                    // 解密
                    byte[] decrypt = e1.decrypt(encrypt);
                    System.out.println("线程1解密后的内容:" + new String(decrypt));
                }
            });

            thread1.start();

            thread2 = new Thread(() -> {
                int i = 0;
                while (true) {
                    System.out.println("线程2加密之前:" + content2 + i);

                    // 加密
                    byte[] encrypt = e1.encrypt(content2 + i++);
                    System.out.println("线程2加密后的内容:" + new String(encrypt));

                    // 解密
                    byte[] decrypt = e1.decrypt(encrypt);
                    System.out.println("线程2解密后的内容:" + new String(decrypt));
                }
            });

            thread2.start();

            thread3 = new Thread(() -> {
                int i = 0;
                while (true) {
                    System.out.println("线程3加密之前:" + content3 + i);

                    // 加密
                    byte[] encrypt = e1.encrypt(content3 + i++);
                    System.out.println("线程3加密后的内容:" + new String(encrypt));

                    // 解密
                    byte[] decrypt = e1.decrypt(encrypt);
                    System.out.println("线程3解密后的内容:" + new String(decrypt));
                }
            });

            thread3.start();



        } catch (NoSuchAlgorithmException e) {
            System.out.println(e.getMessage());
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            System.out.println(e.getMessage());
            e.printStackTrace();
        }
    }

测试结果果然有幺蛾子:


测试结果.png

每次启动都会最终只有一个线程留下来进行加密解密。其他两个不知道为何就消失了。
经过排查,确定是Cipher不是线程安全的。解决方法有两种,在加密和解密方法中给Cipher加锁,或者在每次使用Cipher时新实例化一个对象。我们选择后一种方式。如果加密解密不是很频繁可以使用第一种加锁方式。但是当加密解密密度很高时,使用第一种方式会影响性能。第二种方式会增加一定的内存使用,但是得益于Java8的gc内存回收做的很好,我们不用担心由此带来的内存增加问题。所以我们用空间换时间。

线程安全加密实例

import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

public class Encrypt4 {
    private final String password;
    private final KeyGenerator kgen;
    private final SecretKey secretKey;
    private final byte[] enCodeFormat;
    private final SecretKeySpec key;
    private final SecureRandom secureRandom;


    public Encrypt4(String password) throws NoSuchAlgorithmException, NoSuchPaddingException {
        this.password = password;

        kgen = KeyGenerator.getInstance("AES");

        secureRandom = SecureRandom.getInstance("SHA1PRNG");

        secureRandom.setSeed(password.getBytes());

        kgen.init(128, secureRandom);

        secretKey = kgen.generateKey();

        enCodeFormat = secretKey.getEncoded();

        key = new SecretKeySpec(enCodeFormat, "AES");

    }
    /**
     * AES加密字符串
     *
     * @param content 加密内容
     *
     * @return 密文
     */
    public byte[] encrypt(String content) {
        try {
            byte[] byteContent = content.getBytes("utf-8");

            Cipher cipher = Cipher.getInstance("AES");

            cipher.init(Cipher.ENCRYPT_MODE, key);

            byte[] result = cipher.doFinal(byteContent);

            return result;

        } catch (UnsupportedEncodingException e) {
            System.out.println(e.getMessage());
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            System.out.println(e.getMessage());
            e.printStackTrace();
        } catch (IllegalBlockSizeException e) {
            System.out.println(e.getMessage());
            e.printStackTrace();
        } catch (BadPaddingException e) {
            System.out.println(e.getMessage());
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 解密AES加密过的字符串
     *
     * @param content 解密密文
     *
     * @return 明文
     */
    public byte[] decrypt(byte[] content) {
        try {
            Cipher cipher = Cipher.getInstance("AES");
            cipher.init(Cipher.DECRYPT_MODE, key);// 初始化为解密模式的密码器
            byte[] result = cipher.doFinal(content);
            return result; // 明文

        } catch (InvalidKeyException e) {
            System.out.println(e.getMessage());
            e.printStackTrace();
        } catch (IllegalBlockSizeException e) {
            System.out.println(e.getMessage());
            e.printStackTrace();
        } catch (BadPaddingException e) {
            System.out.println(e.getMessage());
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return null;
    }


    public static void main(String[] args) {

        String content1 = "麻溜地撸个加密程序";
        String content2 = "苟利国家生死以";
        String content3 = "岂因福祸避趋之";
        String password = "123456";
        Encrypt4 e1, e2;

        try {
            e1 = new Encrypt4(password);
            e2 = new Encrypt4(password);
            Thread thread1, thread2, thread3;

            thread1 = new Thread(() -> {
                int i = 0;
                while (true) {
                    System.out.println("线程1加密之前:" + content1 + i);

                    // 加密
                    byte[] encrypt = e1.encrypt(content1 + i++);
                    System.out.println("线程1加密后的内容:" + new String(encrypt));

                    // 解密
                    byte[] decrypt = e1.decrypt(encrypt);
                    System.out.println("线程1解密后的内容:" + new String(decrypt));
                }
            });

            thread1.start();

            thread2 = new Thread(() -> {
                int i = 0;
                while (true) {
                    System.out.println("线程2加密之前:" + content2 + i);

                    // 加密
                    byte[] encrypt = e1.encrypt(content2 + i++);
                    System.out.println("线程2加密后的内容:" + new String(encrypt));

                    // 解密
                    byte[] decrypt = e1.decrypt(encrypt);
                    System.out.println("线程2解密后的内容:" + new String(decrypt));
                }
            });

            thread2.start();

            thread3 = new Thread(() -> {
                int i = 0;
                while (true) {
                    System.out.println("线程3加密之前:" + content3 + i);

                    // 加密
                    byte[] encrypt = e1.encrypt(content3 + i++);
                    System.out.println("线程3加密后的内容:" + new String(encrypt));

                    // 解密
                    byte[] decrypt = e1.decrypt(encrypt);
                    System.out.println("线程3解密后的内容:" + new String(decrypt));
                }
            });

            thread3.start();



        } catch (NoSuchAlgorithmException e) {
            System.out.println(e.getMessage());
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            System.out.println(e.getMessage());
            e.printStackTrace();
        }
    }
}

写在最后的话

AES加密算法非常简单也非常常见,本文只是写一个备忘笔记。特别感谢在我学习过程中对我进行无私帮助的耿腾

你可能感兴趣的:(AES加密笔记——Windows&Linux)