跳至主要內容

手摸手带你打造至强 RSA 工具类

未央大约 15 分钟rsabase64JavaC#加密解密

太长不看,我要代码!

本文不对RSA原理进行详细讲解,只关注于在Java中如何使用。

网上有很多RSA工具类的 demo,但是没有一个好用的。今天老李抽丝剥茧,手摸手带你打造一款属于自己的至强RSA工具类。

RSA是非对称加密算法,包含由公钥私钥组成的密钥对,支持公钥加密,私钥解密私钥加密,公钥解密

实战

生成密钥对

使用Java提供的KeyPairGenerator类,我们只需提供加密算法的名称并设置密钥长度就可以生成密钥对。

// 获取 RSA 算法的密钥对生成器
var keyPairGenerator = KeyPairGenerator.getInstance("RSA");
// 设置密钥长度
keyPairGenerator.initialize(1024);

var keyPair = keyPairGenerator.generateKeyPair();

让我们对这段代码进行封装

public final class RSAUtil {
    // 加密算法
    public static final String KEY_ALGORITHM = "RSA";

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

    public static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
        var keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM);
        keyPairGenerator.initialize(KEY_SIZE);
        return keyPairGenerator.genKeyPair();
    }
}

有了密钥对我们就可以通过公钥或私钥对报文进行加解密了,接下来完成加密方法和解密方法

实现加解密方法

// 加密
public static byte[] encrypt(byte[] data, Key key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
    var cipher = Cipher.getInstance(key.getAlgorithm());
    cipher.init(Cipher.ENCRYPT_MODE, key);
    return cipher.doFinal(data);
}

// 解密
public static byte[] decrypt(byte[] data, Key key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
    var cipher = Cipher.getInstance(key.getAlgorithm());
    cipher.init(Cipher.DECRYPT_MODE, key);
    return cipher.doFinal(data);
}

加密和解密方法都是通过Java提供的Cipher类实现的,我们要做的只是根据加密算法获取对应的Cipher实例并初始化,在Cipher::init方法中告知Cipher我们要进行加密操作还是解密操作,通过Cipher类提供的两个常量Cipher.ENCRYPT_MODECipher.DECRYPT_MODE设置。初始化完成后通过Cipher::doFinal方法进行加解密。

这两个方法除了设置加解密模式的参数(Cipher.ENCRYPT_MODE | Cipher.DECRYPT_MODE)外其他逻辑都是相同的,因此我们对其进行重构,如下:

public static byte[] doCipher(int opmode, Key key, byte[] data) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
    var cipher = Cipher.getInstance(key.getAlgorithm());
    cipher.init(opmode, key);
    return cipher.doFinal(data);
}

下面给出此时工具类的完整代码:

public final class RSAUtil {
    // 加密算法
    public static final String KEY_ALGORITHM = "RSA";

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

    public static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
        var keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM);
        keyPairGenerator.initialize(KEY_SIZE);
        return keyPairGenerator.genKeyPair();
    }

    public static byte[] doCipher(int opmode, Key key, byte[] data) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
        var cipher = Cipher.getInstance(key.getAlgorithm());
        cipher.init(opmode, key);
        return cipher.doFinal(data);
    }
}

下面我们对生成密钥对的方法和加解密方法进行测试,代码如下:

public class App {

    public static void main(String[] args) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException {
        var keyPair = RSAUtil.generateKeyPair();

        testEncryptByPublicKeyDecryptByPrivateKey(keyPair);
        testEncryptByPrivateKeyDecryptByPublicKey(keyPair);
    }

    // 公钥加密私钥解密
    public static void testEncryptByPublicKeyDecryptByPrivateKey(KeyPair keyPair) throws NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
        var message = "冰墩墩";

        var encryptedMessage = RSAUtil.doCipher(Cipher.ENCRYPT_MODE, keyPair.getPublic(), message.getBytes(StandardCharsets.UTF_8));
        var decryptedMessage = RSAUtil.doCipher(Cipher.DECRYPT_MODE, keyPair.getPrivate(), encryptedMessage);

        System.out.println("解密后报文:" + new String(decryptedMessage));
    }

    // 私钥加密公钥解密
    public static void testEncryptByPrivateKeyDecryptByPublicKey(KeyPair keyPair) throws NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
        var message = "雪容融";

        var encryptedMessage = RSAUtil.doCipher(Cipher.ENCRYPT_MODE, keyPair.getPrivate(), message.getBytes(StandardCharsets.UTF_8));
        var decryptedMessage = RSAUtil.doCipher(Cipher.DECRYPT_MODE, keyPair.getPublic(), encryptedMessage);

        System.out.println("解密后报文:" + new String(decryptedMessage));
    }
}

运行程序,控制台输出如下:

解密后报文:冰墩墩
解密后报文:雪容融

理论上到这里就完了,完美实现RSA加解密。但理论和实践之间还有一道鸿沟需要填补,接下来我们就一步步优化,实现我们的至强RSA工具类。

优化

KeyPairGenerator生成的密钥对内部使用字节数组保存公钥和私钥,我们需要将公钥和私钥通过base64编码为人类可读的形式以便阅读和存储。下面我们自定义一个类用于保存编码后的公钥和私钥。

RSAKeyPair

public final class RSAKeyPair {

    private static final Base64.Encoder Base64Encoder = Base64.getEncoder();

    // 保存原始的 keyPair
    private KeyPair keyPair;

    // 编码后的公钥
    private String publicKey;

    // 编码后的私钥
    private String privateKey;

    // 禁止外部实例化
    private RSAKeyPair() {
    }

    // 通过 of 静态方法获取 RSAKeyPair 实例
    public static RSAKeyPair of(KeyPair keyPair) {
        var rsaKeyPair = new RSAKeyPair();

        rsaKeyPair.keyPair = keyPair;
        rsaKeyPair.publicKey = Base64Encoder.encodeToString(keyPair.getPublic().getEncoded());
        rsaKeyPair.privateKey = Base64Encoder.encodeToString(keyPair.getPrivate().getEncoded());
        return rsaKeyPair;
    }

    public KeyPair getKeyPair() {
        return keyPair;
    }

    public String getPublicKey() {
        return publicKey;
    }

    public String getPrivateKey() {
        return privateKey;
    }

    @Override
    public String toString() {
        return String.format("%s%n  publicKey: %s%n  privateKey: %s%n", RSAKeyPair.class.getSimpleName(), publicKey, privateKey);
    }
}

测试一下RSAKeyPair类,一睹密钥对真容:

public class App {

    public static void main(String[] args) throws NoSuchAlgorithmException {
        var keyPair = RSAUtil.generateKeyPair();
        var rsaKeyPair = RSAKeyPair.of(keyPair);

        System.out.println(rsaKeyPair);
    }
}

输出如下:

RSAKeyPair
  publicKey: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCv2jH23VSbsGFKfReQxSxlhPCvPXuLBbDAvYEqKiR+MnxJbcqXwsDIR0tO+Md5/L3zRyodBkr2jULgY21ltIrc30cERjB2KF9vN5h09TJRDobOyjYseXTp2chknRHrpywt+ghSZRs39qJs40VyENTgQ9NBiQXj2ZXucfJ+GFhFrwIDAQAB
  privateKey: MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAK/aMfbdVJuwYUp9F5DFLGWE8K89e4sFsMC9gSoqJH4yfEltypfCwMhHS074x3n8vfNHKh0GSvaNQuBjbWW0itzfRwRGMHYoX283mHT1MlEOhs7KNix5dOnZyGSdEeunLC36CFJlGzf2omzjRXIQ1OBD00GJBePZle5x8n4YWEWvAgMBAAECgYAhOgocyf81l6Mabv5n5UmZOQA9LFHOl9mo4WWpcOMKUUG4oh0YhbzlWss49brDKuU9NWIYr9q0MUbEnSTLhcyC1RZN+GLtIeuQ9ysMbs76oRY0KH8ZQiuGxZV4FX0ZCfrHsdGodDjXxmBOhMOZsvPoncPA0JY/Cy12Sdc1rzzkiQJBANvK11X5BR0HChj+RUeQMHXIqlkkDxOzX7fsR32nf3KEUZBtP27IA9P5f/2yd8zutnCdE67truqwIRFZiB4rDs0CQQDM0kzYDSdOGzQrkHUPvcI9za8LBWKQwue9Vxt4PvoLhOio1rYkAd/IcGKxZEmJZA2iFXDjmmU9CHZLVbhB8W5rAkB5YmLeZjK+vz6CYxsb1LQOuI3rwRBajvvT9bfd2311X0I0g0E/C1Oh4+8dy0yCb2tucjGGsFmj3zXEATA9iQYZAkBRhEVHG30QLe2GhRjB6hD7jffjmAIRgTC//4IUSmQz33LFd6bID+LjoC73UOWfg62VW5kxTIqMTujdtMD/pbn/AkEA10G91iABHviv6zN1OUWdo44LdEVPOrQqBgakQ+UqBCgiHRlOw0tP053ibksA15NbQpQl2k5ORA0xkJZy4rTAXQ==

现在我们已经有了编码后的可读形式的密钥了,但在程序中我们还是需要原始格式的密钥才能进行操作,所以接下来我们完成密钥的加载方法,即将上述文本格式的密钥还原成程序中使用的格式。

加载密钥

Java中使用X509EncodedKeySpec类加载公钥,使用PCKS8EncodedKeySpec类加载私钥。对应的加载方法如下:

public static PublicKey loadPublicKey(byte[] keyData) throws NoSuchAlgorithmException, InvalidKeySpecException {
    var keySpec = new X509EncodedKeySpec(keyData, KEY_ALGORITHM);
    var keyFactory = KeyFactory.getInstance(keySpec.getAlgorithm());
    return keyFactory.generatePublic(keySpec);
}

public static PrivateKey loadPrivateKey(byte[] keyData) throws NoSuchAlgorithmException, InvalidKeySpecException {
    var keySpec = new PKCS8EncodedKeySpec(keyData, KEY_ALGORITHM);
    var keyFactory = KeyFactory.getInstance(keySpec.getAlgorithm());
    return keyFactory.generatePrivate(keySpec);
}

 





 



我们在来重载两个方法,用于直接从base64编码后的密钥加载:

public static PublicKey loadPublicKey(String keyText) throws NoSuchAlgorithmException, InvalidKeySpecException {
    return loadPublicKey(Base64Decoder.decode(keyText));
}

public static PrivateKey loadPrivateKey(String keyText) throws NoSuchAlgorithmException, InvalidKeySpecException {
    return loadPrivateKey(Base64Decoder.decode(keyText));
}

此时完整的工具类如下:

public final class RSAUtil {

    private static final Base64.Decoder Base64Decoder = Base64.getDecoder();

    // 加密算法
    public static final String KEY_ALGORITHM = "RSA";

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

    public static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
        var keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM);
        keyPairGenerator.initialize(KEY_SIZE);
        return keyPairGenerator.genKeyPair();
    }

    public static byte[] doCipher(int opmode, Key key, byte[] data) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
        var cipher = Cipher.getInstance(key.getAlgorithm());
        cipher.init(opmode, key);
        return cipher.doFinal(data);
    }

    public static final class KeyLoader {

        public static PublicKey loadPublicKey(byte[] keyData) throws NoSuchAlgorithmException, InvalidKeySpecException {
            var keySpec = new X509EncodedKeySpec(keyData, KEY_ALGORITHM);
            var keyFactory = KeyFactory.getInstance(keySpec.getAlgorithm());
            return keyFactory.generatePublic(keySpec);
        }

        public static PrivateKey loadPrivateKey(byte[] keyData) throws NoSuchAlgorithmException, InvalidKeySpecException {
            var keySpec = new PKCS8EncodedKeySpec(keyData, KEY_ALGORITHM);
            var keyFactory = KeyFactory.getInstance(keySpec.getAlgorithm());
            return keyFactory.generatePrivate(keySpec);
        }

        public static PublicKey loadPublicKey(String keyText) throws NoSuchAlgorithmException, InvalidKeySpecException {
            return loadPublicKey(Base64Decoder.decode(keyText));
        }

        public static PrivateKey loadPrivateKey(String keyText) throws NoSuchAlgorithmException, InvalidKeySpecException {
            return loadPrivateKey(Base64Decoder.decode(keyText));
        }
    }
}

测试:

public class App {

    public static void main(String[] args) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeySpecException, InvalidKeyException {
        var keyPair = RSAUtil.generateKeyPair();
        var rsaKeyPair = RSAKeyPair.of(keyPair);

        var publicKey = RSAUtil.KeyLoader.loadPublicKey(rsaKeyPair.getPublicKey());
        var privateKey = RSAUtil.KeyLoader.loadPrivateKey(rsaKeyPair.getPrivateKey());

        testEncryptByPublicKeyDecryptByPrivateKey(publicKey, privateKey);
        testEncryptByPrivateKeyDecryptByPublicKey(publicKey, privateKey);
    }

    // 公钥加密私钥解密
    public static void testEncryptByPublicKeyDecryptByPrivateKey(PublicKey publicKey, PrivateKey privateKey) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException {
        var message = "冰墩墩";

        var encryptedMessage = RSAUtil.doCipher(Cipher.ENCRYPT_MODE, publicKey, message.getBytes(StandardCharsets.UTF_8));
        var decryptedMessage = RSAUtil.doCipher(Cipher.DECRYPT_MODE, privateKey, encryptedMessage);

        System.out.println("解密后报文:" + new String(decryptedMessage));
    }

    // 私钥加密公钥解密
    public static void testEncryptByPrivateKeyDecryptByPublicKey(PublicKey publicKey, PrivateKey privateKey) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException {
        var message = "雪容融";

        var encryptedMessage = RSAUtil.doCipher(Cipher.ENCRYPT_MODE, privateKey, message.getBytes(StandardCharsets.UTF_8));
        var decryptedMessage = RSAUtil.doCipher(Cipher.DECRYPT_MODE, publicKey, encryptedMessage);

        System.out.println("解密后报文:" + new String(decryptedMessage));
    }
}






 
 

























输出如下:

解密后报文:冰墩墩
解密后报文:雪容融

现在,我们实现了生成密钥对,加载密钥,加解密报文的方法。此时你可能迫不及待的想找一段报文加密一下,以此检验刚才的劳动成果。

于是你不知道从哪里复制了这么一段话:

冰墩墩(英文:Bing Dwen Dwen,汉语拼音:bīng dūn dūn),是2022年北京冬季奥运会的吉祥物。将熊猫形象与富有超能量的冰晶外壳相结合,头部外壳造型取自冰雪运动头盔,装饰彩色光环,整体形象酷似航天员。

并把它粘贴到了你的代码中:

public class App {

    public static void main(String[] args) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeySpecException, InvalidKeyException {
        var keyPair = RSAUtil.generateKeyPair();
        var rsaKeyPair = RSAKeyPair.of(keyPair);

        var publicKey = RSAUtil.KeyLoader.loadPublicKey(rsaKeyPair.getPublicKey());
        var privateKey = RSAUtil.KeyLoader.loadPrivateKey(rsaKeyPair.getPrivateKey());

        testEncryptByPublicKeyDecryptByPrivateKey(publicKey, privateKey);
    }

    // 公钥加密私钥解密
    public static void testEncryptByPublicKeyDecryptByPrivateKey(PublicKey publicKey, PrivateKey privateKey) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException {
        var message = "冰墩墩(英文:Bing Dwen Dwen,汉语拼音:bīng dūn dūn),是2022年北京冬季奥运会的吉祥物。将熊猫形象与富有超能量的冰晶外壳相结合,头部外壳造型取自冰雪运动头盔,装饰彩色光环,整体形象酷似航天员。";

        var encryptedMessage = RSAUtil.doCipher(Cipher.ENCRYPT_MODE, publicKey, message.getBytes(StandardCharsets.UTF_8));
        var decryptedMessage = RSAUtil.doCipher(Cipher.DECRYPT_MODE, privateKey, encryptedMessage);

        System.out.println("解密后报文:" + new String(decryptedMessage));
    }
}

当你满怀期待的运行你的程序时,jvm却毫不留情的给你抛了个异常:

Exception in thread "main" javax.crypto.IllegalBlockSizeException: Data must not be longer than 117 bytes
	at java.base/com.sun.crypto.provider.RSACipher.doFinal(RSACipher.java:348)
	at java.base/com.sun.crypto.provider.RSACipher.engineDoFinal(RSACipher.java:405)
	at java.base/javax.crypto.Cipher.doFinal(Cipher.java:2202)
	at ink.laoli.RSAUtil.doCipher(RSAUtil.java:32)
	at ink.laoli.App.testEncryptByPublicKeyDecryptByPrivateKey(App.java:32)
	at ink.laoli.App.main(App.java:25)
 






不过还好,jvm还是给了你一点点提示,Data must not be longer than 117 bytes。这就引出了另一个知识点,让我们接着往下看。

RSA 算法可以加解密的数据的最大长度

还记得最开始我们将密钥的长度设置为1024吗?

RSA来说,密钥的长度决定了算法可以加解密的数据的长度。例如密钥长度为1024,那最多可以加密的明文长度就是1024/8 = 128字节,这128字节是包含padding在内的。这是因为当明文长度小于可加密的最大长度时需要用padding进行补齐。

只要用到了padding,就会占用实际的明文的空间。我们一般使用的padding标准有NoPPadding、OAEPPadding、PKCS1Padding等,其中PKCS#1建议的padding就占用了11个字节。所以当密钥长度为1024时,加密的明文的最大长度就是1024/8 - 11 = 117字节。是不是和jvm给你的温馨提示对上了?

现在我们知道了单次最多只能对KEY_SIZE / 8 - 11字节的明文加密,下面就好办了,只需要将原始的明文按KEY_SIZE / 8 - 11分段加密就行了。

先定义两个常量:

// 单次加密的最大明文长度
public static final int MAX_ENCRYPT_BLOCK = KEY_SIZE / 8 - 11;

// 单次解密的最大密文长度
public static final int MAX_DECRYPT_BLOCK = KEY_SIZE / 8;

接下来对doCipher方法进行改造,使其支持分段加解密:

public static byte[] doCipher(int opmode, Key key, int maxBlockSize, byte[] data) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IOException, IllegalBlockSizeException, BadPaddingException {
    var cipher = Cipher.getInstance(key.getAlgorithm());
    cipher.init(opmode, key);

    try (var result = new ByteArrayOutputStream()) {
        var dataLength = data.length;
        var cache = new byte[0];

        for (int i = 0, offset = 0; dataLength - offset > 0; i++, offset = i * maxBlockSize) {
            if (dataLength - offset > maxBlockSize) {
                cache = cipher.doFinal(data, offset, maxBlockSize);
            } else {
                cache = cipher.doFinal(data, offset, dataLength - offset);
            }
            result.write(cache, 0, cache.length);
        }
        return result.toByteArray();
    }
}

使用try-with-resources语法进行对资源的释放,通过ioffset配合循环在原始数据上进行分段加解密。

此时完整的工具类代码如下:

public final class RSAUtil {

    private static final Base64.Decoder Base64Decoder = Base64.getDecoder();

    // 加密算法
    public static final String KEY_ALGORITHM = "RSA";

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

    // 单次加密的最大明文长度
    public static final int MAX_ENCRYPT_BLOCK = KEY_SIZE / 8 - 11;

    // 单次解密的最大密文长度
    public static final int MAX_DECRYPT_BLOCK = KEY_SIZE / 8;

    public static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
        var keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM);
        keyPairGenerator.initialize(KEY_SIZE);
        return keyPairGenerator.genKeyPair();
    }

    public static byte[] doCipher(int opmode, Key key, int maxBlockSize, byte[] data) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IOException, IllegalBlockSizeException, BadPaddingException {
        var cipher = Cipher.getInstance(key.getAlgorithm());
        cipher.init(opmode, key);

        try (var result = new ByteArrayOutputStream()) {
            var dataLength = data.length;
            var cache = new byte[0];

            for (int i = 0, offset = 0; dataLength - offset > 0; i++, offset = i * maxBlockSize) {
                if (dataLength - offset > maxBlockSize) {
                    cache = cipher.doFinal(data, offset, maxBlockSize);
                } else {
                    cache = cipher.doFinal(data, offset, dataLength - offset);
                }
                result.write(cache, 0, cache.length);
            }
            return result.toByteArray();
        }
    }

    public static final class KeyLoader {

        public static PublicKey loadPublicKey(byte[] keyData) throws NoSuchAlgorithmException, InvalidKeySpecException {
            var keySpec = new X509EncodedKeySpec(keyData, KEY_ALGORITHM);
            var keyFactory = KeyFactory.getInstance(keySpec.getAlgorithm());
            return keyFactory.generatePublic(keySpec);
        }

        public static PrivateKey loadPrivateKey(byte[] keyData) throws NoSuchAlgorithmException, InvalidKeySpecException {
            var keySpec = new PKCS8EncodedKeySpec(keyData, KEY_ALGORITHM);
            var keyFactory = KeyFactory.getInstance(keySpec.getAlgorithm());
            return keyFactory.generatePrivate(keySpec);
        }

        public static PublicKey loadPublicKey(String keyText) throws NoSuchAlgorithmException, InvalidKeySpecException {
            return loadPublicKey(Base64Decoder.decode(keyText));
        }

        public static PrivateKey loadPrivateKey(String keyText) throws NoSuchAlgorithmException, InvalidKeySpecException {
            return loadPrivateKey(Base64Decoder.decode(keyText));
        }
    }
}

更新测试类如下:

public class App {

    public static void main(String[] args) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeySpecException, InvalidKeyException, IOException {
        var keyPair = RSAUtil.generateKeyPair();
        var rsaKeyPair = RSAKeyPair.of(keyPair);

        var publicKey = RSAUtil.KeyLoader.loadPublicKey(rsaKeyPair.getPublicKey());
        var privateKey = RSAUtil.KeyLoader.loadPrivateKey(rsaKeyPair.getPrivateKey());

        testEncryptByPublicKeyDecryptByPrivateKey(publicKey, privateKey);
    }

    // 公钥加密私钥解密
    public static void testEncryptByPublicKeyDecryptByPrivateKey(PublicKey publicKey, PrivateKey privateKey) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException {
        var message = "冰墩墩(英文:Bing Dwen Dwen,汉语拼音:bīng dūn dūn),是2022年北京冬季奥运会的吉祥物。将熊猫形象与富有超能量的冰晶外壳相结合,头部外壳造型取自冰雪运动头盔,装饰彩色光环,整体形象酷似航天员。";

        var encryptedMessage = RSAUtil.doCipher(Cipher.ENCRYPT_MODE, publicKey, RSAUtil.MAX_ENCRYPT_BLOCK, message.getBytes(StandardCharsets.UTF_8));
        var decryptedMessage = RSAUtil.doCipher(Cipher.DECRYPT_MODE, privateKey, RSAUtil.MAX_DECRYPT_BLOCK, encryptedMessage);

        System.out.println("解密后报文:" + new String(decryptedMessage));
    }
}

运行输出如下:

解密后报文:冰墩墩(英文:Bing Dwen Dwen,汉语拼音:bīng dūn dūn),是2022年北京冬季奥运会的吉祥物。将熊猫形象与富有超能量的冰晶外壳相结合,头部外壳造型取自冰雪运动头盔,装饰彩色光环,整体形象酷似航天员。

ok,完美完成任意长度数据的加解密!

关于加解密到这里就可以结束了,但是在实际的开发当中,直接调用doCipher方法还是比较繁琐的,让我们继续添加一些有用的方法以便使用。

一些有用的方法

public static byte[] encryptByPublicKey(byte[] data, String publicKeyText) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, IOException, BadPaddingException, InvalidKeyException {
    var publicKey = KeyLoader.loadPublicKey(publicKeyText);
    return doCipher(Cipher.ENCRYPT_MODE, publicKey, MAX_ENCRYPT_BLOCK, data);
}

public static byte[] decryptByPrivateKey(byte[] data, String privateKeyText) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, IOException, BadPaddingException, InvalidKeyException {
    var privateKey = KeyLoader.loadPrivateKey(privateKeyText);
    return doCipher(Cipher.DECRYPT_MODE, privateKey, MAX_DECRYPT_BLOCK, data);
}

public static byte[] encryptByPrivateKey(byte[] data, String privateKeyText) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, IOException, BadPaddingException, InvalidKeyException {
    var privateKey = KeyLoader.loadPrivateKey(privateKeyText);
    return doCipher(Cipher.ENCRYPT_MODE, privateKey, MAX_ENCRYPT_BLOCK, data);
}

public static byte[] decryptByPublicKey(byte[] data, String publicKeyText) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, IOException, BadPaddingException, InvalidKeyException {
    var publicKey = KeyLoader.loadPublicKey(publicKeyText);
    return doCipher(Cipher.DECRYPT_MODE, publicKey, MAX_DECRYPT_BLOCK, data);
}

public static String encryptByPublicKeyToBase64(String text, String publicKeyText) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, IOException, BadPaddingException, InvalidKeyException {
    return Base64Encoder.encodeToString(encryptByPublicKey(text.getBytes(StandardCharsets.UTF_8), publicKeyText));
}

public static String decryptBase64TextByPrivateKey(String base64Data, String privateKeyText) throws NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, InvalidKeySpecException, IOException, BadPaddingException, InvalidKeyException {
    return new String(decryptByPrivateKey(Base64Decoder.decode(base64Data), privateKeyText));
}

public static String encryptByPrivateKeyToBase64(String text, String privateKeyText) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, IOException, BadPaddingException, InvalidKeyException {
    return Base64Encoder.encodeToString(encryptByPrivateKey(text.getBytes(StandardCharsets.UTF_8), privateKeyText));
}

public static String decryptBase64TextByPublicKey(String base64Data, String publicKeyText) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, IOException, BadPaddingException, InvalidKeyException {
    return new String(decryptByPublicKey(Base64Decoder.decode(base64Data), publicKeyText));
}

public static String encryptByPublicKeyToBase64(byte[] data, String publicKeyText) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, IOException, BadPaddingException, InvalidKeyException {
    return Base64Encoder.encodeToString(encryptByPublicKey(data, publicKeyText));
}

这些方法见名知义,不多解释。

测试:

public class App {

    public static void main(String[] args) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, InvalidKeySpecException, IOException, BadPaddingException, InvalidKeyException {
        var keyPair = RSAUtil.generateKeyPair();
        var rsaKeyPair = RSAKeyPair.of(keyPair);

        testEncryptByPublicKeyDecryptByPrivateKey(rsaKeyPair.getPublicKey(), rsaKeyPair.getPrivateKey());
        testEncryptByPrivateKeyDecryptByPublicKey(rsaKeyPair.getPublicKey(), rsaKeyPair.getPrivateKey());
    }

    // 公钥加密私钥解密
    public static void testEncryptByPublicKeyDecryptByPrivateKey(String publicKeyText, String privateKeyText) throws NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, InvalidKeySpecException, IOException, BadPaddingException, InvalidKeyException {
        var message = "冰墩墩(英文:Bing Dwen Dwen,汉语拼音:bīng dūn dūn),是2022年北京冬季奥运会的吉祥物。将熊猫形象与富有超能量的冰晶外壳相结合,头部外壳造型取自冰雪运动头盔,装饰彩色光环,整体形象酷似航天员。";

        var encryptedMessage = RSAUtil.encryptByPublicKeyToBase64(message, publicKeyText);
        var decryptedMessage = RSAUtil.decryptBase64TextByPrivateKey(encryptedMessage, privateKeyText);

        System.out.println("解密后报文:" + decryptedMessage);
    }

    // 私钥加密公钥解密
    public static void testEncryptByPrivateKeyDecryptByPublicKey(String publicKeyText, String privateKeyText) throws NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, InvalidKeySpecException, IOException, BadPaddingException, InvalidKeyException {
        var message = "雪容融(Shuey Rhon Rhon),是2022年北京冬季残奥会的吉祥物,其以灯笼为原型进行设计创作,主色调为红色,头顶有如意环与外围的剪纸图案,面部带有不规则形状的雪块,身体可以向外散发光芒。";

        var encryptedMessage = RSAUtil.encryptByPrivateKeyToBase64(message, privateKeyText);
        var decryptedMessage = RSAUtil.decryptBase64TextByPublicKey(encryptedMessage, publicKeyText);

        System.out.println("解密后报文:" + decryptedMessage);
    }
}

运行结果如下:

解密后报文:冰墩墩(英文:Bing Dwen Dwen,汉语拼音:bīng dūn dūn),是2022年北京冬季奥运会的吉祥物。将熊猫形象与富有超能量的冰晶外壳相结合,头部外壳造型取自冰雪运动头盔,装饰彩色光环,整体形象酷似航天员。
解密后报文:雪容融(Shuey Rhon Rhon),是2022年北京冬季残奥会的吉祥物,其以灯笼为原型进行设计创作,主色调为红色,头顶有如意环与外围的剪纸图案,面部带有不规则形状的雪块,身体可以向外散发光芒。

完整代码

package ink.laoli;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

public final class RSAUtil {

    private static final Base64.Encoder Base64Encoder = Base64.getEncoder();

    private static final Base64.Decoder Base64Decoder = Base64.getDecoder();

    // 加密算法
    public static final String KEY_ALGORITHM = "RSA";

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

    // 单次加密的最大明文长度
    public static final int MAX_ENCRYPT_BLOCK = KEY_SIZE / 8 - 11;

    // 单次解密的最大密文长度
    public static final int MAX_DECRYPT_BLOCK = KEY_SIZE / 8;

    public static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
        var keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM);
        keyPairGenerator.initialize(KEY_SIZE);
        return keyPairGenerator.genKeyPair();
    }

    public static byte[] encryptByPublicKey(byte[] data, String publicKeyText) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, IOException, BadPaddingException, InvalidKeyException {
        var publicKey = KeyLoader.loadPublicKey(publicKeyText);
        return doCipher(Cipher.ENCRYPT_MODE, publicKey, MAX_ENCRYPT_BLOCK, data);
    }

    public static byte[] decryptByPrivateKey(byte[] data, String privateKeyText) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, IOException, BadPaddingException, InvalidKeyException {
        var privateKey = KeyLoader.loadPrivateKey(privateKeyText);
        return doCipher(Cipher.DECRYPT_MODE, privateKey, MAX_DECRYPT_BLOCK, data);
    }

    public static byte[] encryptByPrivateKey(byte[] data, String privateKeyText) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, IOException, BadPaddingException, InvalidKeyException {
        var privateKey = KeyLoader.loadPrivateKey(privateKeyText);
        return doCipher(Cipher.ENCRYPT_MODE, privateKey, MAX_ENCRYPT_BLOCK, data);
    }

    public static byte[] decryptByPublicKey(byte[] data, String publicKeyText) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, IOException, BadPaddingException, InvalidKeyException {
        var publicKey = KeyLoader.loadPublicKey(publicKeyText);
        return doCipher(Cipher.DECRYPT_MODE, publicKey, MAX_DECRYPT_BLOCK, data);
    }

    public static String encryptByPublicKeyToBase64(String text, String publicKeyText) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, IOException, BadPaddingException, InvalidKeyException {
        return Base64Encoder.encodeToString(encryptByPublicKey(text.getBytes(StandardCharsets.UTF_8), publicKeyText));
    }

    public static String decryptBase64TextByPrivateKey(String base64Data, String privateKeyText) throws NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, InvalidKeySpecException, IOException, BadPaddingException, InvalidKeyException {
        return new String(decryptByPrivateKey(Base64Decoder.decode(base64Data), privateKeyText));
    }

    public static String encryptByPrivateKeyToBase64(String text, String privateKeyText) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, IOException, BadPaddingException, InvalidKeyException {
        return Base64Encoder.encodeToString(encryptByPrivateKey(text.getBytes(StandardCharsets.UTF_8), privateKeyText));
    }

    public static String decryptBase64TextByPublicKey(String base64Data, String publicKeyText) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, IOException, BadPaddingException, InvalidKeyException {
        return new String(decryptByPublicKey(Base64Decoder.decode(base64Data), publicKeyText));
    }

    public static String encryptByPublicKeyToBase64(byte[] data, String publicKeyText) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, IOException, BadPaddingException, InvalidKeyException {
        return Base64Encoder.encodeToString(encryptByPublicKey(data, publicKeyText));
    }

    public static byte[] doCipher(int opmode, Key key, int maxBlockSize, byte[] data) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IOException, IllegalBlockSizeException, BadPaddingException {
        var cipher = Cipher.getInstance(key.getAlgorithm());
        cipher.init(opmode, key);

        try (var result = new ByteArrayOutputStream()) {
            var dataLength = data.length;
            var cache = new byte[0];

            for (int i = 0, offset = 0; dataLength - offset > 0; i++, offset = i * maxBlockSize) {
                if (dataLength - offset > maxBlockSize) {
                    cache = cipher.doFinal(data, offset, maxBlockSize);
                } else {
                    cache = cipher.doFinal(data, offset, dataLength - offset);
                }
                result.write(cache, 0, cache.length);
            }
            return result.toByteArray();
        }
    }

    public static final class KeyLoader {

        public static PublicKey loadPublicKey(byte[] keyData) throws NoSuchAlgorithmException, InvalidKeySpecException {
            var keySpec = new X509EncodedKeySpec(keyData, KEY_ALGORITHM);
            var keyFactory = KeyFactory.getInstance(keySpec.getAlgorithm());
            return keyFactory.generatePublic(keySpec);
        }

        public static PrivateKey loadPrivateKey(byte[] keyData) throws NoSuchAlgorithmException, InvalidKeySpecException {
            var keySpec = new PKCS8EncodedKeySpec(keyData, KEY_ALGORITHM);
            var keyFactory = KeyFactory.getInstance(keySpec.getAlgorithm());
            return keyFactory.generatePrivate(keySpec);
        }

        public static PublicKey loadPublicKey(String keyText) throws NoSuchAlgorithmException, InvalidKeySpecException {
            return loadPublicKey(Base64Decoder.decode(keyText));
        }

        public static PrivateKey loadPrivateKey(String keyText) throws NoSuchAlgorithmException, InvalidKeySpecException {
            return loadPrivateKey(Base64Decoder.decode(keyText));
        }
    }
}

最近在做一个C#的项目,顺便给出部分实现:

using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Encodings;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Security;
using System;
using System.IO;
using System.Text;

namespace InkLaoli
{
    sealed class RSAUtil
    {
        private static readonly int KEY_SIZE = 2048;

        private static readonly int MAX_ENCRYPT_BLOCK = KEY_SIZE / 8 - 11;

        private static readonly int MAX_DECRYPT_BLOCK = KEY_SIZE / 8;

        public static byte[] EncryptByPublicKey(byte[] data, string publicKeyText)
        {
            var publicKey = PublicKeyFactory.CreateKey(Convert.FromBase64String(publicKeyText));
            return DoCipher(true, publicKey, MAX_ENCRYPT_BLOCK, data);
        }

        public static string EncryptByPublicKeyToBase64(string text, string publicKeyText)
        {
            return Convert.ToBase64String(EncryptByPublicKey(Encoding.UTF8.GetBytes(text), publicKeyText));
        }

        public static byte[] DecryptByPrivateKey(byte[] data, string privateKeyText)
        {
            var privateKey = PrivateKeyFactory.CreateKey(Convert.FromBase64String(privateKeyText));
            return DoCipher(false, privateKey, MAX_DECRYPT_BLOCK, data);
        }

        public static string DecryptBase64TextByPrivateKey(string text, string privateKeyText)
        {
            return Encoding.UTF8.GetString(DecryptByPrivateKey(Convert.FromBase64String(text), privateKeyText));
        }

        private static byte[] DoCipher(bool forEncryption, ICipherParameters parameters, int maxBlockSize, byte[] data)
        {
            var cipher = new Pkcs1Encoding(new RsaEngine());
            cipher.Init(forEncryption, parameters);

            using (var result = new MemoryStream())
            {
                var dataLength = data.Length;
                var cache = new byte[0];

                for (int i = 0, offset = 0; dataLength - offset > 0; i++, offset = i * maxBlockSize)
                {
                    if (dataLength - offset > maxBlockSize)
                    {
                        cache = cipher.ProcessBlock(data, offset, maxBlockSize);
                    } else
                    {
                        cache = cipher.ProcessBlock(data, offset, dataLength - offset);
                    }
                    result.Write(cache, 0, cache.Length);
                }
                return result.ToArray();
            }
        }
    }
}

C#的实现中使用了第三方库BouncyCastleopen in new window,如需了解详情请自行查看文档。

(完)