Java 实现RSA签名和加密

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调用时通常会使用私钥进行签名,接收方通过公钥进行验证,确保请求身份及内容完整。

  • 6
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
RSA加密签名都是基于RSA算法的,但是加密签名的目的和流程有所不同。 RSA加密流程如下: 1. 选择两个大质数p和q,计算n=p*q,并记为RSA模数。 2. 计算欧拉函数φ(n)=(p-1)*(q-1)。 3. 选择一个整数e,1<e<φ(n),且e与φ(n)互质,e作为公钥指数。 4. 计算d,使得d*e≡1(mod φ(n)),d作为私钥指数。 5. 加密时,对明文m进行加密,计算c=m^e(mod n)。 6. 解密时,对密文c进行解密,计算m=c^d(mod n)。 RSA签名流程如下: 1. 选择两个大质数p和q,计算n=p*q,并记为RSA模数。 2. 计算欧拉函数φ(n)=(p-1)*(q-1)。 3. 选择一个整数e,1<e<φ(n),且e与φ(n)互质,e作为签名者的私钥指数。 4. 计算d,使得d*e≡1(mod φ(n)),d作为签名者的公钥指数。 5. 对消息m进行签名,计算s=m^d(mod n)。 6. 验证签名时,对接收到的消息m和签名s进行验证,计算m'=s^e(mod n),如果m'=m,则验证通过。 下面是Java代码示例: RSA加密: ```java import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.PrivateKey; import java.security.PublicKey; import javax.crypto.Cipher; public class RSAEncrypt { public static void main(String[] args) throws Exception { String plainText = "Hello, world!"; KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); kpg.initialize(2048); KeyPair kp = kpg.generateKeyPair(); PublicKey publicKey = kp.getPublic(); PrivateKey privateKey = kp.getPrivate(); Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.ENCRYPT_MODE, publicKey); byte[] encryptedText = cipher.doFinal(plainText.getBytes()); System.out.println("加密后的密文:" + new String(encryptedText)); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] decryptedText = cipher.doFinal(encryptedText); System.out.println("解密后的明文:" + new String(decryptedText)); } } ``` RSA签名: ```java import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.PrivateKey; import java.security.PublicKey; import java.security.Signature; public class RSASign { public static void main(String[] args) throws Exception { String plainText = "Hello, world!"; KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); kpg.initialize(2048); KeyPair kp = kpg.generateKeyPair(); PublicKey publicKey = kp.getPublic(); PrivateKey privateKey = kp.getPrivate(); Signature signature = Signature.getInstance("SHA256withRSA"); signature.initSign(privateKey); signature.update(plainText.getBytes()); byte[] signedText = signature.sign(); System.out.println("签名后的内容:" + new String(signedText)); signature.initVerify(publicKey); signature.update(plainText.getBytes()); boolean result = signature.verify(signedText); System.out.println(result ? "验签成功" : "验签失败"); } } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值