Java MD5与RSA加密使用

发布时间 2023-04-23 15:17:52作者: 風栖祈鸢

Java MD5与RSA加密使用

转发数据到广州,那边要求 HTTP 请求的头部需要用 MD5 签名,请求体数据需要使用 RSA 加密,研究了一下。

MD5

MD5(Message Digest Algorithm 5)是一种广泛使用的加密哈希函数,可将任意长度的消息转换为128位的哈希值(通常以32个十六进制字符表示)。MD5 算法是一种单向加密算法,也就是说只能进行加密,无法解密。因此,它常用于密码的加密存储和数字签名验证等场景。

MD5 算法的主要原理是通过多次对输入数据进行压缩、迭代和变换,最终生成一个哈希值。MD5 算法的具体实现包括以下几个步骤:

  1. 填充消息:首先需要对消息进行填充,使其长度满足一个特定的条件(例如长度为64的倍数),以便于后续的处理。
  2. 初始值设置:设置4个32位的寄存器(A、B、C、D)的初始值,作为哈希值的组成部分。
  3. 消息分块:将填充后的消息按照长度为512位的块进行分组,每组包含16个32位的字。
  4. 循环迭代:对每个消息块进行循环迭代,共进行64轮,每轮都包含4个变换步骤。
  5. 输出结果:最后将4个寄存器的值连接起来,形成128位的哈希值。

MD5 算法被广泛应用于互联网安全领域,例如密码加密、数字签名、消息认证等。但是,由于 MD5 算法存在一些漏洞,例如碰撞攻击,因此在一些安全性要求更高的场合,如用户密码存储,建议使用更为安全的加密算法,例如 SHA-2 系列的哈希函数。

彩虹表攻击:彩虹表攻击是一种密码破解方法,通常用于破解哈希函数加密的密码。彩虹表是一种预先计算好的表,其中包含了大量可能的明文和它们对应的哈希值。攻击者使用彩虹表可以快速地反推出哈希值对应的明文密码。这种攻击方法相对于暴力破解等其他破解方法来说速度非常快,因为攻击者不需要每次都重新计算哈希值,而是直接在预先计算好的彩虹表中查找匹配。

简单来说,MD5 加密是单向的,是没法从加密后的密文解密得到明文的。因此主要的应用就是密码的密文保存、对比数据是否被篡改。

MD5Util

MD5 加密的应用比较简单,放一个 MD5Util 看一下:

public class MD5Util {

    public static String encipherHeader(String loginName, String loginPwd, String timestamp) {

        //String timestampStr = Long.toString(timestamp);
        StringBuffer sb = new StringBuffer();
        sb.append("appkey=");
        sb.append(loginName);
        sb.append(",secret=RSA,signt=");
        sb.append(timestamp);
        sb.append(",secretKey=");
        sb.append(loginPwd);
        String header = sb.toString();

        String md5Header = md5Encipher(header);

        return md5Header;
    }

    public static String md5Encipher(String str){

        String md5Str = null;
        StringBuffer sb = new StringBuffer();

        try {
            // 创建 MessageDigest 对象并指定算法为 MD5
            MessageDigest md = MessageDigest.getInstance("MD5");

            // 将字符串转换为字节数组并进行加密
            byte[] md5Bytes = md.digest(str.getBytes());

            // 将字节数组转换为字符串
            for (byte b : md5Bytes) {
                sb.append(String.format("%02x", b));
            }
            md5Str = sb.toString();

            System.out.println("MD5 加密前原始数据:" + str);
            System.out.println("MD5 加密结果:" + md5Str);

        } catch (NoSuchAlgorithmException e) {
            System.err.println("不支持的加密算法:MD5");
            e.printStackTrace();
        }
        return md5Str;
    }

}

其中 md5Encipher 是对字符串进行加密的方法,headerEncipher 是我自己要用的对 HTTPHeader 加密的方法。

测试

写一个 Main 方法测试一下加密:

    public static void main(String[] args) {
        // 接收对方传来的 MD5 加密后的密文
        String pwd = "4d7ad62f7d74f7defd845740661f53e2";

        // 根据对方传来的数据计算 MD5 加密的结果
        String encipher = MD5Util.md5Encipher("appkey=1234567890,secret=RSA,signt=1637109703238,secretKey=abcdefghijklmnopqrstuvwxyz");
        System.out.println("加密结果:" + encipher);
        if(pwd.equals(encipher)){
            // 两次结果一致,数据没有被篡改/签名有效
            System.out.println("数据相等");
        }else {
            // 两次结果不一致,数据被篡改了/签名无效
            System.out.println("数据不相等");
        }
    }

直接写在工具类里,运行,控制台输出:

MD5 加密前原始数据:appkey=1234567890,secret=RSA,signt=1637109703238,secretKey=abcdefghijklmnopqrstuvwxyz
MD5 加密结果:4d7ad62f7d74f7defd845740661f53e2
加密结果:4d7ad62f7d74f7defd845740661f53e2
==========数据相等==========

进程已结束,退出代码0

验证成功,没什么问题。

RSA

RSA是一种公钥加密算法,其加密和解密过程都需要用到两个密钥,一个是公钥,另一个是私钥。公钥可以自由发布,私钥则是保密的。

RSA的安全性基于大数分解难题,也就是说,对于一个足够大的整数,要找到它的所有质因数是非常困难的。RSA算法的核心是选择两个大质数p和q,然后计算它们的乘积N=p * q。然后根据一定的算法计算出一个公钥和一个私钥。

公钥包含两个参数N和e,私钥包含两个参数N和d。其中,N为大质数p和q的乘积,e是一个小于N的正整数,且e与(p-1) * (q-1)互质,d是e的模逆元,满足 (e * d) % ((p-1) * (q-1)) = 1。

RSA加密过程如下:

  1. 接收方生成一对密钥,其中一个是公钥,公开给发送方;另一个是私钥,保存在自己手中。
  2. 发送方获取接收方的公钥,将明文进行加密后发送。
  3. 接收方接收到密文后,使用自己的私钥进行解密,得到明文。

RSA算法的优点是安全性高,加密速度较快,可以用于数据的加密、数字签名等方面。缺点是加密和解密的速度较慢,且加密的数据长度有限制。

RSA 加密在网上找了找都是讲 RSA 算法的,或者应用起来比较复杂。所以搞个简单的工具类,万一以后还要用到就可以直接用了。

RSAUtil

也是放一个 RSAUtil,其中包括了生成公钥私钥、使用公钥加密、使用私钥解密的方法:

public class RSAUtil {

    //密钥长度
    private final static int KEY_SIZE = 1024;

    //保存产生的公钥与私钥
    private static Map<String, String> keyMap = new HashMap<String, String>();

    /**
     * RSA公钥加密
     *
     * @param str       加密字符串
     * @param publicKey 公钥
     * @return 密文
     * @throws Exception
     */
    public static String encrypt(String str, String publicKey) throws Exception {
        // 将公钥解码为 base64
        byte[] decoded = decryptBASE64(publicKey);
        RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded));
        // RSA加密
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, pubKey);
        String outStr = encryptBASE64(cipher.doFinal(str.getBytes("UTF-8")));
        return outStr;
    }

    /**
     * RSA私钥解密
     *
     * @param str        加密字符串
     * @param privateKey 私钥
     * @return 明文
     * @throws Exception
     */
    public static String decrypt(String str, String privateKey) throws Exception {
        //64位解码加密后的字符串
        byte[] inputByte = decryptBASE64(str);
        //base64编码的私钥
        byte[] decoded = decryptBASE64(privateKey);
        RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decoded));
        //RSA解密
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, priKey);
        String outStr = new String(cipher.doFinal(inputByte));
        return outStr;
    }

    // 编码返回字符串
    public static String encryptBASE64(byte[] key) throws Exception {
        return (new BASE64Encoder()).encodeBuffer(key);
    }

    // 解码返回byte
    public static byte[] decryptBASE64(String key) throws Exception {
        return (new BASE64Decoder()).decodeBuffer(key);
    }

    /**
     * 随机生成密钥对
     * @throws Exception
     */
    public static void generateKeyPair() throws Exception {
        // KeyPairGenerator 基于RSA算法,用于生成一对公钥和私钥
        KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
        // 初始化密钥对生成器
        keyPairGen.initialize(KEY_SIZE, new SecureRandom());
        // 生成一个密钥对,保存在keyPair中
        KeyPair keyPair = keyPairGen.generateKeyPair();

        // 获取公钥
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        // 获取私钥
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();

        // 获取公钥字符串
        String publicKeyString = encryptBASE64(publicKey.getEncoded());
        // 得到私钥字符串
        String privateKeyString = encryptBASE64(privateKey.getEncoded());

        // 保存公钥和私钥
        keyMap.put("pubKey", publicKeyString);
        keyMap.put("privateKey", privateKeyString);
    }

}

其中用到了 common-codec 的依赖:

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

测试

写一个 Main 方法测试一下获取密钥和加密解密的过程:

    public static void main(String[] args) {
        try {
            // 生成公钥和私钥
            generateKeyPair();
            String publicKey = keyMap.get("pubKey");
            System.out.println("公钥:" + publicKey);
            String privateKey = keyMap.get("privateKey");
            System.out.println("私钥:" + privateKey);

            String orgData = "然而也应当记住黑暗的时日";
            System.out.println("原始数据:" + orgData);

            String encryptStr = encrypt(orgData,publicKey);
            System.out.println("加密数据:" + encryptStr);

            String decryptStr = decrypt(encryptStr,privateKey);
            System.out.println("解密数据:" + decryptStr);

            if(orgData.equals(decryptStr)){
                System.out.println("==========原始数据与解密数据一致==========");
            }else {
                System.out.println("==========原始数据与解密数据不一致==========");
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

也是直接在工具类里写的,运行,控制台输出:

公钥:MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC/+dP4fJbr4EjhLxjUMcI7zOpnOES5VBnM03Uf
w5yuPglwc2UVeV0B12AC3GIwTZ/94Ohf0+a3g9ecx6CVEzyYeuIfnOjXhM7waVZdR8LcbwsN8kgX
92lpc2qNSquMRU57Wt/cxSoarBXyh1DcCRxzQFACEc04voeVRHzuB6QKZwIDAQAB

私钥:MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAL/50/h8luvgSOEvGNQxwjvM6mc4
RLlUGczTdR/DnK4+CXBzZRV5XQHXYALcYjBNn/3g6F/T5reD15zHoJUTPJh64h+c6NeEzvBpVl1H
wtxvCw3ySBf3aWlzao1Kq4xFTnta39zFKhqsFfKHUNwJHHNAUAIRzTi+h5VEfO4HpApnAgMBAAEC
gYBLBwt1yNN++hfhkfOFMrEzh+FwV8hcGec/asESmfOJEYvE3AR8gQL9bjwCwjjJofzOTvDiSsGX
pTpF9qrmuC7sxqKN+xVdAJzoTIzUSXd/Zd/RLhxDWHLSppyJ5c1n1pikDKrAkUqfWnp2gjMUT4Pd
pU4WH/W7IFV9H0o+AOCbcQJBAPV/KN871ydNkwQL8+zu2L3iXTHMWKmEv40AXI+vh9c5iS08fiJi
VL3TQjC/W8SQ4Emj2lF48XSIdTO7gPFF7I8CQQDIMHkrFjeWYyk6QuCC5mV9F95RyQDk0yvrcLtg
aEvA9wZVv/FcZOq7I2ejcYWt1wi99sXpFwdZ0oiI3GZFByCpAkEAo+RIfP+OG4cGZuUz6zFpMRs1
7FDnwAQHfTKImMQug9i9Y53G911+BVxMDA80TH4Lvh3NWibLy2huFiNPacOssQJAU2fmw+3gwRaV
ccG1WrR1alYMeZS+e5gD/3cbioJJtZ72E7oB7JXbOpb4sh81LAWgjc0IDiJbHLBb1HHHZlEe6QJB
ANzDR19vzmit+miwFkNbVfCPSFX0oKbWZaNfkJ+zmg4scCDppny4S2pxZQ+zyOGATXUlJH3YvHd2
lqDpIbXxong=

原始数据:然而也应当记住黑暗的时日
加密数据:fi//U40WewohsDAva8u0j71pzwOsIoCAxpR1aTvNr7HnU3pmU1MmePonePePab0cZBwIvl/3TQyV
GLduPS2XOdIIqKrT28bXLL02f7UXkFCICrD/JiotClG6gbKOtEKCpgOIKKoLEXA7RPDLzw7QFSjz
8m9z4s3/uHjrCcaaTwY=

解密数据:然而也应当记住黑暗的时日
==========原始数据与解密数据一致==========

进程已结束,退出代码0

没什么问题,加解密能够应用就行了,其中的原理不必深究了。。。