加解密技术基本使用指南(Base64、Hex、AES、SM4、RSA 算法)

加密技术概述

加密技术是对信息进行编码和解码的技术,编码是把原来可读信息(又称明文)译成代码形式(又称密文),其逆过程就是解码(解密),加密技术的要点是加密算法,加密算法可以分为三类:

  • 对称加密,如 AES、SM4(国密)
    • 基本原理:将明文分成N个组,然后使用密钥对各个组进行加密,形成各自的密文,最后把所有的分组密文进行合并,形成最终的密文。
    • 优势:算法公开、计算量小、加密速度快、加密效率高
    • 缺陷:双方都使用同样密钥,安全性得不到保证
  • 非对称加密,如 RSA、git的ssh公钥和私钥
    • 基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端 私钥是用于加密的,公钥是用于解密的。
      • 私钥加密,持有私钥或公钥才可以解密
      • 公钥加密,持有私钥才可解密
    • 优点:安全,难以破解
    • 缺点:算法比较耗时
    • 非对称算法一般是用来传送对称加密算法的密钥
  • 不可逆加密,如 MD5,SHA
    • 基本原理:加密过程中不需要使用密钥,输入明文后由系统直接经过加密算法处理成密文,这种加密后的数据是无法被解密的,无法根据密文推算出明文。

编码器

Base64

原理剖析:跳转网址

Base64 是网络上最常见的用于传输 8Bit字节码的编码方式之一,Base64 就是一种基于64个可打印字符来表示二进制数据的方法。

Base64 一般用于在 HTTP 协议下传输二进制数据,由于 HTTP 协议是文本协议,所以在 HTTP 协议下传输二进制数据需要将二进制数据转换为字符数据。然而直接转换是不行的。因为网络传输只能传输可打印字符。

**可打印字符:**在ASCII码中规定,0~31、127 这 33 个字符属于控制字符,32~126 这 95 个字符属于可打印字符,也就是说网络传输只能传输这 95 个字符,不在这个范围内的字符无法传输。那么该怎么才能传输其他字符呢?其中一种方式就是使用 Base64。

大多数编码都是由字符串转化成二进制的过程,而 Base64 的编码则是从二进制转换为字符串。与常规恰恰相反。

Base64 编码主要用在传输、存储、表示二进制领域,不能算得上加密,只是无法直接看到明文。也可以通过打乱 Base64 编码来进行加密。

中文有多种编码(比如:utf-8、gb2312、gbk等),不同编码对应 Base64 编码结果都不一样。


java.util.Base64 工具类:该类仅由用于获得 Base64 编码方案的编码器和解码器的静态方法组成。

作用:

  • 使用 Base64 里边的编码器对数据进行编码(加密)
  • 使用 Base64 里边的解码器对数据进行解码(解密)

Base64 工具类 静态方法:

// 使用Basic型base64编码方案
static Base64.Encoder getEncoder() 		// 获取加密器(编码器)
static Base64.Decoder getDecoder() 		// 获取解密器(解码器)
    
// 使用MIME型base64编码方案
static Base64.Encoder getMineEncoder() 		// 获取加密器(编码器)
static Base64.Decoder getMineDecoder() 		// 获取解密器(解码器)
static Base64.Encoder getMimeEncoder(int lineLength, byte[] lineSeparator)	// 指定行长度和行分隔符获取加密器
    
// 使用 URL and Filename safe 型base64编码方案
static Base64.Encoder getUrlEncoder() 		// 获取加密器(编码器)
static Base64.Decoder getUrlDecoder() 		// 获取解密器(解码器)

java.util.Base64.Encoder:是Base64的内部类,用于对数据进行加密

Encoder 的成员方法:

String encodeToString(byte[] src) 		// 使用Base64编码方案将指定的字节数组编码为字符串。

java.util.Base64.Decoder:是Base64的内部类,用于对数据进行解密

Decoder 的成员方法:

 byte[] decode(String src) 		// 使用Base64编码方案将Base64编码的字符串解码为新分配的字节数组。

Hex(十六进制)

十六进制,英文名称:Hex number system(简写为 hex)是一种基数为 16 的计数系统,是一种逢 16 进 1 的进位制。

通常由 09,AF 组成,其中:A~F表示 10~15,这些称作十六进制数字。


自定义工具类:EncoderUtils

import java.util.Base64;

public class EncoderUtils {
    
    

    public static final String UTF_8 = "UTF-8";
    public static final String BASE_64 = "base64";
    public static final String HEX = "hex";

    /**
     * base64 编码
     */
    public static String encodeByteToBase64(byte[] byteArray) {
    
    
        return Base64.getEncoder().encodeToString(byteArray);
    }

    /**
     * base64 解码
     */
    public static byte[] decodeBase64ToByte(String base64EncodedString) {
    
    
        return Base64.getDecoder().decode(base64EncodedString);
    }

    /**
     * 将二进制转换成十六进制(Hex)
     */
    public static String parseByte2HexStr(byte[] buf){
    
    
        StringBuilder sb = new StringBuilder();
        for (byte b : buf) {
    
    
            String hex = Integer.toHexString(b & 0xFF);
            if (hex.length() == 1) {
    
    
                hex = '0' + hex;
            }
            sb.append(hex.toUpperCase());
        }
        return sb.toString();
    }

    /**
     * 将十六进制(Hex)转换成二进制
     */
    public static byte[]  parseHexStr2Byte(String hexStr){
    
    
        if (hexStr.length() < 1){
    
    
            return null;
        } else {
    
    
            int half = hexStr.length() / 2;
            byte[] result = new byte[half];
            for (int i = 0; i < half; i++) {
    
    
                int high = Integer.parseInt(hexStr.substring(i*2, i*2 + 1), 16);
                int low = Integer.parseInt(hexStr.substring(i*2 + 1, i*2 + 2), 16);
                result[i] = (byte) (high * 16 + low);
            }
            return result;
        }
    }
}

对称加密

AES 算法

概述

AES 加密算法全称 Advanced Encryption Standard(高级加密标准),是最为常见的对称加密算法之一。

AES 加密需要:明文 + 密钥 + 偏移量(IV)+ 密码模式(算法/模式/填充)

AES 解密需要:密文 + 密钥 + 偏移量(IV)+ 密码模式(算法/模式/填充)

AES 的密码模式一般为 AES/CBC/PKCS5Padding

  • AES :加解密算法

  • CBC :数据分组模式

  • PKCS5Padding :数据按照一定的大小进行分组,最后分剩下那一组,不够长度,就需要进行补齐, 也可以叫 补齐模式


实际工作中的加密流程

在实际的工作中,客户端跟服务器交互一般都是字符串格式,所以一个比较好的加密流程是:

  • 加密流程 :明文通过 密钥 (有时也需要 偏移量 ),利用 AES 加密算法,然后通过 Base64 转码,最后生成加密后的字符串。
  • 解密流程 :加密后的字符串通过 密钥 (有时也需要 偏移量 ),利用 AES 解密算法,然后通过 Base64 转码,最后生成解密后的字符串。

数据分组模式

AES 的数据分组模式有以下几种:

  • 电码本模式(Electronic codebook,ECB):需要加密的消息按照块密码的块大小被分为数个块,并对每个块进行独立加密。

  • 计算器模式(CTR)

  • 密码反馈模式(CFB)

  • 输出反馈模式(OFB)

  • 密码分组链接模式(CBC)

    将整段明文切成若干小段,然后每一小段与初始块或者上一段的密文段进行异或运算后,再与密钥进行加密。

    在 CBC 模式下,使用 AES 加解密方式进行分组加解密时,需要用到的两个参数:

    • 初始化向量,也就是偏移量
    • 加解密秘钥

    在这里插入图片描述


填充模式

在对数据进行加解密时,通常将数据按照固定的大小(block size)分成多个组,那么随之就产生了一个问题,如果分到最后一组,不够一个 block size 了,就需要进行补齐操作。

例如电子密码本(ECB)和密文块链接(CBC)为对称密钥加密设计的块密码工作模式要求输入明文长度必须是块长度的整数倍,因此信息必须填充至满足要求。

常见填充模式:

算法/模式/填充 16字节加密后数据长度 不满16字节加密后长度
AES/CBC/NoPadding 16 不支持
AES/CBC/PKCS5Padding 32 16
AES/CBC/ISO10126Padding 32 16
AES/CFB/NoPadding 16 原始数据长度
AES/CFB/PKCS5Padding 32 16
AES/CFB/ISO10126Padding 32 16
AES/ECB/NoPadding 16 不支持
AES/ECB/PKCS5Padding 32 16
AES/ECB/ISO10126Padding 32 16
AES/OFB/NoPadding 16 不支持
AES/OFB/PKCS5Padding 32 16
AES/OFB/ISO10126Padding 32 16
AES/PCBC/NoPadding 16 不支持
AES/PCBC/PKCS5Padding 32 16
AES/PCBC/ISO10126Padding 32 16

偏移量

一般是为了增加 AES 加密的复杂度,增加数据的安全性。一般在 AES_256 中会使用到偏移量 ,而在 AES_128 加密中不会使用到


字符集

在 AES 加密中,特别也要注意到字符集的问题。一般用到的字符集是 utf-8 和 gbk 。


AES 具体的加密流程介绍

在这里插入图片描述

  • 明文 P:没有经过加密的数据

  • 密钥 K:用来加密明文的密码,在对称加密算法中,加密与解密的密钥是相同的

    密钥为接收方与发送方协商产生,但不可以直接在网络上传输,否则会导致密钥泄漏,通常是通过非对称加密算法加密密钥,然后再通过网络传输给对方,或者直接面对面商量密钥。密钥是绝对不可以泄漏的,否则会被攻击者还原密文,窃取机密数据。

  • AES 加密函数:把明文 P 和密钥 K 作为加密函数的参数输入,则加密函数E会输出密文 C

  • 密文 C:经加密函数处理后的数据

  • AES 解密函数:把密文 C 和密钥 K 作为解密函数的参数输入,则解密函数会输出明文 P


AES加密方法

AES为分组加密,分组加密也就是把明文分成一组一组的,每组的长度相等,每次加密一组数据,直到加密完整个明文。

AES 密钥长度(32位比特字) 分组长度(32位比特字) 加密轮数
AES-128 4 4 10
AES-192 6 4 12
AES-256 8 4 14

在AES标准规范中,分组长度只能是128位,也就是说,每个分组为16个字节,每个字节8位,密钥的长度可以使用128位、192位或者258位。密钥的长度不同,推荐加密轮数也不同,比如 AES-128 也就是密钥的长度为128位,加密轮数为10轮,AES-192为12轮,AES-256为14轮。

以AES-128为例,加密中一轮的4个操作:字节代换、行位移、列混合、轮密钥加

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

  • 行位移:就是一个简单的左循环移位操作。

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

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


自定义工具类:AESUtils

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;

/**
 * AES-128-CBC加密模式
 */
@Slf4j
@NoArgsConstructor
@AllArgsConstructor
@Data
@Component
public class AESUtils {
    
    

    public static final String AES = "AES";
    // 填充模式,"算法/模式/补码方式"
    public static final String PADDING = "AES/CBC/PKCS5Padding";

    // 密钥,长度16位,可以用26个字母和数字组成
    private String key = "hj7x89H$yuBI0456";
    // 偏移量,长度16位。增加加密算法的强度
    private String iv = "NIfb&95GUY86Gfgh";


    /**
     * @Description AES算法加密明文
     * @param data 明文
     * @return 密文字节数组
     */
    public byte[] encryptAES(String data) throws Exception {
    
    
        // "算法/模式/补码方式"
        Cipher cipher = Cipher.getInstance(PADDING);
        SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), AES);
        // CBC模式,需要一个向量iv,可增加加密算法的强度
        IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes());
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
        return cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
    }

    public String encryptAESAndEncoder(String data, String strOutType) throws Exception {
    
    
        byte[] encrypted = encryptAES(data);
        if (EncoderUtils.BASE_64.equalsIgnoreCase(strOutType)){
    
    
            // BASE64做转码。同时能起到2次加密的作用。
            return EncoderUtils.encodeByteToBase64(encrypted).trim();
        } else if (EncoderUtils.HEX.equalsIgnoreCase(strOutType)){
    
    
            // 将二进制转换成十六进制
            return EncoderUtils.parseByte2HexStr(encrypted).trim();
        } else {
    
    
            log.warn("can not match strOutType, default use hex");
            // 将二进制转换成十六进制
            return EncoderUtils.parseByte2HexStr(encrypted).trim();
        }
    }

    public String encryptAESAndHexEncoder(String data) throws Exception {
    
    
        return encryptAESAndEncoder(data, EncoderUtils.HEX);
    }

    /**
     * @Description AES算法解密密文
     * @param data 密文
     * @return 明文
     */
    public String decryptAES(byte[] data) throws Exception {
    
    
        // "算法/模式/补码方式"
        Cipher cipher = Cipher.getInstance(PADDING);
        SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), AES);
        IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes());
        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
        return new String(cipher.doFinal(data));
    }

    public String decryptAESAndDecode(String data, String strOutType) throws Exception {
    
    
        byte[] decodeData = null;
        if (EncoderUtils.BASE_64.equalsIgnoreCase(strOutType)){
    
    
            decodeData = EncoderUtils.decodeBase64ToByte(data);
        } else if (EncoderUtils.HEX.equalsIgnoreCase(strOutType)){
    
    
            decodeData = EncoderUtils.parseHexStr2Byte(data);
        } else {
    
    
            log.warn("can not match strOutType, default use hex");
            decodeData = EncoderUtils.parseHexStr2Byte(data);
        }
        return decryptAES(decodeData);
    }

    public String decryptAESAndHexDecode(String data) throws Exception {
    
    
        return decryptAESAndDecode(data, EncoderUtils.HEX);
    }

    public boolean verifyAES(String cipherText, String paramStr, String strOutType) throws Exception {
    
    
        byte[] decodeData = null;
        if (EncoderUtils.BASE_64.equalsIgnoreCase(strOutType)){
    
    
            decodeData = EncoderUtils.decodeBase64ToByte(cipherText);
        } else if (EncoderUtils.HEX.equalsIgnoreCase(strOutType)){
    
    
            decodeData = EncoderUtils.parseHexStr2Byte(cipherText);
        } else {
    
    
            log.warn("can not match strOutType, default use hex");
            decodeData = EncoderUtils.parseHexStr2Byte(cipherText);
        }
        return paramStr.equalsIgnoreCase(decryptAES(decodeData));
    }

    public boolean verifyAES(String cipherText, String paramStr) throws Exception {
    
    
        return verifyAES(cipherText, paramStr, EncoderUtils.HEX);
    }

    public static void main(String[] args) throws Exception {
    
    
        // 待加密的字串
        String data = "password123";
        AESUtils aesUtils = new AESUtils();
        String encryptedData = aesUtils.encryptAESAndHexEncoder(data);
        System.out.println("加密结果:" + encryptedData);
        String decryptedData = aesUtils.decryptAESAndHexDecode(encryptedData);
        System.out.println("解密结果:" + decryptedData);
    }
}

SM4 算法

概述

SM4 分组加密算法是中国无线标准中使用的分组加密算法,在2012年被国家商用密码管理局确定为国家密码行业标准,标准编号 GM/T 0002-2012 并且改名为 SM4 算法,与 SM2 椭圆曲线公钥密码算法,SM3 密码杂凑算法共同作为国家密码的行业标准,在我国密码行业中有着极其重要的位置。

SM4 算法的密钥长度和分组长度均为 128 bit,加解密算法均采用 32 轮非平衡 Feistel 迭代结构,该结构最先出现在分组密码 LOKI 的密钥扩展算法中。SM4 通过 32 轮非线性迭代后加上一个反序变换,这样只需要解密密钥是加密密钥的逆序,就能使得解密算法与加密算法保持一致。故 SM4 算法的加解密过程中使用的算法是完全相同的,唯一不同点在于该算法的解密密钥是由它的加密密钥进行逆序变换后得到的。

  • 基本运算:SM4 密码算法使用模2加和循环移位作为基本运算。
  • 基本密码部件:SM4 密码算法使用了 S 盒、非线性变换 τ、线性变换部件 L、合成变换 T 基本密码部件。
  • 轮函数:SM4 密码算法采用对基本轮函数进行迭代的结构。利用上述基本密码部件,便可构成轮函数。SM4 密码算法的轮函数是一种以字为处理单位的密码函数。
  • 加密算法:SM4 密码算法是一个分组算法。数据分组长度为 128 比特,密钥长度为128比特。加密算法采用32轮迭代结构,每轮使用一个轮密钥。
  • 解密算法:SM4 密码算法是对合运算,因此解密算法与加密算法的结构相同,只是轮密铝的使用顺序相反,解密轮密钥是加密轮密钥的逆序。
  • 密钥扩展算法:SM4 密码算法使用 128 位的加密密钥,并采用 32 轮法代加密结构,每一轮加密使用一个 32 位的轮密钥,共使用 32 个轮密钥。因此需要使用密钥扩展算法,从加密密钥产生出 32 个轮密钥。
  • SM4 的安全性:SM4 密码算法经过我国专业密码机构的充分分析测试,可以抵抗差分攻击、线性攻击等现有攻击,因此是安全的。

注:S 盒是一种利用非线性变换构造的分组密码的一个组件,主要是为了实现分组密码过程中的混淆的特性和设计的。SM4 算法中的 S 盒在设计之初完全按照欧美分组密码的设计标准进行,它采用的方法是能够很好抵抗差值攻击的仿射函数逆映射复合法。


自定义工具类:Sm4Utils

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.util.Arrays;
import java.util.Base64;
import java.util.Base64.Encoder;
import java.util.Base64.Decoder;

@Slf4j
@NoArgsConstructor
@AllArgsConstructor
@Data
@Component
public class Sm4Utils {
    
    

    static {
    
    
        // 注册 BouncyCastle
        Security.addProvider(new BouncyCastleProvider());
    }

    public static final String SM4 = "SM4";
    // 加密算法/分组加密模式/分组填充方式
    // PKCS5Padding-以8个字节为一组进行分组加密
    // 定义分组加密模式使用:PKCS5Padding
    public static final String PADDING = "SM4/ECB/PKCS5Padding";
    // 128-32位16进制;256-64位16进制
    public static final int DEFAULT_KEY_SIZE = 128;

    // 密钥,长度16位,可以用26个字母和数字组成
    private String key = "hj7x89H$yuBI0456";

    private static Cipher generateEcbCipher(int mode, byte[] key) throws Exception {
    
    
        Cipher cipher = Cipher.getInstance(Sm4Utils.PADDING, BouncyCastleProvider.PROVIDER_NAME);
        cipher.init(mode, new SecretKeySpec(key, SM4));
        return cipher;
    }

    public byte[] encryptSM4(byte[] data) throws Exception {
    
    
        Cipher cipher = generateEcbCipher(Cipher.ENCRYPT_MODE, key.getBytes(StandardCharsets.UTF_8));
        return cipher.doFinal(data);
    }

    public String encryptSM4AndEncoder(String data, String strOutType) throws Exception {
    
    
        byte[] encrypted = encryptSM4(data.getBytes(StandardCharsets.UTF_8));
        if (EncoderUtils.BASE_64.equalsIgnoreCase(strOutType)){
    
    
            // BASE64做转码。同时能起到2次加密的作用。
            return EncoderUtils.encodeByteToBase64(encrypted).trim();
        } else if (EncoderUtils.HEX.equalsIgnoreCase(strOutType)){
    
    
            // 将二进制转换成十六进制
            return EncoderUtils.parseByte2HexStr(encrypted).trim();
        } else {
    
    
            log.warn("can not match strOutType, default use hex");
            // 将二进制转换成十六进制
            return EncoderUtils.parseByte2HexStr(encrypted).trim();
        }
    }

    public String encryptSM4AndHexEncoder(String data) throws Exception {
    
    
        return encryptSM4AndEncoder(data, EncoderUtils.HEX);
    }

    public byte[] decryptSM4(byte[] cipherText) throws Exception {
    
    
        Cipher cipher = generateEcbCipher(Cipher.DECRYPT_MODE, key.getBytes(StandardCharsets.UTF_8));
        return cipher.doFinal(cipherText);
    }

    public String decryptSM4AndDecode(String data, String strOutType) throws Exception {
    
    
        byte[] decodeData = null;
        if (EncoderUtils.BASE_64.equalsIgnoreCase(strOutType)){
    
    
            decodeData = EncoderUtils.decodeBase64ToByte(data);
        } else if (EncoderUtils.HEX.equalsIgnoreCase(strOutType)){
    
    
            decodeData = EncoderUtils.parseHexStr2Byte(data);
        } else {
    
    
            log.warn("can not match strOutType, default use hex");
            decodeData = EncoderUtils.parseHexStr2Byte(data);
        }
        return new String(decryptSM4(decodeData));
    }

    public String decryptSM4AndHexDecode(String data) throws Exception {
    
    
        return decryptSM4AndDecode(data, EncoderUtils.HEX);
    }

    public boolean verifySM4(String cipherText, String paramStr, String strOutType) throws Exception {
    
    
        byte[] decodeData = null;
        if (EncoderUtils.BASE_64.equalsIgnoreCase(strOutType)){
    
    
            decodeData = EncoderUtils.decodeBase64ToByte(cipherText);
        } else if (EncoderUtils.HEX.equalsIgnoreCase(strOutType)){
    
    
            decodeData = EncoderUtils.parseHexStr2Byte(cipherText);
        } else {
    
    
            log.warn("can not match strOutType, default use hex");
            decodeData = EncoderUtils.parseHexStr2Byte(cipherText);
        }
        return Arrays.equals(decryptSM4(decodeData), paramStr.getBytes(StandardCharsets.UTF_8));
    }

    public boolean verifySM4(String cipherText, String paramStr) throws Exception {
    
    
        return verifySM4(cipherText, paramStr, EncoderUtils.HEX);
    }

    public static void main(String[] args) throws Exception {
    
    
        // 待加密的字串
        String data = "张三";
        Sm4Utils sm4Utils = new Sm4Utils();
        String encryptedData = sm4Utils.encryptSM4AndHexEncoder(data);
        System.out.println("加密结果:" + encryptedData);
        String decryptedData = sm4Utils.decryptSM4AndHexDecode(encryptedData);
        System.out.println("解密结果:" + decryptedData);
    }
}

非对称加密

RSA 算法

概述

  • RSA是基于大数因子分解难题。目前各种主流计算机语言都支持RSA算法的实现
  • java6及以上版本支持RSA算法
  • RSA算法可以用于数据加密和数字签名
  • RSA算法相对于 DES/AES 等对称加密算法,速度要慢的多
  • 总原则:公钥加密,私钥解密 / 私钥加密,公钥解密

使用方式

RSA算法构建密钥对简单,这里以甲乙双方发送数据为模型

  1. 甲方在本地构建密钥对(公钥+私钥),并将公钥公布给乙方

  2. 甲方将数据用私钥进行加密,发送给乙方

  3. 乙方用甲方提供的公钥对数据进行解密

如果乙方向传送数据给甲方:

  1. 乙方用公钥对数据进行加密,然后传送给甲方

  2. 甲方用私钥对数据进行解密


自定义工具类:RSAUtils

import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;

/**
 * 非对称加密算法RSA算法组件
 */
public class RSAUtils {
    
    
    //非对称密钥算法
    public static final String KEY_ALGORITHM = "RSA";


    /**
     * 密钥长度,DH算法的默认密钥长度是1024
     * 密钥长度必须是64的倍数,在512到65536位之间
     */
    private static final int KEY_SIZE = 512;
    //公钥
    private static final String PUBLIC_KEY = "RSAPublicKey";

    //私钥
    private static final String PRIVATE_KEY = "RSAPrivateKey";

    /**
     * 初始化密钥对
     *
     * @return Map 甲方密钥的Map
     */
    public static Map<String, Object> initKey() throws Exception {
    
    
        //实例化密钥生成器
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM);
        //初始化密钥生成器
        keyPairGenerator.initialize(KEY_SIZE);
        //生成密钥对
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        //甲方公钥
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        //甲方私钥
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        //将密钥存储在map中
        Map<String, Object> keyMap = new HashMap<String, Object>();
        keyMap.put(PUBLIC_KEY, publicKey);
        keyMap.put(PRIVATE_KEY, privateKey);
        return keyMap;

    }


    /**
     * 私钥加密
     *
     * @param data 待加密数据
     * @param key       密钥
     * @return byte[] 加密数据
     */
    public static byte[] encryptByPrivateKey(byte[] data, byte[] key) throws Exception {
    
    

        //取得私钥
        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(key);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        //生成私钥
        PrivateKey privateKey = keyFactory.generatePrivate(pkcs8KeySpec);
        //数据加密
        Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
        cipher.init(Cipher.ENCRYPT_MODE, privateKey);
        return cipher.doFinal(data);
    }

    /**
     * 公钥加密
     *
     * @param data 待加密数据
     * @param key       密钥
     * @return byte[] 加密数据
     */
    public static byte[] encryptByPublicKey(byte[] data, byte[] key) throws Exception {
    
    

        //实例化密钥工厂
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        //初始化公钥
        //密钥材料转换
        X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(key);
        //产生公钥
        PublicKey pubKey = keyFactory.generatePublic(x509KeySpec);

        //数据加密
        Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
        cipher.init(Cipher.ENCRYPT_MODE, pubKey);
        return cipher.doFinal(data);
    }

    /**
     * 私钥解密
     *
     * @param data 待解密数据
     * @param key  密钥
     * @return byte[] 解密数据
     */
    public static byte[] decryptByPrivateKey(byte[] data, byte[] key) throws Exception {
    
    
        //取得私钥
        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(key);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        //生成私钥
        PrivateKey privateKey = keyFactory.generatePrivate(pkcs8KeySpec);
        //数据解密
        Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        return cipher.doFinal(data);
    }

    /**
     * 公钥解密
     *
     * @param data 待解密数据
     * @param key  密钥
     * @return byte[] 解密数据
     */
    public static byte[] decryptByPublicKey(byte[] data, byte[] key) throws Exception {
    
    

        //实例化密钥工厂
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        //初始化公钥
        //密钥材料转换
        X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(key);
        //产生公钥
        PublicKey pubKey = keyFactory.generatePublic(x509KeySpec);
        //数据解密
        Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
        cipher.init(Cipher.DECRYPT_MODE, pubKey);
        return cipher.doFinal(data);
    }

    /**
     * 取得私钥
     *
     * @param keyMap 密钥map
     * @return byte[] 私钥
     */
    public static byte[] getPrivateKey(Map<String, Object> keyMap) {
    
    
        Key key = (Key) keyMap.get(PRIVATE_KEY);
        return key.getEncoded();
    }

    /**
     * 取得公钥
     *
     * @param keyMap 密钥map
     * @return byte[] 公钥
     */
    public static byte[] getPublicKey(Map<String, Object> keyMap) throws Exception {
    
    
        Key key = (Key) keyMap.get(PUBLIC_KEY);
        return key.getEncoded();
    }

    /**
     * 测试
     */
    public static void main(String[] args) throws Exception {
    
    
        //初始化密钥
        //生成密钥对
        Map<String, Object> keyMap = RSACoder.initKey();
        //公钥
        byte[] publicKey = RSACoder.getPublicKey(keyMap);
        //私钥
        byte[] privateKey = RSACoder.getPrivateKey(keyMap);
        System.out.println("公钥:" + Base64.encodeBase64String(publicKey));
        System.out.println("私钥:" + Base64.encodeBase64String(privateKey));

        System.out.println("==========密钥对构造完毕,甲方将公钥公布给乙方,开始进行加密数据的传输==========");
        String str = "RSA密码交换算法";
        System.out.println("===========甲方向乙方发送加密数据==============");
        System.out.println("原文:" + str);
        //甲方进行数据的加密
        byte[] code1 = RSACoder.encryptByPrivateKey(str.getBytes(), privateKey);
        System.out.println("加密后的数据:" + Base64.encodeBase64String(code1));
        System.out.println("===========乙方使用甲方提供的公钥对数据进行解密==============");
        //乙方进行数据的解密
        byte[] decode1 = RSACoder.decryptByPublicKey(code1, publicKey);
        System.out.println("乙方解密后的数据:" + new String(decode1));

        System.out.println("===========反向进行操作,乙方向甲方发送数据==============");

        str = "乙方向甲方发送数据RSA算法";
        System.out.println("原文:" + str);
        //乙方使用公钥对数据进行加密
        byte[] code2 = RSACoder.encryptByPublicKey(str.getBytes(), publicKey);
        System.out.println("===========乙方使用公钥对数据进行加密==============");
        System.out.println("加密后的数据:" + Base64.encodeBase64String(code2));

        System.out.println("=============乙方将数据传送给甲方======================");
        System.out.println("===========甲方使用私钥对数据进行解密==============");
        //甲方使用私钥对数据进行解密
        byte[] decode2 = RSACoder.decryptByPrivateKey(code2, privateKey);
        System.out.println("甲方解密后的数据:" + new String(decode2));
    }
}

拓展

javax.crypto.Cipher 类

javax.crypto.Cipher 类是从 jdk1.4 就开始引入,所属 jdk 拓展包 Java Cryptographic Extension(JCE)框架,该框架主要用于加密解密和密码功能。

目前已支持的算法

总共支持以下加密算法: AES / DES / DESede / RSA

类型值 密码长度 说明
AES/CBC/NoPadding 128 AES算法的CBC模式实现
AES/CBC/PKCS5Padding 128 AES算法的CBC模式实现, 并用PKCS5Padding规则填充
AES/ECB/NoPadding 128 AES算法的ECB模式实现
AES/ECB/PKCS5Padding 128 AES算法的ECB模式实现, 并用PKCS5Padding规则填充
DES/CBC/NoPadding 56
DES/CBC/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) 密码长度有范围可选

常见的 加密模式 有以下几种:

  • ECB(Electronic Codebook Book,电码本模式):将明文分成若干小段,然后对每小段进行加密
  • CBC(Cipher Block Chaining,密码分组链接模式):先将明文切分成若干小段,然后每一小段与初始块或者上一段的密文段进行异或运算后,再与密钥进行加密

常见的 填充规则 有以下几种:

  • 大部分情况下,明文并非刚好 N 位的倍数。对于最后一个分组,如果长度小于N位,则需要用数据填充至 N 位

Cipher 类里面常用方法:

// 获取 Cipher 实现指定转换的对象
public static final Cipher getInstance(String transformation, String provider)
    
// 使用密钥和一组算法参数初始化此密码
public final void init(int opmod, Key key);
public final void init(int opmod, Certificate certificate);
public final void init(int opmod, Key key, SecureRandom random);
public final void init(int opmod, Certificate certificate, SecureRandom random);
public final void init(int opmod, Key key, AlgorithmParameterSpec params);
public final void init(int opmod, Key key, AlgorithmParameterSpec params, SecureRandom random);
public final void init(int opmod, Key key, AlgorithmParameters params);
public final void init(int opmod, Key key, AlgorithmParameters params,SecureRandom random);
    
// 完成多部分加密或解密操作,具体取决于此密码的初始化方式
public final byte[] doFinal(byte[] input);
public final byte[] doFinal(byte[] input, int inputOffset, int inputLen);
public final int doFinal(byte[] input, int inputOffset, int inputLen, byte[] output);
public final int doFinal(byte[] input, int inputOffset, int inputLen, byte[] output,int outputOffset);

// 在多步加密或解密数据时,首先需要一次或多次调用update方法,用以提供加密或解密的所有数据
public byte[] update(byte[] input);
public byte[] update(byte[] input, int inputOffset, int inputLen);
public int update(byte[]input, int inputOffset, int inputLen, byte[] output);
public int update(byte[]input, int inputOffset, int inputLen, byte[] output, int outputOffset);
// 如果还有输入数据,多步操作可以使用前面提到的doFinal方法之一结束。如果没有数据,多步操作可以使用下面的doFinal方法之一结束
public final byte[] doFinal();
public final int doFinal(byte[] output, int outputOffset);

其中:

  • 通过调用 Cipher 类中的 getInstance() 静态工厂方法得到 Cipher 对象。

    参数 transformation :密码学算法名称,比如 DES,也可以在后面包含模式和填充方式

    两种写法:

    • “算法/模式/填充”,示例:“DES/CBC/PKCS5Padding”
    • “算法”,示例:“DES”

    如果没有指定模式或填充方式,就使用特定提供者指定的默认模式或默认填充方式。

    例如,SunJCE 提供者使用 ECB 作为 DES、DES-EDE 和 Blowfish 等 Cipher 的默认模式,并使用 PKCS5Padding 作为它们默认的填充方案。这意味着在 SunJCE 提供者中,下列形式的声明是等价的:

    • Cipherc1=Cipher.getInstance(“DES/ECB/PKCS5Padding”);
    • Cipher c1=Cipher.getInstance(“DES”);
  • getInstance() 工厂方法返回的对象没有进行初始化,因此在使用前必须进行初始化。

    通过 getInstance() 得到的 Cipher 对象必须使用下列四个模式之一进行初始化,这四个模式在 Cipher 类中被定义为 finalinteger常数,可以使用符号名来引用这些模式:

    • ENCRYPT_MODE :加密数据
    • DECRYPT_MODE :解密数据
    • WRAP_MODE :将一个 Key 封装成字节,可以用来进行安全传输
    • UNWRAP_MODE :将前述已封装的密钥解开成 java.security.Key 对象

    每个 Cipher 初始化方法使用一个模式参数 opmod,并用此模式初始化 Cipher 对象。此外还有其他参数,包括密钥 key、包含密钥的证书 certificate、算法参数 params 和随机源 random。

    必须指出的是,加密和解密必须使用相同的参数。当 Cipher 对象被初始化时,它将失去以前得到的所有状态。

  • 如果在 transformation 参数部分指定了 padding 或 unpadding 方式,则所有的 doFinal 方法都要注意所用的 padding 或unpadding 方式。

    调用 doFinal 方法将会重置 Cipher 对象到使用 init 进行初始化时的状态,就是说,Cipher 对象被重置,使得可以进行更多数据的加密或解密,至于这两种模式,可以在调用 init 时进行指定。

  • 包裹 wrap 密钥必须先使用 WRAP_MODE 初始化 Cipher 对象,然后调用以下方法:

    public final byte[] wrap(Key key);
    

    如果将调用 wrap 方法的结果(wrap 后的密钥字节)提供给解包裹 unwrap 的人使用,必须给接收者发送以下额外信息:

    1. 密钥算法名称

      密钥算法名称可以调用 Key 接口提供的 getAlgorithm 方法得到:

      public String getAlgorithm();
      
    2. 被包裹密钥的类型(Cipher.SECRET_KEY,Cipher.PRIVATE_KEY,Cipher.PUBLIC_KEY)

    为了对调用 wrap 方法返回的字节进行解包,必须先使用 UNWRAP_MODE 模式初始化 Ciphe r对象,然后调用以下方法:

    public final Keyunwrap(byte[] wrappedKey,String wrappedKeyAlgorithm,int wrappedKeyType));
    

    其中:

    • 参数 wrappedKey 是调用wrap方法返回的字节

    • 参数 wrappedKeyAlgorithm 是用来包裹密钥的算法

    • 参数 wrappedKeyType 是被包裹密钥的类型,该类型必须是

      Cipher.SECRET_KEY, Cipher.PRIVATE_KEY, Cipher.PUBLIC_KEY 三者之一


Bouncy Castle 密码包

概述

BouncyCastle(轻量级密码术包)是一种用于 Java 平台的开放源码的轻量级密码术包;Bouncycstle 包含了大量的密码算法,其支持椭圆曲线密码算法,并提供 JCE 1.2.1的实现。

Java 标准库提供了一系列常用的哈希算法。但如果要用的某种算法,Java 标准库没有提供,则需要自己写或者直接使用一个现成的第三方库。BouncyCastle 就是一个提供了很多哈希算法和加密算法的第三方库。它提供了 Java 标准库没有的一些算法,例如,RipeMD160 哈希算法。

因为 Bouncy Castle 被设计成轻量级的,所以从 J2SE 1.4 到 J2ME(包括 MIDP)平台,它都可以运行。它是在 MIDP 上运行的唯一完整的密码术包。


使用

  • pom.xml 文件中引入 bouncycastle 的 jar 包依赖,把 BouncyCastle 提供的 jar 包放到 classpath 中。这个jar包就是 bcprov-jdk15on-xxx.jar,可以从官方网站下载。

    <dependency>
        <groupId>org.bouncycastle</groupId>
        <artifactId>bcprov-jdk15on</artifactId>
        <version>1.59</version>
    </dependency>
    
  • Java 标准库的 java.security 包提供了一种标准机制,允许第三方提供商无缝接入。要使用 BouncyCastle 提供的 RipeMD160 算法,需要先把 BouncyCastle 注册一下:

    @Test
    public void case1(){
          
          
        static {
          
          
            // 注册BouncyCastle:
        	Security.addProvider(new BouncyCastleProvider());
        }
        
        // 按名称正常调用:
        try {
          
          
            md = MessageDigest.getInstance("RipeMD160");
            md.update("HelloWorld".getBytes("UTF-8"));
        } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
          
          
            e.printStackTrace();
        }
        byte[] result = md.digest();
        System.out.println(new BigInteger(1, result).toString(16));
    }
    

    注意:注册 BouncyCastle 是通过下面的语句实现的。注册只需要在启动时进行一次,后续就可以使用 BouncyCastle 提供的所有哈希算法和加密算法。

    Security.addProvider(new BouncyCastleProvider());
    

猜你喜欢

转载自blog.csdn.net/footless_bird/article/details/124029210
今日推荐