非对称加密算法 - RSA(结合Java keytool与openssl工具生成证书)

DH算法的诞生为后续非对称加密算法奠定了基础,较为典型的对称加密算法(如ElGamal、RSA和ECC算法等)都是在DH算法提出后相继出现的,并且其算法核心都源于数学问题。

RSA算法基于大数因子分解难题,而ElGamal算法和ECC算法则是基于离散对数难题。

目前,各种主流计算机语言都提供了对RSA算法的支持。在Java 7中,我们可以很方便地构建该算法。

一、简述

1978年,美国麻省理工学院(MIT)的Ron Rivest、Adi Shamir和Leonard Adleman三位学者提出了一种新的非对称加密算法,这种算法以这三位学者的姓氏开头字母命名,被称为RSA算法。

RSA算法是唯一被广泛接受并实现的通用公开加密算法,目前已经成为非对称加密算法国际标准。不仅如此,RSA算法既可用于数据加密也可用于数字签名。我们熟知的电子邮件加密软件PGP就采用了RSA算法对数据进行加密/解密和数字签名处理。

RSA算法将非对称加密算法推向了一个高潮,但它并不是没有缺点。相比DES以及其他对称加密算法而言,RSA算法要慢得多。作为加密算法,尤其是作为非对称加密算法的典型,针对RSA算法的破解自诞生之日起就从未停止过。

  • 1999年,RSA-155(512位)被成功因数分解,历时5个月。
  • 2002年,RSA-158也被成功因数分解。

当然,基于大数因子分解数学难题的非对称加密算法仅有RSA算法这一种,但这不代表非对称加密算法除了RSA算法后继无人。基于离散对数问题的非对称加密算法包括ElGamal和ECC这两种算法,它们同样是优秀的非对称加密算法。

二、模型分析

我们继续以消息传递模型为例,阐述基于RSA算法的消息传递模型是如何工作的。

 图8-5 构建RSA算法密钥对

通过了解基于DH算法的消息传递模型,我们更容易理解基于非对称加密算法的消息传递模型的工作方式。或者说,基于DH算法的消息传递模型更为底层。

RSA算法代表了真正的非对称加密算法,其操作较之DH算法较为简单。在构建密钥对方面,RSA算法就相当简单,如图8-5所示。

在图8-5中,我们仍以甲乙两方收发消息为例。甲方作为消息的发送方,乙方作为消息的接收方。我们假设甲乙双方在消息传递之前已将RSA算法作为消息传递的加密算法。为完成加密消息传递,甲乙双方需要以下操作:

  1. 由消息发送的一方构建密钥对,这里由甲方完成;
  2. 由消息发送的一方公布公钥至消息接收方,这里由甲方将公钥公布给乙方。

完成这两步操作后,甲乙双方就可以进行加密消息传递了,如图8-6和图8-7所示。

 图8-6 甲方向乙方发送RSA算法加密数据

这里需要读者朋友注意:甲方向乙方发送数据时,使用私钥对数据做加密处理;乙方接收到加密数据后,使用公钥对数据做解密处理。

我们需要注意,在非对称加密算法领域中,对于私钥加密的数据,只能使用公钥解密。简言之,“私钥加密,公钥解密”。

相对于“私钥加密,公钥解密”的实现,RSA算法提供了另一种加密/解密方式:“公钥加密,私钥解密”。这使得乙方可以使用公钥加密数据,如图8-7所示。

图8-7 乙方向甲方发送RSA算法加密数据

甲方可以向乙方发送加密消息,乙方也同样可以向甲方发送加密消息。在图8-7中,乙方使用公钥加密消息,甲方则使用私钥对加密消息解密。这是一种“公钥加密,私钥解密”的加密/解密方式。

用公钥加密数据的方式是否可取呢?公钥是通过甲方发送给乙方的,其在传递过程中很有可能被截获,也就是说窃听者很有可能获得公钥。如果窃听者获得了公钥,向甲方发送数据,甲方是无法辨别消息的真伪的。因此,虽然可以使用公钥对数据加密,但这种方式还是会有存在一定的安全隐患。如果要建立更安全的加密消息传递模型,就需要甲乙两方构建两套非对称加密算法密钥,仅遵循“私钥加密,公钥解密”的方式进行加密消息传递。

三、实现

Java 7提供了RSA算法实现,相关内容请读者朋友阅读第3章。RSA算法在理解上较之DH算法更为简单,在实现层面上也同样如此。

RSA算法与DH算法在密钥管理和加密/解密两方面有所不同:一方面,甲方保留了私钥,而将公钥公布于乙方,甲乙双方密钥一一对应;另一方面,私钥用于解密,公钥则用于加密,反之亦然。

这里要注意一点,在RSA算法中,公钥既可用于解密也可用于加密,私钥也是如此。但公钥加密的数据只能用私钥对其解密,而私钥加密的数据只能用公钥对其解密,这种对应关系是不能违背的。

RSA算法实现较之DH算法实现较为简单,大部分内容与DH算法较为接近。与DH算法不同的是,RSA算法仅需要一套密钥即可完成加密/解密操作。Bouncy Castle对RSA算法做了相应扩充,增加了ISO9796-1Padding填充方式。

有关RSA算法的Java 7实现与Bouncy Castle实现细节如表8-3所示。

 完整实现如代码清单8-12所示。

代码清单8-12 RSA算法实现

import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
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;
import javax.crypto.Cipher;
/**
 * RSACoderTest
 *
 * @author Anonyms
 * @since 2023/3/16 10:55:00
 */
public abstract class RSACoder {
    // 非对称加密密钥算法
    public static final String KEY_ALGORITHM = "RSA";
    // 公钥
    private static final String PUBLIC_KEY = "RSAPublicKey";
    // 私钥
    private static final String PRIVATE_KEY = "RSAPrivateKey";
    /**
     * RSA密钥长度
     * 默认1024位,
     * 密钥长度必须是64的倍数,
     * 范围在512~65536位之间。
     */
    private static final int KEY_SIZE = 512;
    /**
     * 私钥解密
     * @param data 待解密数据
     * @param key 私钥
     * @return byte[] 解密数据
     * @throws Exception
     */
    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[] 解密数据
     * @throws Exception
     */
    public static byte[] decryptByPublicKey(byte[] data, byte[] key) throws Exception {
            // 取得公钥
            X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(key);
            KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
            // 生成公钥
            PublicKey publicKey = keyFactory.generatePublic(x509KeySpec);
            // 对数据解密
            Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
            cipher.init(Cipher.DECRYPT_MODE, publicKey);
            return cipher.doFinal(data);
    }
    /**
     * 公钥加密
     * @param data 待加密数据
     * @param key 公钥
     * @return byte[] 加密数据
     * @throws Exception
     */
    public static byte[] encryptByPublicKey(byte[] data, byte[] key) throws Exception {
            // 取得公钥
            X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(key);
            KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
            PublicKey publicKey = keyFactory.generatePublic(x509KeySpec);
            // 对数据加密
            Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
            return cipher.doFinal(data);
    }
    /**
     * 私钥加密
     * @param data 待加密数据
     * @param key 私钥
     * @return byte[] 加密数据
     * @throws Exception
     */
    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 keyMap 密钥Map
     * @return byte[] 私钥
     * @throws Exception
     */
    public static byte[] getPrivateKey(Map<String, Object> keyMap) throws Exception {
            Key key = (Key) keyMap.get(PRIVATE_KEY);
            return key.getEncoded();
    }
    /**
     * 取得公钥
     * @param keyMap 密钥Map
     * @return byte[] 公钥
     * @throws Exception
     */
    public static byte[] getPublicKey(Map<String, Object> keyMap) throws Exception {
            Key key = (Key) keyMap.get(PUBLIC_KEY);
            return key.getEncoded();
    }
    /**
     * 初始化密钥
     * @return Map 密钥Map
     * @throws Exception
     */
    public static Map<String, Object> initKey() throws Exception {
            // 实例化密钥对生成器
            KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance (KEY_ALGORITHM);
            // 初始化密钥对生成器
            keyPairGen.initialize(KEY_SIZE);
            // 生成密钥对
            KeyPair keyPair = keyPairGen.generateKeyPair();
            // 公钥
            RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
            // 私钥
            RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
            // 封装密钥
            Map<String, Object> keyMap = new HashMap<String, Object>(2);
            keyMap.put(PUBLIC_KEY, publicKey);
            keyMap.put(PRIVATE_KEY, privateKey);
            return keyMap;
    }
}

RSA算法实现易于理解,对于RSA算法的测试只需要注意经公钥加密的数据是否可以通过私钥将其解密,反之,经私钥加密的数据是否可以通过公钥将其解密。完整的测试用例如代码清单8-13所示。

代码清单8-13 RSA算法实现测试用例

import static org.junit.Assert.*;
import org.apache.commons.codec.binary.Base64;
import org.junit.Before;
import org.junit.Test;
import java.util.Map;
/**
 * RSACoderTest
 *
 * @author Anonyms
 * @since 2023/3/16 10:55:00
 */
public class RSACoderTest {
    // 公钥
    private byte[] publicKey;
    // 私钥
    private byte[] privateKey;
    /**
     * 初始化密钥
     * @throws Exception
     */
    @Before
    public void initKey() throws Exception {
           // 初始化密钥
           Map<String, Object> keyMap = RSACoder.initKey();
           publicKey = RSACoder.getPublicKey(keyMap);
           privateKey = RSACoder.getPrivateKey(keyMap);
           System.err.println("公钥:\n" + Base64.encodeBase64String(publicKey));
           System.err.println("私钥:\n" + Base64.encodeBase64String(privateKey));
    }
    /**
     * 校验
     * @throws Exception
     */
    @Test
    public void test() throws Exception {
           System.err.println("\n---私钥加密--公钥解密---");
           String inputStr1 = "RSA加密算法";
           byte[] data1 = inputStr1.getBytes();
           System.err.println("原文:\n" + inputStr1);
           // 加密
           byte[] encodedData1 = RSACoder.encryptByPrivateKey(data1, privateKey);
           System.err.println("加密后:\n" + Base64.encodeBase64String (encodedData1));
           // 解密
           byte[] decodedData1 = RSACoder.decryptByPublicKey(encodedData1,
                      publicKey);
            String outputStr1 = new String(decodedData1);
            System.err.println("解密后:\n" + outputStr1);
            // 校验
            assertEquals(inputStr1, outputStr1);
            System.err.println("\n---公钥加密--私钥解密---");
            String inputStr2 = "RSA Encypt Algorithm";
            byte[] data2 = inputStr2.getBytes();
            System.err.println("原文:\n" + inputStr2);
            // 加密
            byte[] encodedData2 = RSACoder.encryptByPublicKey(data2, publicKey);
            System.err.println("加密后:\n" + Base64.encodeBase64String (encodedData2));
            // 解密
            byte[] decodedData2 = RSACoder.decryptByPrivateKey(encodedData2,
                       privateKey);
            String outputStr2 = new String(decodedData2);
            System.err.println("解密后: " + outputStr2);
            // 校验
            assertEquals(inputStr2, outputStr2);
    }
}

在控制台中输出了密钥的Base64编码信息,如下文所示:

公钥: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJ0PYlHiU439wWfWvQKMEgjjE/swks9KCG0UJy2qmSBs 2e76f0eTswnNl7nFxbBr5TRRyl4EhkudWDIr0u6kBgcCAwEAAQ== 私钥: MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAnQ9iUeJTjf3BZ9a9AowSCOMT+zCS z0oIbRQnLaqZIGzZ7vp/R5OzCc2XucXFsGvlNFHKXgSGS51YMivS7qQGBwIDAQABAkAjMB4sEFP9 /PtG43KHTpB/0zhXz8MklAadQaWhcpZKEB3LRINLH4jJ2UJxGWXMDS5nZtbG3/1jYd2Ee0bwGb7B AiEA8M/t/6MB4Yx4C4OBkd51TDByKJmKIEnFu2HSyzA6c2ECIQCm9zi9OuX2N/5rVytXfA+Oj7L1 wetSOrGbEHXX3D/aZwIhAMRHJlyr13eoj5wK1ww2/vJXtmSjKPNCThl6FV8p8yphAiBVJyC44aEG wefvtrVUGOGWQ5Nx40Sw215ZRzvSq3GlYQIhALIHY3fxmLlEg3NC269ouFsYeTF/EO+M02+pazz+ 3UPv 

与DH算法不同,RSA算法仅需要一套密钥即可完成加密/解密操作,并且我们发现公钥的密钥长度明显小于私钥的密钥长度,更便于发送和携带。私钥加密公钥解密,如下所示:

---私钥加密--公钥解密--- 原文: RSA加密算法 加密后: EePGm+yWtFvgSvc1pmh1hNoy3KyH0gssjc2FlvPSNkFAOOFOvvVIPQAmeRtTD+L3oUKUC61zQeqf N2B/t0ylxg== 解密后: RSA加密算法

反之,公钥加密私钥解密,如下所示:

---公钥加密--私钥解密--- 原文: RSA Encypt Algorithm 加密后: hehNcGA8EGilNk3FJ7snGOU9XKGHN7t6DJlHQG9Ddi+h/xdk/IzWs3+SJfFsEnrTQe+96UvpEmF2 atA7+Fndgw== 解密后: RSA Encypt Algorithm

RSA算法公钥长度远小于私钥长度,并遵循“公钥加密,私钥解密”和“私钥加密,公钥解密”这两项加密/解密原则。

四、使用Java的keytool生成RSA的非对称密钥证书

Java自带的工具keytool可以用来生成密钥证书,也可以查看或删除证书库里的证书。无论是什么类型的操作系统,这个工具都会出现在jdk安装目录的bin文件夹下。下图是Mac系统下的截图:

 keytool工具是基于“证书库”来对密钥进行管理的。“证书库”是一个后缀名为“.keystore”的文件。

1、创建证书并保存证书

命令格式如下:

keytool -genkey -alias 你的证书别名 -keyalg 密钥算法 -keystore 证书库文件保存的位置和文件名 -keysize 密钥长度 -validity 证书有效期天数

参数含义:

  1. -genkey:创建证书;
  2. -alias:证书的别名。在一个证书库文件中,别名是唯一用来区分多个证书的标识符;
  3. -keyalg:密钥的算法,非对称加密的话就是RSA;
  4. -keystore:证书库文件保存的位置和文件名。如果路径写错的话,会出现报错信息。如果在路径下,证书库文件不存在,那么就会创建一个;
  5. -keysize:密钥长度,一般都是1024;
  6. -validity:证书的有效期,单位是天。比如36500的话,就是100年;

我们的创建证书命令如下:

keytool -genkey -alias token_key_0303 -keyalg RSA -keystore /Users/mazongzhu/dev_tools/mykeystore/on1_keystore.keystore -keysize 1024 -validity 36500

然后,提示如下信息:

输入密钥库口令:

注意这是“证书库”的密码,而不是你所要生成的证书的密码,这是两个不同的密码。这里我们输入的密码为 230303,输入完成后展示如下:

  • 您的名字与姓氏是什么?
  • 您的组织单位名称是什么?
  • 您的组织名称是什么?
  • 您所在的城市或区域名称是什么?
  • 您所在的省/市/自治区名称是什么?
  • 该单位的双字母国家/地区代码是什么?

为了方便,以上这些提示我们都不填写,直接按回车后,提示如下信息:

CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown是否正确?
  [否]:

在这里我们输入y并回车。我们看到一个提示风险信息如下:

Warning:
生成的证书 使用的 1024 位 RSA 密钥 被视为存在安全风险。此密钥大小将在未来的更新中被禁用。

这是因为现在1024比特位的RSA都有安全风险了,为了增加安全一般都用3074甚至4096位了。另外,基于椭圆曲线的算法被认为用较短的密钥就能实现与RSA相当的安全性,现在越来越广泛了。

查看证书是否创建完成,执行以下命令(注意这里是需要输入密码的:230303):

keytool -list -keystore /Users/mazongzhu/dev_tools/mykeystore/on1_keystore.keystore

这个命令会向你列出这个证书文件里的所有证书,每个证书都是单独的一条记录如下图所示:

 这里我们就看到刚才我们使用创建命令中-alisa 的别名为token_key_0303了。

2、导出公钥文件

使用以下命令来从证书库里的某个证书中导出公钥,并保存成一份公钥文件:

keytool -export -alias token_key_0303 -keystore /Users/mazongzhu/dev_tools/mykeystore/on1_keystore.keystore -file /Users/mazongzhu/dev_tools/mykeystore/token_key_0303.cer

参数说明:

  • -export:用于导出公钥文件的命令参数;
  • -alias:你的证书在证书库中的别名,也是唯一标识;
  • -keystore:完整的证书库文件所在的目录及文件名;
  • -file:导出后的公钥文件所在的完整目录及文件名;

keytool仍然会要求你输入证书库的密码,注意是”证书库“的密码,按照上文的密码为:230303

我们在/Users/mazongzhu/dev_tools/mykeystore/token_key_0303.cer下,会看到该文件。特别需要注意的是,私钥是无法从证书库中导出的,因为那样非常不安全。如果你特别需要私钥或是私钥字符串,只能考虑用编程的方式从密钥库文件中去获取了。

之后,你就会在自己所指定的那个目录下,找到一个cer文件。一般来说你都可以直接双击打开这个文件,其中能够看到你的证书信息以及公钥信息。

cer转pem:

有时候我们的应用需要pem文件,比如Java应用,我们可以使用openssl来进行转换(openssl相关教程不在这里介绍),执行以下命令即可:

openssl x509 -inform der -in token_key_0303.cer -out token_key_0303.der

五、使用openssl生成Java可读证书

1、生成2048bit的RSA私钥

openssl genrsa -out token_key_0306.pem 2048

2、转换私钥为PKCS#8格式(Java可读取的)

openssl pkcs8 -topk8 -inform PEM -outform DER -in token_key_0306.pem -out token_private_key_0306.der -nocrypt

3、导出pem的共钥DER文件(Java可读的)

openssl rsa -in token_key_0306.pem -pubout -outform DER -out token_public_key_0306.der

4、Java读取公、私钥实现方式

public class RSAKeyReader {

    public static PrivateKey getPrivateKey(String filename)
            throws Exception {

        byte[] keyBytes = Files.readAllBytes(Paths.get(filename));

        PKCS8EncodedKeySpec spec =
                new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        return kf.generatePrivate(spec);
    }

    public static PublicKey getPublicKey(String filename)
            throws Exception {

        byte[] keyBytes = Files.readAllBytes(Paths.get(filename));

        X509EncodedKeySpec spec =
                new X509EncodedKeySpec(keyBytes);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        return kf.generatePublic(spec);
    }

    public static void main(String[] args) throws Exception {
        PrivateKey privateKey = getPrivateKey("/Users/mazongzhu/dev_tools/mykeystore/token_private_key_0306.der");
        System.out.println(privateKey.toString());
        PublicKey publicKey = getPublicKey("/Users/mazongzhu/dev_tools/mykeystore/token_public_key_0306.der");
        System.out.println(publicKey.toString());
    }
}

参考《Java加密与解密的艺术(第2版)》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值