原创:扣钉日记(微信公众号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,但与密钥相比,口令有如下弱点:
- 口令通常较短,这使得间接应用口令加密的强度较差。
- 口令随机性较差,因为用户个别应用较容易记住的货色来生成口令。
为了使得用户能间接应用口令加密,又能最大水平防止口令的弱点,于是PBE(Password Based Encryption)算法诞生,思路如下:
- 既然明码算法须要密钥,那在加解密前,先应用口令生成密钥,而后再应用此密钥去加解密。
- 为了补救口令随机性较差的问题,生成密钥时应用随机盐来混同口令来产生准密钥,再应用散列函数对准密钥进行屡次散列迭代,以生成最终的密钥。
因而,应用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算法加密模式有ECB
、CBC
、CFB
、CTR
、GCM
等,填充模式有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()
禁用。
生成与读取证书
概念
随着对密码学理解的深刻,会发现有特地多奇怪的名词呈现,让人蛊惑不已,如PKCS8
、X.509
、ASN.1
、DER
、PEM
等,接下来就来廓清下这些名词是什么,以及它们之间的关系。
首先,理解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.csropenssl 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.p12keytool -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转PEMopenssl 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转jkskeytool -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点起因:
- 有些公司会自建CA,应用自签证书,如晚期的12306,而jdk只信赖它预置的根证书,所以https握手时这种证书会认证失败。
- 新成立的根CA机构证书,没预置在旧的jdk外面,导致这些CA机构签发的证书不被信赖。
要解决这种证书信赖问题,有两种办法,如下:
1. 将证书导致到jdk的预置证书库中
# 将cert.crt导入jdk预置密钥库文件,密钥库文件明码默认是changeitsudo 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的罕用类:
本篇花了近一周工夫整顿,内容较多,对这块不太熟悉的同学,能够先关注珍藏起来当示例手册,待须要时再参阅即可。