乐趣区

关于java:Java实现7种常见密码算法

原创:扣钉日记(微信公众号 ID:codelogs),欢送分享,转载请保留出处。

简介

后面在密码学入门一文中解说了各种常见的密码学概念、算法与使用场景,但没有介绍过代码,因而,为作补充,这一篇将会介绍应用 Java 语言如何实现应用这些算法,并介绍一下应用过程中可能遇到的坑。

Java 加密体系 JCA

Java 形象了一套明码算法框架 JCA(Java Cryptography Architecture),在此框架中定义了一套接口与类,以标准 Java 平台明码算法的实现,而 Sun,SunRsaSign,SunJCE 这些则是一个个 JCA 的实现 Provider,以实现具体的明码算法,这有点像 List 与 ArrayList、LinkedList 的关系一样,Java 开发者只须要应用 JCA 即可,而不必管具体是怎么实现的。

JCA 里定义了一系列类,如 Cipher、MessageDigest、MAC、Signature 等,别离用于实现加密、密码学哈希、认证码、数字签名等算法,一起来看看吧!

对称加密

对称加密算法,应用 Cipher 类即可,以宽泛应用的 AES 为例,如下:

public byte[] encrypt(byte[] data, Key key) {
    try {Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        byte[] iv = SecureRandoms.randBytes(cipher.getBlockSize());
        // 初始化密钥与加密参数 iv
        cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
        // 加密
        byte[] encryptBytes = cipher.doFinal(data);
        // 将 iv 与密文拼在一起
        ByteArrayOutputStream baos = new ByteArrayOutputStream(iv.length + encryptBytes.length);
        baos.write(iv);
        baos.write(encryptBytes);
        return baos.toByteArray();} catch (Exception e) {return ExceptionUtils.rethrow(e);
    }
}

public byte[] decrypt(byte[] data, Key key) {
    try {Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        // 获取密文后面的 iv
        IvParameterSpec ivSpec = new IvParameterSpec(data, 0, cipher.getBlockSize());
        cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
        // 解密 iv 前面的密文
        return cipher.doFinal(data, cipher.getBlockSize(), data.length - cipher.getBlockSize());
    } catch (Exception e) {return ExceptionUtils.rethrow(e);
    }
}

如上,对称加密次要应用 Cipher,不论是 AES 还是 DES,Cipher.getInstance()传入不同的算法名称即可,这里的 Key 参数就是加密时应用的密钥,稍后会介绍它是怎么来的,临时先疏忽它。
另外,为了使得每次加密进去的密文不同,我应用了随机的 iv 向量,并将 iv 向量拼接在了密文后面。

注:如果某个算法名称,如下面的AES/CBC/PKCS5Padding,你不晓得它在 JCA 中的规范名称是什么,能够到 https://docs.oracle.com/en/ja… 中查问即可。

非对称加密

非对称加密同样是应用 Cipher 类,只是传入的密钥对象不同,以 RSA 算法为例,如下:

public byte[] encryptByPublicKey(byte[] data, PublicKey publicKey){
    try{Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        return cipher.doFinal(data);
    }catch (Exception e) {throw Errors.toRuntimeException(e);
    }
}

public byte[] decryptByPrivateKey(byte[] data, PrivateKey privateKey){
    try{Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        return cipher.doFinal(data);
    }catch (Exception e) {throw Errors.toRuntimeException(e);
    }
}

一般来说应应用公钥加密,私钥解密,但其实反过来也是能够的,这里的 PublicKey 与 PrivateKey 也先疏忽,前面会介绍它怎么来的。

密码学哈希

密码学哈希算法包含 MD5、SHA1、SHA256 等,在 JCA 中都应用 MessageDigest 类即可,如下:

public static String sha256(byte[] bytes) throws NoSuchAlgorithmException {MessageDigest digest = MessageDigest.getInstance("SHA-256");
    digest.update(bytes);
    return Hex.encodeHexString(digest.digest());
}

音讯认证码

音讯认证码应用 Mac 类实现,以常见的 HMAC 搭配 SHA256 为例,如下:

public byte[] digest(byte[] data, Key key) throws InvalidKeyException, NoSuchAlgorithmException{Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(key);
    return mac.doFinal(data);
}

数字签名

数字签名应用 Signature 类实现,以 RSA 搭配 SHA256 为例,如下:

public byte[] sign(byte[] data, PrivateKey privateKey) {
    try {Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initSign(privateKey);
        signature.update(data);
        return signature.sign();} catch (Exception e) {return ExceptionUtils.rethrow(e);
    }
}

public boolean verify(byte[] data, PublicKey publicKey, byte[] sign) {
    try {Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initVerify(publicKey);
        signature.update(data);
        return signature.verify(sign);
    } catch (Exception e) {return ExceptionUtils.rethrow(e);
    }
}

密钥协商算法

在 JCA 中,应用 KeyAgreement 来调用密钥协商算法,以 ECDH 协商算法为例,如下:

public static void testEcdh() {KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
    ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1");
    keyGen.initialize(ecSpec);
    // A 生成本人的私密信息
    KeyPair keyPairA = keyGen.generateKeyPair();
    KeyAgreement kaA = KeyAgreement.getInstance("ECDH");
    kaA.init(keyPairA.getPrivate());
    // B 生成本人的私密信息
    KeyPair keyPairB = keyGen.generateKeyPair();
    KeyAgreement kaB = KeyAgreement.getInstance("ECDH");
    kaB.init(keyPairB.getPrivate());

    // B 收到 A 发送过去的公用信息,计算出对称密钥
    kaB.doPhase(keyPairA.getPublic(), true);
    byte[] kBA = kaB.generateSecret();

    // A 收到 B 发送过去的公开信息,计算对对称密钥
    kaA.doPhase(keyPairB.getPublic(), true);
    byte[] kAB = kaA.generateSecret();
    Assert.isTrue(Arrays.equals(kBA, kAB), "协商的对称密钥不统一");
}

基于口令加密 PBE

通常,对称加密算法须要应用 128 位字节的密钥,但这么长的密钥用户是记不住的,用户容易记住的是口令,也即 password,但与密钥相比,口令有如下弱点:

  1. 口令通常较短,这使得间接应用口令加密的强度较差。
  2. 口令随机性较差,因为用户个别应用较容易记住的货色来生成口令。

为了使得用户能间接应用口令加密,又能最大水平防止口令的弱点,于是 PBE(Password Based Encryption)算法诞生,思路如下:

  1. 既然明码算法须要密钥,那在加解密前,先应用口令生成密钥,而后再应用此密钥去加解密。
  2. 为了补救口令随机性较差的问题,生成密钥时应用随机盐来混同口令来产生准密钥,再应用散列函数对准密钥进行屡次散列迭代,以生成最终的密钥。

因而,应用 PBE 算法进行加解密时,除了要提供口令外,还须要提供随机盐 (salt) 与迭代次数(iteratorCount),如下:

public static byte[] encrypt(byte[] plainBytes, String password, byte[] salt, int iteratorCount) {
    try {PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
        SecretKey key = SecretKeyFactory.getInstance("PBEWithMD5AndTripleDES").generateSecret(keySpec);

        Cipher cipher = Cipher.getInstance("PBEWithMD5AndTripleDES");
        cipher.init(Cipher.ENCRYPT_MODE, key, new PBEParameterSpec(salt, iteratorCount));
        byte[] encryptBytes = cipher.doFinal(plainBytes);
        byte[] iv = cipher.getIV();
        ByteArrayOutputStream baos = new ByteArrayOutputStream(iv.length + encryptBytes.length);
        baos.write(iv);
        baos.write(encryptBytes);
        return baos.toByteArray();} catch (Exception e) {throw Errors.toRuntimeException(e);
    }
}

public static byte[] decrypt(byte[] secretBytes, String password, byte[] salt, int iteratorCount) {
    try {PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
        SecretKey key = SecretKeyFactory.getInstance("PBEWithMD5AndTripleDES").generateSecret(keySpec);

        Cipher cipher = Cipher.getInstance("PBEWithMD5AndTripleDES");
        IvParameterSpec ivParameterSpec = new IvParameterSpec(secretBytes, 0, cipher.getBlockSize());
        cipher.init(Cipher.DECRYPT_MODE, key, new PBEParameterSpec(salt, iteratorCount, ivParameterSpec));
        return cipher.doFinal(secretBytes, cipher.getBlockSize(), secretBytes.length - cipher.getBlockSize());
    } catch (Exception e) {throw Errors.toRuntimeException(e);
    }
}

public static void main(String[] args) throws Exception {byte[] content = "hello".getBytes(StandardCharsets.UTF_8);
    byte[] salt = Base64.decode("QBadPOP6/JM=");
    String password = "password";
    byte[] encoded = encrypt(content, password, salt, 1000);
    System.out.println("密文:" + Base64.encode(encoded));
    byte[] plainBytes = decrypt(encoded, password, salt, 1000);
    System.out.println("明文:" + new String(plainBytes, StandardCharsets.UTF_8));
}

留神,尽管应用 PBE 加解密数据,都须要应用雷同的 password、salt、iteratorCount,但这外面只有 password 是须要窃密的,salt 与 iteratorCount 不须要,能够保留在数据库中,比方每个用户注册时给他生成一个随机盐。

到此,JCA 明码算法就介绍完了,来回顾一下:

整体来说,JCA 对明码算法相干的类设计与封装还是十分清晰简略的!

但应用明码算法时,依赖 SecretKey、PublicKey、PrivateKey 对象提供密钥信息,那这些密钥对象是怎么来的呢?

密钥生成与读取

密码学随机数

密码学随机数算法在平安场景中应用宽泛,如:生成对称密钥、盐、iv 等,因而相比一般的随机数算法(如线性同余),它须要更高强度的不可预测性,在 Java 中,应用 SecureRandom 来生成更平安的随机数,如下:

public class SecureRandoms {public static byte[] randBytes(int len) throws NoSuchAlgorithmException {byte[] bytes = new byte[len];
        SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
        secureRandom.nextBytes(bytes);
        return bytes;
    }
}

SecureRandom 应用了更高强度的随机算法,同时会读取机器自身的随机熵值,如/dev/urandom,因而相比一般的 Random,它具备更强的随机性,因而,对于须要生成密钥的场景,该用哪个要拧得清。

对称密钥

在 JCA 中对称密钥应用 SecretKey 示意,若要生成一个新的 SecretKey,可应用 KeyGenerator,如下:

// 生成新的密钥
public static SecretKey genSecretKey() {KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
    keyGenerator.init(SecureRandom.getInstance("SHA1PRNG"));
    SecretKey secretKey = keyGenerator.generateKey();}

而如果是从文件中读取密钥的话,则能够借助 SecretKeyFactory 将其转换为 SecretKey,如下:

// 读取密钥
public static SecretKey getSecretKey() {byte[] keyBytes = readKeyBytes();
    String alg = "AES";
    SecretKey secretKey = SecretKeyFactory.getInstance(alg).generateSecret(new SecretKeySpec(keyBytes, alg));
}

非对称密钥

在 JCA 中,对于非对称密钥,公钥应用 PublicKey 示意,私钥应用 PrivateKey 示意,若要生成一个新的公私钥对,可应用 KeyPairGenerator,如下:

// 生成新的公私钥对
public static void genKeyPair() {KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
    keyPairGen.initialize(2048);
    KeyPair keyPair = keyPairGen.generateKeyPair();
    PublicKey publicKey = keyPair.getPublic();
    PrivateKey privateKey = keyPair.getPrivate();}

而如果是从文件中读取公私钥的话,个别公钥是 X509 格局,而私钥是 PKCS8 格局,别离对应 JCA 中的 X509EncodedKeySpec 与 PKCS8EncodedKeySpec,如下:

// 读取私钥
public static PrivateKey getPrivateKey() {byte[] privateKeyBytes = readPrivateKeyBytes();
    PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
    PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(pkcs8EncodedKeySpec);
}

// 读取公钥
public static PublicKey getPublicKey() {byte[] publicKeyBytes = readPublicKeyBytes();
    X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyBytes);
    PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec);
}

留神,KeyGenerator、KeyPairGenerator 与 KeyFactory 从命名上看起来有点类似,但它们实现的性能是齐全不同的,KeyGenerator、KeyPairGenerator 用于生成新的密钥,而 KeyFactory 则用于将 KeySpec 转换为对应的 Key 密钥对象。

JCA 密钥相干类关系一览,如下:

常见问题

密文无奈解密问题

有时,在应用明码算法时,会发现他人提供的密文应用正确的密钥却无奈解密进去,特地容易产生在跨语言的状况下,如加密方应用的 C# 语言,而解密方却应用的 Java。

遇到这种状况,你须要和对方认真确认加密时应用的加密模式、填充模式以及 IV 等明码参数是否完全一致。

如 AES 算法加密模式有 ECBCBCCFBCTRGCM 等,填充模式有 PKCS#5, ISO 10126, ANSI X9.23 等,以及对方是应用了固定的 IV 向量还是将 IV 向量拼在了密文中,这些都须要确认分明并与对方保持一致能力正确解密。

签名失败问题

签名失败也是应用明码算法时常见的状况,比方对方生成的 MD5 值与你生成的 MD5 不统一,常见有 2 种起因,如下:
1. 应用的字符编码不统一导致
明码算法为了通用性,操作对象都是字节数组,而你要签名的对象个别是字符串,因而你须要将字符串转为字节数组之后再做 md5 运算,如下:

  • 调用方:md5(str.getBytes())
  • 服务方:md5(str.getBytes())

看起来两边的代码截然不同,但问题就在 getBytes() 函数中,getBytes()函数默认会应用操作系统的字符编码将字符串转为字节数组,而中文 Windows 默认字符编码是 GBK,而 Linux 默认是 UTF-8,这就导致当 str 中有中文时,调用方与服务方获取到的字节数组是不一样的,那生成的 MD5 值当然也不一样了。

因而,强烈推荐在应用 getBytes() 函数时,传入对立的字符编码,如下:

  • 调用方:md5(str.getBytes("UTF-8"))
  • 服务方:md5(str.getBytes("UTF-8"))
    这样就能无效地避过这个十分费解的坑了。

2. json 的 escape 性能导致
有些 json 框架,做 json 序列化时会默认做一些本义操作,如把 & 字符本义为\u0026,但如果服务端做 json 反序列化时没有做反本义,这会导致两边计算的签名值不一样,如下:

  • 调用方:md5("&")
  • 服务方:md5("\\u0026")
    这也是一个十分费解的坑,如 Gson 默认就会有这种行为,可应用 new GsonBuilder().disableHtmlEscaping() 禁用。

生成与读取证书

概念

随着对密码学理解的深刻,会发现有特地多奇怪的名词呈现,让人蛊惑不已,如 PKCS8X.509ASN.1DERPEM 等,接下来就来廓清下这些名词是什么,以及它们之间的关系。

首先,理解 3 个概念,如下:

  • 密钥:包含对称密钥与非对称密钥等。
  • 证书:蕴含用户或网站的身份信息、公钥,以及 CA 的签名。
  • 密钥库:用于存储密钥与证书的仓库。

ASN.1 语法

ASN.1 形象语法标记(Abstract Syntax Notation One),和 XML、JSON 相似,用于形容对象构造,能够把它看成一种描述语言,简略的示例如下:

Report ::= SEQUENCE {
author OCTET STRING,
title OCTET STRING,
body OCTET STRING,
}

这个语法形容了一个构造体,它蕴含 3 个属性 author、title、body,且都是字符串类型。

DER 与 PEM

DER 是 ASN.1 的一种序列化编码方案,也就是说 ASN.1 用来形容对象构造,而 DER 用于将此对象构造编码为可存储的字节数组。

PEM(Privacy Enhanced Mail)是一种将二进制数据,以文本模式进行存储或传输的计划,晚期次要用于邮件中替换证书,它的文本内容常以 -----BEGIN XXX----- 结尾,并以 -----END XXX----- 结尾,而两头 Body 局部则为 Base64 编码后的数据,如下是一个证书的 PEM 样例。

以下面证书为例,PEM 与 DER 的关系大略如下:

PEM = "-----BEGIN CERTIFICATE-----" + base64(DER) +  "-----END CERTIFICATE-----"

X.509、PKCS8、PKCS12 等

X.509、PKCS8、PKCS12 等都是公钥密码学规范 (PKCS) 组织制订的各种密码学标准,该组织应用 ASN.1 语法为密钥、证书、密钥库等定义了规范的对象构造,常见的如下:

  • X.509 标准:用于形容证书与公钥的规范格局。
  • PKCS7 标准:可形容的对象很多,不过个别也是用于形容证书的。
  • PKCS8 标准:用于形容私钥的规范格局。
  • PKCS12 标准:用于形容密钥库的规范格局。
  • PKCS1 标准:用于形容 RSA 算法及其公私钥的规范格局。

这些标准都有相应的 RFC 文档,感兴趣的能够返回查看:

PEM:https://www.rfc-editor.org/rfc/rfc7468   
X.509:https://datatracker.ietf.org/doc/html/rfc5280  
PKCS7:https://datatracker.ietf.org/doc/html/rfc2315  
PKCS8:https://datatracker.ietf.org/doc/html/rfc8351  
PKCS12:https://datatracker.ietf.org/doc/html/rfc7292  
PKCS1:https://datatracker.ietf.org/doc/html/rfc8017#appendix-A  

类比一下,如果把 ASN.1 比作 Java,那 X.509 就是应用 Java 定义的一个名叫 X509 的类,这个类外面蕴含身份信息、公钥信息等相干字段,而 DER 就是一种 Java 对象序列化计划,用于将 X509 这个类的对象序列化为字节数组,字节数组保留为文件后,这个文件就是咱们常说的证书或密钥文件。

常见证书文件

因为 PKCS 组织并未给证书文件定下规范的文件名后缀,所以证书文件有十分多的后缀名,如下:

  • .der: DER 编码的证书,个别是 X.509 标准的,无奈用文本编辑器间接关上
  • .pem: PEM 编码的证书,个别是 X.509 标准的
  • .crt: 常见于 unix 类零碎,个别是 X.509 标准的,可能是 DER 编码或 PEM 编码
  • .cer: 常见于 windows 零碎,个别是 X.509 标准的,可能是 DER 编码或 PEM 编码
  • .p7b: 常见于 windows 零碎,PKCS7 标准证书,可能是 DER 编码或 PEM 编码
  • .pfx:PKCS12 标准的密钥库文件,也有取名为.p12 的
  • .jks:java 专用的密钥库文件格式,在 java 技术栈内应用较多,非 java 个别应用.pfx

证书概念小结

生成证书与密钥库

openssl 命令提供了大量的工具,用以生成密钥、证书与密钥库文件,如下,是一个典型的生成密钥与证书的过程:

# 生成 pkcs1 rsa 私钥
openssl genrsa -out rsa_private_key_pkcs1.key 2048
# 生成 pkcs1 rsa 公钥
openssl rsa -in rsa_private_key_pkcs1.key -RSAPublicKey_out -out rsa_public_key_pkcs1.key

# 生成证书申请文件 cert.csr
openssl req -new -key rsa_private_key_pkcs1.key -out cert.csr
# 自签名(演示时应用,生产环境个别不必自签证书)  
openssl x509 -req -days 365 -in cert.csr -signkey rsa_private_key_pkcs1.key -out cert.crt
# ca 签名(将证书申请文件提交给 ca 机构签名)
openssl x509 -req -days 365 -in cert.csr -CA ca_cert.crt -CAkey ca_private_key.pem -CAcreateserial -out cert.crt

# 生成 p12 密钥库文件
openssl pkcs12 -export -in cert.crt -inkey rsa_private_key_pkcs1.key -name demo -out keystore.p12

有时他人发来的密钥或证书文件无奈读取,也可应用 openssl 确认一下,如果 openssl 能读出来,那大概率是本人程序有问题,如果 openssl 读不进去,那大概率是他人发的文件有问题,如下:

# 查看 pkcs1 rsa 私钥
openssl rsa -in rsa_private_key_pkcs1.key -text -noout
# 查看 pkcs1 rsa 公钥
openssl rsa -RSAPublicKey_in -in rsa_public_key_pkcs1.key -text -noout

# 查看 x.509 证书
openssl x509 -in cert.crt -text -nocert

# 查看 pkcs12 密钥库文件
openssl pkcs12 -in keystore.p12
keytool -v -list -storetype pkcs12 -keystore keystore.p12

因为密钥、证书、密钥库文件,其实都是应用 ASN.1 语法形容的,所以它们都能按 ASN.1 语法解析进去,如下:

openssl asn1parse -i -inform pem -in cert.crt

证书格局转换

某些状况下,咱们须要在不同格局的密钥或证书文件之间转换,也可应用 openssl 命令来实现。
密钥格局转换,如下:

# rsa 公钥转换为 X509 公钥
openssl rsa -RSAPublicKey_in -in rsa_public_key_pkcs1.key -pubout -out public_key_x509.key
# rsa 私钥转换为 PKCS8 格局
openssl pkcs8 -topk8 -inform PEM -in rsa_private_key_pkcs1.key -outform PEM -nocrypt -out private_key_pkcs8.key
# pkcs8 转 rsa 私钥
openssl pkcs8 -inform PEM -nocrypt -in private_key_pkcs8.key -traditional -out rsa_private_key_pkcs1.key

证书格局转换,如下:

# 证书 DER 转 PEM
openssl x509 -inform der -in cert.der -outform pem -out cert.pem -noout
# x509 证书转 pkcs7 证书
openssl crl2pkcs7 -nocrl -certfile cert.crt -out cert.p7b
# 查看 pkcs7 证书
openssl pkcs7 -print_certs -in cert.p7b -noout

因为密钥库中蕴含证书与私钥,故能够从密钥库文件中提取出证书与私钥,如下:

# 从 pkcs12 密钥库中提取证书
openssl pkcs12 -in keystore.p12 -clcerts -nokeys -out cert.crt
# 从 pkcs12 密钥库中提取私钥
openssl pkcs12 -in keystore.p12 -nocerts -nodes -out private_key.key
# pkcs12 转 jks
keytool -importkeystore -srckeystore keystore.p12 -srcstoretype pkcs12 -srcalias demo -destkeystore keystore.jks -deststoretype jks -deststorepass 123456 -destalias demo
# 从 jks 中提取证书
keytool -export -alias demo -keystore keystore.jks -file cert.crt

读取密钥或证书文件

应用 JCA 来读取密钥或证书文件,也是十分不便的。

PEM 转 DER

若要将 PEM 格式文件转换为 DER,只须要把 ---BEGIN XXX------END XXX---去掉,而后应用 Base64 解码即可,如下:

private static byte[] pemFileToDerBytes(String pemFilePath) throws IOException {InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream(pemFilePath);
    String pemStr = StreamUtils.copyToString(is, StandardCharsets.UTF_8);
    // 去掉 ---BEGIN XXX--- 与 ---END XXX---
    pemStr = pemStr.replaceAll("---+[^-]+---+", "")
            .replaceAll("\\s+", "");
    //base64 解码为 DER 二进制内容
    return Base64.getDecoder().decode(pemStr);
}

读取 PKCS8 私钥

在 JCA 中,应用 PKCS8EncodedKeySpec 解析 PKCS8 私钥文件,如下:

public static void testPkcs8PrivateKeyFile() {byte[] derBytes = pemFileToDerBytes("cert/private_key_pkcs8.key");
    PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(derBytes);
    RSAPrivateCrtKey rsaPrivateCrtKey = (RSAPrivateCrtKey)KeyFactory.getInstance("RSA").generatePrivate(pkcs8EncodedKeySpec);
    BigInteger n = rsaPrivateCrtKey.getModulus();
    BigInteger e = rsaPrivateCrtKey.getPublicExponent();
    BigInteger d = rsaPrivateCrtKey.getPrivateExponent();
    System.out.printf("n: %X \n e: %X \n d: %X \n", n, e, d);
    BigInteger plain = BigInteger.valueOf(new Random().nextInt(1000000000));
    // RSA 加密
    long t1 = System.nanoTime();
    BigInteger secret = plain.modPow(e, n);
    long t2 = System.nanoTime();
    // RSA 解密
    BigInteger plain2 = secret.modPow(d, n);
    long t3 = System.nanoTime();
    System.out.printf("plain: %d \n plain2: %d \n", plain, plain2);
    System.out.printf("enc time: %d \n", (t2 - t1));
    System.out.printf("dec time: %d \n", (t3 - t2));
}

读取 X.509 公钥

在 JCA 中,应用 X509EncodedKeySpec 解析 X.509 公钥文件,如下:

public static void testX509PublicKeyFile() {byte[] derBytes = pemFileToDerBytes("cert/public_key_x509.key");
    X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(derBytes);
    RSAPublicKey rsaPublicKey = (RSAPublicKey)KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec);
    BigInteger e = rsaPublicKey.getPublicExponent();
    BigInteger n = rsaPublicKey.getModulus();
    System.out.printf("e: %X \n n: %X \n", e, n);
}

读取 X.509 证书

读取 X.509 证书文件,可应用 CertificateFactory 类,如下:

public static void testX509CertFile() {byte[] derBytes = pemFileToDerBytes("cert/cert.crt");
    Collection<? extends Certificate> certificates = CertificateFactory.getInstance("X.509")
            .generateCertificates(new ByteArrayInputStream(derBytes));
    for(Certificate certificate : certificates){X509Certificate x509Certificate = (X509Certificate)certificate;
        System.out.printf("SubjectDN: %s \n", x509Certificate.getSubjectDN());
        System.out.printf("IssuerDN: %s \n", x509Certificate.getIssuerDN());
        System.out.printf("SigAlgName: %s \n", x509Certificate.getSigAlgName());
        System.out.printf("Signature: %s \n", Hex.encodeHexString(x509Certificate.getSignature()));
        System.out.printf("PublicKey: %s \n", x509Certificate.getPublicKey());
    }
}

读取 PKCS12 密钥库文件

读取 PKCS12 标准的密钥库文件,可应用 KeyStore 类,如下:

public static void testPkcs12File() {KeyStore keyStore = KeyStore.getInstance("PKCS12");
    InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("cert/keystore.p12");
    char[] password = "123456".toCharArray();
    keyStore.load(is, password);
    // 获取证书
    X509Certificate x509Certificate = (X509Certificate)keyStore.getCertificate("demo");
    System.out.println("X509Certificate:");
    System.out.printf("SubjectDN: %s \n", x509Certificate.getSubjectDN());
    System.out.printf("IssuerDN: %s \n", x509Certificate.getIssuerDN());
    System.out.printf("SigAlgName: %s \n", x509Certificate.getSigAlgName());
    System.out.printf("Signature: %s \n", Hex.encodeHexString(x509Certificate.getSignature()));
    System.out.printf("PublicKey: %s \n", x509Certificate.getPublicKey());
    // 获取私钥
    Key key = keyStore.getKey("demo", password);
    System.out.printf("PrivateKey: %s \n", key);
}

如果要读取 .jks 文件,只须要将 KeyStore.getInstance("PKCS12") 中的 PKCS12 更换为 JKS 即可,其它局部放弃不变,不过因为 JKS 是 java 专有格局,目前 java 也不举荐应用了,所以能不必的话,就尽量不要用了。

常见问题

证书信赖问题

证书的绝大多数利用场景是 Https 协定,但在拜访 https 接口时,有时会因为证书信赖问题导致 https 握手失败,次要有以下 2 点起因:

  1. 有些公司会自建 CA,应用自签证书,如晚期的 12306,而 jdk 只信赖它预置的根证书,所以 https 握手时这种证书会认证失败。
  2. 新成立的根 CA 机构证书,没预置在旧的 jdk 外面,导致这些 CA 机构签发的证书不被信赖。

要解决这种证书信赖问题,有两种办法,如下:
1. 将证书导致到 jdk 的预置证书库中

# 将 cert.crt 导入 jdk 预置密钥库文件,密钥库文件明码默认是 changeit
sudo keytool -importcert -file cert.crt -alias demo -keystore $JAVA_HOME/jre/lib/security/cacerts -storepass changeit

# 查看密钥库文件,查看是否导入胜利
keytool -list -v -alias demo -keystore $JAVA_HOME/jre/lib/security/cacerts -storepass changeit

2. 以编码的形式信赖证书
以 jdk 自带的 https sdk 为例,可在代码中手动将问题证书增加到信赖列表中,如下:

public String testReqHttpsTrustCert() throws Exception {
    // 读取 jdk 预置证书
    KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
    try(InputStream ksIs = new FileInputStream(System.getProperty("java.home") + "/lib/security/cacerts")) {keyStore.load(ksIs, "changeit".toCharArray());
    }

    // 读取证书文件
    CertificateFactory cf = CertificateFactory.getInstance("X.509");
    try(InputStream certIs = this.getClass().getResourceAsStream("/cert/cert.crt")) {Certificate c = cf.generateCertificate(certIs);
        keyStore.setCertificateEntry("demo", c);
    }

    // 生成信赖管理器
    TrustManagerFactory tmFact = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmFact.init(keyStore);

    // 生成 SSLSocketFactory
    SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
    sslContext.init(null, tmFact.getTrustManagers(), new SecureRandom());
    SSLSocketFactory ssf = sslContext.getSocketFactory();

    // 发送 https 申请
    URL url = new URL("https://www.demo.com/user/list");
    HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
    connection.setHostnameVerifier((hostname, session) -> hostname.endsWith("demo.com"));
    connection.setSSLSocketFactory(ssf);

    String result;
    try(InputStream inputStream = connection.getInputStream()){result = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
    }
    connection.disconnect();
    return result;
}

注:尽管 2 种办法都能够解决问题,但第 1 种办法使得 java 程序对环境造成了依赖,一旦部署环境发生变化,java 程序可能就报错了,因而更举荐应用第 2 种办法。

总结

到这里,JCA 相干类的应用就介绍完了,如下表格中总结了 JCA 的罕用类:

本篇花了近一周工夫整顿,内容较多,对这块不太熟悉的同学,能够先关注珍藏起来当示例手册,待须要时再参阅即可。

退出移动版