Java编码与加解密实现


一、编码压缩

1. Base64

Base64 编码是一种将二进制数据转换为文本字符串的编码方式,它将原始的二进制数据按照一定的规则转换为一串由 ASCII 字符组成的字符串。

Base64 编码主要用于在网络传输或者存储数据时,因为在一些网络传输或者存储的环境中,只能够传输或存储 ASCII 字符,无法直接传输或存储二进制数据,因此需要将二进制数据转换后进行传输或存储,同时 Base64 编码也可以对数据进行简单的加密和解密。

在实际应用中对于 Java 对象一般采用对象序列化或 Json 实现转化,然后再通过二次的 Base64 编码后执行网络通讯传输等操作。

(1) sun.misc

通过 sun.misc 包下的 Base64 类库实现快捷的 Base64 数据编码。

public void demo1() throws IOException {
    String msg = "The plain text";
    String result = new sun.misc.BASE64Encoder().encode(msg.getBytes());
    byte[] bytes = new sun.misc.BASE64Decoder().decodeBuffer(result);
    System.out.println("Origin: " + MESSAGE);
    System.out.println("After encode: " + result);
    System.out.println("After decode: " + new String(bytes));
}
(2) java.util.Base64

通过 java.util.Base64 类库实现 Base64 数据编码。

public void demo2() {
    String msg = "The plain text";
    // 基本编码
    String encodeStr = java.util.Base64.getEncoder().encodeToString(msg.getBytes());
    byte[] decodedBytes = java.util.Base64.getDecoder().decode(encodeStr);
    System.out.println("Base64 编码(基本): " + encodeStr);
    System.out.println("Base64 解码(基本): " + new String(decodedBytes));

    // URL 编码
    encodeStr = java.util.Base64.getUrlEncoder().encodeToString(msg.getBytes());
    decodedBytes = java.util.Base64.getUrlDecoder().decode(encodeStr);
    System.out.println("Base64 编码(URL): " + encodeStr);
    System.out.println("Base64 解码(URL): " + new String(decodedBytes));

    // MIME 编码
    StringBuilder builder = new StringBuilder();
    for (int i = 0; i < 10; ++i) {
        builder.append(UUID.randomUUID());
    }
    byte[] mimeBytes = builder.toString().getBytes(StandardCharsets.UTF_8);
    String mimeEncodedString = java.util.Base64.getMimeEncoder().encodeToString(mimeBytes);
    System.out.println("Base64 编码(MIME): " + mimeEncodedString);
}
(3) apache.commons.Base64

上面两种 Base64 编码都是 JDK 系统自带,当然也可依赖第三方框架实现,如下为通过 Apache commons 依赖实现。

<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.15</version>
</dependency>

引入依赖后开箱即用,相应的示例代码如下:

public void demo3() {
    String msg = "The plain text";
    String result = Base64.encodeBase64String(msg.getBytes());
    byte[] bytes = Base64.decodeBase64(result);
    System.out.println("Origin: " + MESSAGE);
    System.out.println("Base64 编码: " + result);
    System.out.println("Base64 解码: " + new String(bytes));
}

2. GZIP

通过将数据压缩为 GZIP 格式可在一定程度上的减小数据大小。

public static byte[] compress(byte[] bytes) {
    try (
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            GZIPOutputStream gzip = new GZIPOutputStream(out)
    ) {
        gzip.write(bytes);
        // 需要先关闭,否则会报 Unexpected end of ZLIB input stream
        gzip.close();
        return out.toByteArray();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

相对应的,可将压缩后的 GZIP 格式数据解压还原为普通字节数据,示例如下:

public static byte[] uncompress(byte[] bytes) {
    try (
            ByteArrayInputStream in = new ByteArrayInputStream(bytes);
            GZIPInputStream unzip = new GZIPInputStream(in);
            ByteArrayOutputStream out = new ByteArrayOutputStream()
    ) {
        int n;
        byte[] buffer = new byte[1024];
        while ((n = unzip.read(buffer)) >= 0) {
            out.write(buffer, 0, n);
        }
        return out.toByteArray();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

3. Deflater

DeflaterJava 标准库提供的压缩/解压缩算法,基于 DEFLATE 算法实现,可以通过设置压缩级别来控制压缩率和压缩速度。

(1) 压缩

Deflater 在初始化时可设置压缩级别,取值 0~9,数值越大压缩率越高效率越慢。

初始化第二个参数 nowap 用于指定是否使用 ZLIB 头和尾来解压缩数据,默认值为 false

public static byte[] compress(byte[] data) throws IOException {
    Deflater deflater = new Deflater(9, true);
    deflater.setInput(data);
    deflater.finish();
    try (ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length)) {
        byte[] buffer = new byte[1024];
        while (!deflater.finished()) {
            int count = deflater.deflate(buffer);
            bos.write(buffer, 0, count);
        }
        return bos.toByteArray();
    } catch (Exception e) {
        throw new IOException(e);
    }
}
(2) 解压缩

Inflater 初始化中的 nowap 参数需要与 Deflater 一致,否则将会抛出异常。

public static byte[] decompress(byte[] compressedData) throws IOException {
    Inflater inflater = new Inflater(true);
    inflater.setInput(compressedData);
    try (ByteArrayOutputStream bos = new ByteArrayOutputStream(compressedData.length)) {
        byte[] buffer = new byte[1024];
        while (!inflater.finished()) {
            int count = inflater.inflate(buffer);
            bos.write(buffer, 0, count);
        }
        return bos.toByteArray();
    } catch (Exception e) {
        throw new IOException(e);
    }
}

二、加密算法

1. 哈希算法

哈希算法即通过一定的算法规则为目标数据生成一串指定的哈希编码,最常见的即 MD5SHA-256,在许多网站下载文件时通常会标注文件对应的 MD5 等信息,可供检验文件是否被恶意篡改。

其核心在于相同数据文件经过哈希算法得到的哈希值一定是固定的,当应用发行商发布文件时同时公布其文件的哈希值,用户下载后可通过哈希加密工具执行计算得到一串哈希值,若与发行商的不一致则代表文件数据可能被人为篡改不安全。

2. 对称加密

对称加密即传统意义上的数据加密,对称加密中的对称即代表加密和解密的密钥是一致,即存在一份密钥同时用于数据加密和解密,如常见的 AESDES 算法等等。

对称加密的缺点在于数据虽然实现了加密,但如何让加解密双方都安全的获取密钥成了难题。由于对称的关系,一旦密钥遭到泄露,安全性即土崩瓦解。

3. 非对称加密

非对称加密相较于对称加密的提升在于存在两套密钥:公开密钥 (public) 和私有密钥 (private),公钥用于加密,私钥用于解密。

假设存在两个人 User-AUser-B,各自生成了其对应的公钥和私钥,二人将自己的公钥分别给对方。即 User-A 拥有 User-B 的公钥,而 User-B 拥有 User-A 的公钥,二者的私钥仍由自己保管。当 User-A 要向 User-B 发送数据时,通过 User-B 的公钥进行加密然后发送给 User-B,而 User-B 收到后用自己的私钥解密得到内容。由于公钥仅用于加密数据因此即便泄露也不影响,解密只能由私钥进行,而私钥由自身保管不经有网络传输因此大大提高了安全性。

4. 数字签名

在使用非对称加密时通常会加上数字签名,其效果类似于哈希用于验证数据的完整性。

三、哈希算法

1. MD5

MD5 是最为常用的数据验证方式之一,通过特定算法为目标数据计算生成唯一的标识码,一旦数据遭到恶意篡改通过比对标识码即可快速确认。

Java 中通过 JDK 自带类库 MessageDigest 即可实现 MD5 算法,示例代码如下:

public void demo1() throws Exception {
    String message = "Hello, world!";
    MessageDigest digest = MessageDigest.getInstance("MD5");
    byte[] bytes = digest.digest(data.getBytes())

    StringBuilder builder = new StringBuilder();
    for (byte b : bytes) {
        builder.append(String.format("%02x", b & 0xff));
    }
    System.out.println("MD5: " + builder);
}

2. SHA-256

SHA-256 实现与 MD5 算法类似,将上述 getInstance("MD5") 替换为 getInstance("SHA-256") 即可,这里不再重复介绍。

3. SHA-3

SHA-3 需要依赖第三方工具库,在 Maven 工程引入如下依赖。

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcpkix-jdk15on</artifactId>
    <version>1.61</version>
</dependency>

完成以来导入后即可直接使用,示例如下:

public String SHA3Demo() {
    String message = "Hello, world!";
    byte[] bytes = data.getBytes();
    // 256 表示 SHA-3-256 算法
    SHAKEDigest sha3 = new SHAKEDigest(256);
    sha3.update(bytes, 0, bytes.length);
    byte[] digestBytes = new byte[sha3.getDigestSize()];
    sha3.doFinal(digestBytes, 0);
    String result = new String(Hex.encode(digestBytes));
    System.out.println("SHA-3: " + result);
}

4. SM3算法

除了 JDK 中自带哈希算法,也可使用国密三 (SM3) 实现类似效果。

在使用之前需要先引入相应 Maven 依赖。

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15to18</artifactId>
    <version>1.71</version>
</dependency>

其相对应的使用示例如下:

@Test
public void demo() {
    String origin = "I am original information.";
    byte[] result = getSM3ByCrypto(origin.getBytes());
    System.out.println(Arrays.toString(result));
}

public byte[] getSM3ByCrypto(byte[] data) {
    SM3Digest digest = new SM3Digest();
    digest.update(data, 0, data.length);
    byte[] result = new byte[digest.getDigestSize()];
    digest.doFinal(result, 0);
    return result;
}

四、SM4算法

国密四加密是中国国家密码管理局发布的一种密码算法标准,是一种对称加密算法,相较于旧的国密标准,国密四加密更加安全、高效、易于实现。

1. 依赖导入

在使用之前需要先引入相应 Maven 依赖。

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15to18</artifactId>
    <version>1.71</version>
</dependency>

2. 工具类

新建工具类 SM4Util 定义加解密具体实现。

public class SM4Util {
    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    /**
     * 密钥长度 16 字节,128位
     */
    public static byte[] generateKey(String seed) throws NoSuchAlgorithmException, NoSuchProviderException {
        KeyGenerator kg = KeyGenerator.getInstance("SM4", BouncyCastleProvider.PROVIDER_NAME);
        SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
        if (null != seed && !"".equals(seed)) {
            random.setSeed(seed.getBytes());
        }
        kg.init(128, random);
        return kg.generateKey().getEncoded();
    }

    /**
     * 加密
     */
    public static byte[] encryptByCrypto(String algorithm, byte[] key, byte[] iv, byte[] data) throws Exception {
        return sm4core(algorithm, Cipher.ENCRYPT_MODE, key, iv, data);
    }

    /**
     * 解密
     */
    public static byte[] decryptByCrypto(String algorithm, byte[] key, byte[] iv, byte[] data) throws Exception {
        return sm4core(algorithm, Cipher.DECRYPT_MODE, key, iv, data);
    }

    private static byte[] sm4core(String algorithm, int type, byte[] key, byte[] iv, byte[] data) throws Exception {
        Cipher cipher = Cipher.getInstance(algorithm, BouncyCastleProvider.PROVIDER_NAME);
        Key sm4Key = new SecretKeySpec(key, "SM4");
        if (algorithm.contains("ECB")) {
            cipher.init(type, sm4Key);
        } else {
            IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
            cipher.init(type, sm4Key, ivParameterSpec);
        }

        return cipher.doFinal(data);
    }  
}

3. 示例演示

其中 Key 为密钥,是加密算法中用于对数据进行加密和解密的保密参数。在国密四标准中,其密钥长度为 128 位,即 16 字节。

IV 又称初始向量,是在 CBC 模式下使用的一个随机数,用于增加密码分组加密的强度,一般情况下为 8 个字节 (64位),在每个分组加密前与上一个分组的密文进行异或运算。

public void SM4Demo2() throws Exception {
    String plainText = "I am original information.";
    String ivKey = "ibudai.xyz";                // 初始向量
    byte[] iv = ivKey.getBytes(StandardCharsets.UTF_8);
    String code = "@demo-ibudai.xyz";           // 密钥
    byte[] key = SM4Util.generateKey(code);
    String algorithm = "SM4/ECB/PKCS5PADDING";  // 国密四具体算法

    // 加密
    byte[] result1 = SM4Util.encryptByCrypto(algorithm, key, iv, plainText.getBytes());
    System.out.println(Arrays.toString(result1));

    // 解密
    byte[] result2 = SM4Util.decryptByCrypto(algorithm, key, iv, result1);
    System.out.println(new String(result2));
}

4. 算法选择

国密四算法提供一系列可选参数,如上述的 SM4/ECB/PKCS5PADDING ,其余可选参数如下:

public enum SM4Algorithm {
    ECB_NOPADDING("SM4/ECB/NOPADDING"),
    ECB_PKCS5PADDING("SM4/ECB/PKCS5PADDING"),
    ECB_ISO10126PADDING("SM4/ECB/ISO10126PADDING"),

    CBC_NOPADDING("SM4/CBC/NOPADDING"),
    CBC_PKCS5PADDING("SM4/CBC/PKCS5PADDING"),
    CBC_ISO10126PADDING("SM4/CBC/ISO10126PADDING"),

    CTR_NOPADDING("SM4/CTR/NOPADDING"),
    CTR_PKCS5PADDING("SM4/CTR/PKCS5PADDING"),
    CTR_ISO10126PADDING("SM4/CTR/ISO10126PADDING"),

    CTS_NOPADDING("SM4/CTS/NOPADDING"),
    CTS_PKCS5PADDING("SM4/CTS/PKCS5PADDING"),
    CTS_ISO10126PADDING("SM4/CTS/ISO10126PADDING"),

    PCBC_NOPADDING("SM4/PCBC/NOPADDING"),
    PCBC_PKCS5PADDING("SM4/PCBC/PKCS5PADDING"),
    PCBC_ISO10126PADDING("SM4/PCBC/ISO10126PADDING");

    private String type;
}

五、AES算法

1. 依赖引入

maven 工程中引入下列依赖:

<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.11</version>
</dependency>

2. 工具类

新建 AESUtil 工具类简单封装一下加解密方法。

public class AESUtil {

    /**
     * 加密
     */
    public static String encrypt(String data, String key, String iv) throws Exception {
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        int blockSize = cipher.getBlockSize();
        byte[] dataBytes = data.getBytes();
        int length = dataBytes.length;
        if (length % blockSize != 0) {
            length = length + (blockSize - (length % blockSize));
        }
        byte[] plaintext = new byte[length];
        System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length);
        SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
        IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes());
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
        byte[] encrypted = cipher.doFinal(plaintext);
        return new Base64().encodeToString(encrypted);
    }

    /**
     * 解密
     */
    public static String desEncrypt(String data, String key, String iv) throws Exception {
        byte[] encrypted1 = new Base64().decode(data);
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
        IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes());
        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
        byte[] bytes = cipher.doFinal(encrypted1);
        return new String(bytes);
    }
}

3. 示例演示

注意 ivkey 的长度都必须为 16 位。

public void Encrypt() {
    String data = "123";
    String iv = "1234567890123456";
    String key = "1234567890123456";
    String algoritym = "AES/CBC/NoPadding";

    try {
        String enStr = AESUtil.encrypt(data, key, iv);
        String deStr = AESUtil.desEncrypt(enStr, key, iv).trim();
        System.out.println("Origin:" + data);
        System.out.println("Encode:" + enStr);
        System.out.println("Decode:" + deStr);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

六、分组加密

1. 实现机制

在之前的场景中,无论是通过对称加密还是非对称加密,执行后的数据的都是直观不可读的,在存储时也将导致一个问题:不可模糊查询。

分组加密解决的一大痛点即保证加密的同时仍保留了模糊查询,通过对目标数据的拆分分组后单独加密存储,牺牲一定空间的前提下实现模糊查询的效果。

2. 示例演示

让我们通过一个示例更直观的了解分组加密的实现原理。

假设我需要存储 布袋青年 四个字,在密钥为 123456 的前提经过 AES 加密后得到 U2FsdGVkX19Sj4XRoc3rsbJS7VisiCR2fyUW+7e9gow=,但将该值作为目标数据进行存储时,若输入 布袋 并无法模糊匹配命中,也就导致了无法对加密数据模糊查询。

分组加密的核心在于数据拆词,同样针对 布袋青年 数据,将其拆分为 布袋青年,并将拆分后的数据进行同样的加密得到 U2FsdGVkX1/joWAz2Ti/m1HutDLbWW/oVSE6PyxisNY=U2FsdGVkX1+NBvJIYl2r40mXgNSmUBc8jvZuEn3EUIA=,将得到的两个数值拼接作为 布袋青年 的加密值。此时若需要执行模糊查询,同样以输入 布袋 为例,其条件即为 where data like %encode('布袋')%,即保证数据加密的前提下实现模糊查询。

这样处理当然也存在一定的弊端,拆词需要多次加密相较一次性加密更耗性能,且拼接的结果也更占存储空间,但其实现了加密的前提下仍然能模糊查询。为了达到更好的性能,通常一个数据的进行拆分时单个词组的长度控制在 2 ~ 4 个长度。


文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录