您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > JAVA

Java实现7种常见密码算法

时间:2022-10-22 14:35:31  来源:今日头条  作者:扣钉日记

简介

前面在密码学入门一文中讲解了各种常见的密码学概念、算法与运用场景,但没有介绍过代码,因此,为作补充,这一篇将会介绍使用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/java/javase/11/docs/specs/security/standard-names.html 中查询即可。

非对称加密

非对称加密同样是使用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算法加密模式有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

证书概念小结

 

Certificate

生成证书与密钥库

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的常用类:

 

本篇花了近一周时间整理,内容较多,对这块不太熟悉的同学,可以先关注收藏起来当示例手册,待需要时再参阅即可。



Tags:Java   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
JavaScript的异步编程常见模式
在JavaScript中,异步编程是一种处理长时间运行操作(如网络请求或I/O操作)的常见方式。它允许程序在等待这些操作完成时继续执行其他任务,从而提高应用程序的响应性和性能。JavaS...【详细内容】
2024-04-12  Search: Java  点击:(2)  评论:(0)  加入收藏
17 个你需要知道的 JavaScript 优化技巧
你可能一直在使用JavaScript搞开发,但很多时候你可能对它提供的最新功能并不感冒,尽管这些功能在无需编写额外代码的情况下就可以解决你的问题。作为前端开发人员,我们必须了解...【详细内容】
2024-04-03  Search: Java  点击:(6)  评论:(0)  加入收藏
你不可不知的 15 个 JavaScript 小贴士
在掌握如何编写JavaScript代码之后,那么就进阶到实践&mdash;&mdash;如何真正地解决问题。我们需要更改JS代码使其更简单、更易于阅读,因为这样的程序更易于团队成员之间紧密协...【详细内容】
2024-03-21  Search: Java  点击:(27)  评论:(0)  加入收藏
Oracle正式发布Java 22
Oracle 正式发布 Java 22,这是备受欢迎的编程语言和开发平台推出的全新版本。Java 22 (Oracle JDK 22) 在性能、稳定性和安全性方面进行了数千种改进,包括对Java 语言、其API...【详细内容】
2024-03-21  Search: Java  点击:(10)  评论:(0)  加入收藏
构建一个通用灵活的JavaScript插件系统?看完你也会!
在软件开发中,插件系统为应用程序提供了巨大的灵活性和可扩展性。它们允许开发者在不修改核心代码的情况下扩展和定制应用程序的功能。本文将详细介绍如何构建一个灵活的Java...【详细内容】
2024-03-20  Search: Java  点击:(20)  评论:(0)  加入收藏
Java 8 内存管理原理解析及内存故障排查实践
本文介绍Java8虚拟机的内存区域划分、内存垃圾回收工作原理解析、虚拟机内存分配配置,以及各垃圾收集器优缺点及场景应用、实践内存故障场景排查诊断,方便读者面临内存故障时...【详细内容】
2024-03-20  Search: Java  点击:(15)  评论:(0)  加入收藏
如何编写高性能的Java代码
作者 | 波哥审校 | 重楼在当今软件开发领域,编写高性能的Java代码是至关重要的。Java作为一种流行的编程语言,拥有强大的生态系统和丰富的工具链,但是要写出性能优异的Java代码...【详细内容】
2024-03-20  Search: Java  点击:(24)  评论:(0)  加入收藏
在Java应用程序中释放峰值性能:配置文件引导优化(PGO)概述
译者 | 李睿审校 | 重楼在Java开发领域,优化应用程序的性能是开发人员的持续追求。配置文件引导优化(Profile-Guided Optimization,PGO)是一种功能强大的技术,能够显著地提高Ja...【详细内容】
2024-03-18  Search: Java  点击:(27)  评论:(0)  加入收藏
对JavaScript代码压缩有什么好处?
对JavaScript代码进行压缩主要带来以下好处: 减小文件大小:通过移除代码中的空白符、换行符、注释,以及缩短变量名等方式,可以显著减小JavaScript文件的大小。这有助于减少网页...【详细内容】
2024-03-13  Search: Java  点击:(2)  评论:(0)  加入收藏
跨端轻量JavaScript引擎的实现与探索
一、JavaScript 1.JavaScript语言JavaScript是ECMAScript的实现,由ECMA 39(欧洲计算机制造商协会39号技术委员会)负责制定ECMAScript标准。ECMAScript发展史: 2.JavaScript...【详细内容】
2024-03-12  Search: Java  点击:(2)  评论:(0)  加入收藏
▌简易百科推荐
Java 8 内存管理原理解析及内存故障排查实践
本文介绍Java8虚拟机的内存区域划分、内存垃圾回收工作原理解析、虚拟机内存分配配置,以及各垃圾收集器优缺点及场景应用、实践内存故障场景排查诊断,方便读者面临内存故障时...【详细内容】
2024-03-20  vivo互联网技术    Tags:Java 8   点击:(15)  评论:(0)  加入收藏
如何编写高性能的Java代码
作者 | 波哥审校 | 重楼在当今软件开发领域,编写高性能的Java代码是至关重要的。Java作为一种流行的编程语言,拥有强大的生态系统和丰富的工具链,但是要写出性能优异的Java代码...【详细内容】
2024-03-20    51CTO  Tags:Java代码   点击:(24)  评论:(0)  加入收藏
在Java应用程序中释放峰值性能:配置文件引导优化(PGO)概述
译者 | 李睿审校 | 重楼在Java开发领域,优化应用程序的性能是开发人员的持续追求。配置文件引导优化(Profile-Guided Optimization,PGO)是一种功能强大的技术,能够显著地提高Ja...【详细内容】
2024-03-18    51CTO  Tags:Java   点击:(27)  评论:(0)  加入收藏
Java生产环境下性能监控与调优详解
堆是 JVM 内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 和 Survivor 区,...【详细内容】
2024-02-04  大雷家吃饭    Tags:Java   点击:(57)  评论:(0)  加入收藏
在项目中如何避免和解决Java内存泄漏问题
在Java中,内存泄漏通常指的是程序中存在一些不再使用的对象或数据结构仍然保持对内存的引用,从而导致这些对象无法被垃圾回收器回收,最终导致内存占用不断增加,进而影响程序的性...【详细内容】
2024-02-01  编程技术汇  今日头条  Tags:Java   点击:(70)  评论:(0)  加入收藏
Java中的缓存技术及其使用场景
Java中的缓存技术是一种优化手段,用于提高应用程序的性能和响应速度。缓存技术通过将计算结果或者经常访问的数据存储在快速访问的存储介质中,以便下次需要时可以更快地获取。...【详细内容】
2024-01-30  编程技术汇    Tags:Java   点击:(73)  评论:(0)  加入收藏
JDK17 与 JDK11 特性差异浅谈
从 JDK11 到 JDK17 ,Java 的发展经历了一系列重要的里程碑。其中最重要的是 JDK17 的发布,这是一个长期支持(LTS)版本,它将获得长期的更新和支持,有助于保持程序的稳定性和可靠性...【详细内容】
2024-01-26  政采云技术  51CTO  Tags:JDK17   点击:(90)  评论:(0)  加入收藏
Java并发编程高阶技术
随着计算机硬件的发展,多核处理器的普及和内存容量的增加,利用多线程实现异步并发成为提升程序性能的重要途径。在Java中,多线程的使用能够更好地发挥硬件资源,提高程序的响应...【详细内容】
2024-01-19  大雷家吃饭    Tags:Java   点击:(107)  评论:(0)  加入收藏
这篇文章彻底让你了解Java与RPA
前段时间更新系统的时候,发现多了一个名为Power Automate的应用,打开了解后发现是一个自动化应用,根据其描述,可以自动执行所有日常任务,说的还是比较夸张,简单用了下,对于office、...【详细内容】
2024-01-17  Java技术指北  微信公众号  Tags:Java   点击:(99)  评论:(0)  加入收藏
Java 在 2023 年仍然流行的 25 个原因
译者 | 刘汪洋审校 | 重楼学习 Java 的过程中,我意识到在 90 年代末 OOP 正值鼎盛时期,Java 作为能够真正实现这些概念的语言显得尤为突出(尽管我此前学过 C++,但相比 Java 影响...【详细内容】
2024-01-10  刘汪洋  51CTO  Tags:Java   点击:(78)  评论:(0)  加入收藏
站内最新
站内热门
站内头条