Skip to main content
 首页 » 编程设计

Java 实现RSA签名和加密

2022年07月19日148小虾米

Java 实现RSA签名和加密

RSA在1977年发明,是公钥加密方式的事实标准,名称有其三位作者首字母组成。本文我们介绍Java中如何使用RSA实现加密和签名。

RSA属于非对称加密算法,有两个密钥。区别于共享密钥的对称加密算法,如DES和AES。公钥可以共享给任何人,私钥自己进行保管。公钥用于加密数据,使得该加密数据只能用私钥进行解密;私钥也可用于签名数据,签名和数据一起发送,然后使用公钥验证数据是否被篡改。

Java生成密钥对

在实际做任何形式加密之前,需要有公钥和私钥两个密钥对。幸运的是,Java提供了非常简单的方法,请看示例代码:

public static KeyPair generateKeyPair() throws Exception { 
    KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); 
    generator.initialize(2048, new SecureRandom()); 
    KeyPair pair = generator.generateKeyPair(); 
 
    return pair; 
} 

首先获得RSA KeyPairGenerator实例,然后使用2048为长度进行初始化并传入SecureRandom实例。后者用作生成器的熵或随机数据源。第三行生成密钥对,所有内容都就是这样,非常简单。如果需要使用密钥对存储器,需要使用Java KeyStore工具,后面会详细解释。

加密和解密

现在我们已经有了密钥对,下面我们下对消息进行加密,然后再解密。首先看加密方法:

public static String encrypt(String plainText, PublicKey publicKey) throws Exception { 
    Cipher encryptCipher = Cipher.getInstance("RSA"); 
    encryptCipher.init(Cipher.ENCRYPT_MODE, publicKey); 
 
    byte[] cipherText = encryptCipher.doFinal(plainText.getBytes(UTF_8)); 
 
    return Base64.getEncoder().encodeToString(cipherText); 
} 

注意:
这里使用base64编码,主要是典型的REST API中很常用,我们当然也可以不用base64编码而直接使用字节数组。
确保明确地且一致地为字符串指定字节编码。否则字节错位将导致密文和无法解密或验证签名。

示例中首先获得RSA密码实例并设置为加密模式,然后使用公钥加密消息。然后一次性传入消息字符串的字节数组并获得加密后的字节数组,最后转码并返回。

下面需要解密方法:

public static String decrypt(String cipherText, PrivateKey privateKey) throws Exception { 
    byte[] bytes = Base64.getDecoder().decode(cipherText); 
 
    Cipher decriptCipher = Cipher.getInstance("RSA"); 
    decriptCipher.init(Cipher.DECRYPT_MODE, privateKey); 
 
    return new String(decriptCipher.doFinal(bytes), UTF_8); 
} 

与上面加密代码几乎类似。首先获得RSA密码实例,使用私钥进行初始化,然后解密字节数组至字符串。完整代码如下:

//First generate a public/private key pair 
KeyPair pair = generateKeyPair(); 
 
//Our secret message 
String message = "the answer to life the universe and everything"; 
 
//Encrypt the message 
String cipherText = encrypt(message, pair.getPublic()); 
 
//Now decrypt it 
String decipheredMessage = decrypt(cipherText, pair.getPrivate()); 
 
System.out.println(decipheredMessage); 

运行示例,不一会即在控制台打印“the answer to life the universe and everything”。你应该注意到花了一会时间,运维RSA是相当慢,比对称加密算法如AES要慢的多。

签名和验证

我们使用公钥进行加密,然后使用私钥解密。理论上反过来也行(私钥加密,公钥解密),但这不安全且大多数库(包括java.security)也不支持。然而,这种方式在构建API时比较有用。使用私钥对消息进行签名,然后使用公钥进行验证签名。这种机制可以确保消息确实来着公钥创建者(私钥持有者),使得传输过程消息不会被篡改。下面先看签名方法:

public static String sign(String plainText, PrivateKey privateKey) throws Exception { 
    Signature privateSignature = Signature.getInstance("SHA256withRSA"); 
    privateSignature.initSign(privateKey); 
    privateSignature.update(plainText.getBytes(UTF_8)); 
 
    byte[] signature = privateSignature.sign(); 
 
    return Base64.getEncoder().encodeToString(signature); 
} 

看起来和加密/解密方法类似。 首先获得SHA256withRSA类型的Signature实例,使用私钥进行初始化,使用消息中的所有字节更新它(也可以使用部分块(例如大型文件)来做这件事),然后使用.sign()方法生成签名。最后返回base64编码字符串。

你可能想知道那个SHA256位是干什么的。如前所述,RSA是一个相当慢的算法。所以SHA256withRSA实际上并没有计算所有输入的签名(可能是千兆字节的数据),它实际上计算了整个输入的SHA 256,填充然后计算签名。如果感兴趣可以在RFC中描述整个过程。

下面看验证消息方法:

public static boolean verify(String plainText, String signature, PublicKey publicKey) throws Exception { 
    Signature publicSignature = Signature.getInstance("SHA256withRSA"); 
    publicSignature.initVerify(publicKey); 
    publicSignature.update(plainText.getBytes(UTF_8)); 
 
    byte[] signatureBytes = Base64.getDecoder().decode(signature); 
 
    return publicSignature.verify(signatureBytes); 
} 

也和前面过程类似。首先获得Signature实例,使用公钥设置验证,喂入消息文件字节数组然后使用签名字节数组看签名是否匹配。验证方法返回布尔类型表示签名是否有效。

完整代码如下:

KeyPair pair = generateKeyPair(); 
 
String signature = sign("foobar", pair.getPrivate()); 
 
//Let's check the signature 
boolean isCorrect = verify("foobar", signature, pair.getPublic()); 
System.out.println("Signature correct: " + isCorrect); 

输出应该为:“Signature correct: true”。尝试修改输入消息的字节或消息内容再次验证,应该返回验证失败。

Java KeyStore

生产环境一般不会动态生成密钥对,而是使用Java KeyStore。KeyStore提供存储机制在单个文件中存储多个键,该文件本身也是加密的(使用PBEWithMD5AndTripleDES,非常复杂),且同时有每个存储和每个密钥的密码。

使用jdk工具生成keystore:

keytool -genkeypair -alias mykey -storepass s3cr3t -keypass s3cr3t -keyalg RSA -keystore keystore.jks 

如果你的操作系统找不到keytool,请确认是否安装了jdk并在path中加入bin目录。

该命令会创建keystore.jks文件,包括存储在别名为mykey的公钥/私钥对,存储和key都使用密码保护。keytool请求一系列问题,可以不填,但最后一个问题需要你需要输入y,缺省为no,不能简单盲目按回车。

现在我们有jks文件,建议放在src/java/resources目录中,下面示例代码从存储中读取key:

public static KeyPair getKeyPairFromKeyStore() throws Exception { 
    InputStream ins = RsaExample.class.getResourceAsStream("/keystore.jks"); 
 
    KeyStore keyStore = KeyStore.getInstance("JCEKS"); 
    keyStore.load(ins, "s3cr3t".toCharArray());   //Keystore password 
    KeyStore.PasswordProtection keyPassword =       //Key password 
            new KeyStore.PasswordProtection("s3cr3t".toCharArray()); 
 
    KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry("mykey", keyPassword); 
 
    java.security.cert.Certificate cert = keyStore.getCertificate("mykey"); 
    PublicKey publicKey = cert.getPublicKey(); 
    PrivateKey privateKey = privateKeyEntry.getPrivateKey(); 
 
    return new KeyPair(publicKey, privateKey); 
} 

示例中首先通过getResourceAsStream方法打开类路径下的密钥存储文件。如果有需要可以将其调整为从FileInputStream中读取,当密钥存储不在类路径中时。
这里需要两次指定密码,一次是加载存储,另一次是key自身。在生产环境中这些密码通常是不同的且不能硬编码,所以要记住这一点!
通过“mykey”获得私钥和证书(公钥),然后使用它们创建密钥对。接下来可以和前面代码一样使用密钥对:

KeyPair pair = getKeyPairFromKeyStore(); 
 
String signature = sign("foobar", pair.getPrivate()); 
 
//Let's check the signature 
boolean isCorrect = verify("foobar", signature, pair.getPublic()); 
System.out.println("Signature correct: " + isCorrect); 

总结

本文介绍了非对称加密RSA算法及实现,应用场景包括于公钥加密私钥解密,或私钥签名公钥验证。不同系统之间通过API调用时通常会使用私钥进行签名,接收方通过公钥进行验证,确保请求身份及内容完整。


本文参考链接:https://blog.csdn.net/neweastsun/article/details/101780730
阅读延展