1. 首页
  2. > 海外公司注册 >

专利电子签名证书私钥密码忘记(怎样把签名转换成电子签名)

先说一下两个重要的工具


对称加密


采用单钥密码系统的加密方法,同一个密钥可以同时用作信息的加密和解密,这种加密方法称为对称加密,也称为单密钥加密。——百度百科


对称加密算法的特点

  • 加密和解密使用同样的密钥
  • 计算速度快,适用于对大量数据加密处理
  • 安全性取决于算法,也取决于密钥的管理,一旦密钥泄漏,数据则暴露无遗

对称加密算法的使用场景

基于上述的特点,在一些需要高效实时传输的加密通讯场景中,比如使用VPN或者代理进行通讯时,可以使用对称加密。另外在同一个系统内部不同模块,比如前后端,从前端输入的敏感信息,可以使用对称加密算法进行加密后将密文传到后端,避免传输过程中明文被截获,因为同系统内部之间密钥管理相对容易,而对于共享密钥有泄漏风险的其他任何场景,则不适合使用对称加密算法进行加密。


常见的对称加密算法

算法


描述


DES(Data Encryption Standard)


数据加密标准,速度较快,适用于加密大量数据


3DES(Triple DES)


基于DES,对一块数据用三个不同的密钥进行三次加密,强度更高


AES(Advanced Encryption Standard)


高级加密标准,速度快,安全级别高,支持128、192、256、512位密钥的加密


Blowfish


速度快且安全,而且没有专利和商业限制。了解更多>>[1]


OpenSSL实现对称加密

OpenSSL> enc --help usage: enc -ciphername [-AadePp] [-base64] [-bufsize number] [-debug] [-in file] [-iv IV] [-K key] [-k password] [-kfile file] [-md digest] [-none] [-nopad] [-nosalt] [-out file] [-pass arg] [-S salt] [-salt] -A Process base64 data on one line (requires -a) -a Perform base64 encoding/decoding (alias -base64) -bufsize size Specify the buffer size to use for I/O -d Decrypt the input data -debug Print debugging information -e Encrypt the input data (default) -in file Input file to read from (default stdin) -iv IV IV to use, specified as a hexadecimal string -K key Key to use, specified as a hexadecimal string -md digest Digest to use to create a key from the passphrase -none Use NULL cipher (no encryption or decryption) -nopad Disable standard block padding -out file Output file to write to (default stdout) -P Print out the salt, key and IV used, then exit (no encryption or decryption is performed) -p Print out the salt, key and IV used -pass source Password source -S salt Salt to use, specified as a hexadecimal string -salt Use a salt in the key derivation routines (default) -v Verbose


只对文件进行base64编码,而不使用加解密

/*对文件进行base64编码*/ openssl enc -base64 -in plain.txt -out base64.txt /*对base64格式文件进行解密操作*/ openssl enc -base64 -d -in base64.txt -out plain2.txt /*使用diff命令查看可知解码前后明文一样*/ diff plain.txt plain2.txt

不同方式的密码输入方式

/*命令行输入,密码123456*/ openssl enc -aes-128-cbc -in plain.txt -out out.txt -pass pass:123456 /*文件输入,密码123456*/ echo 123456 > passwd.txt openssl enc -aes-128-cbc -in plain.txt -out out.txt -pass file:passwd.txt /*环境变量输入,密码123456*/ passwd=123456 export passwd openssl enc -aes-128-cbc -in plain.txt -out out.txt -pass env:passwd /*从文件描述输入*/ openssl enc -aes-128-cbc -in plain.txt -out out.txt -pass fd:1 /*从标准输入输入*/ openssl enc -aes-128-cbc -in plain.txt -out out.txt -pass stdin

Java实现对称加密

DES

/** * 生成 DES 算法密钥 * @return byte[] * @throws Exception */ public static byte[] generateDESKey() throws Exception { KeyGenerator keyGenerator = KeyGenerator.getInstance("DES"); // must be equal to 56 keyGenerator.init(56); SecretKey secretKey = keyGenerator.generateKey(); byte[] encodedKey = secretKey.getEncoded(); return encodedKey; } /** * DES加密 * @param encodedKey generateDESKey生成的密钥 * @param dataBytes byte[]形式的待加密数据 * @return byte[] * @throws Exception */ public static byte[] encryptByDES(byte[] encodedKey, byte[] dataBytes) throws Exception { SecretKey secretKey = new SecretKeySpec(encodedKey, "DES"); Cipher cipher = Cipher.getInstance("DES"); cipher.init(Cipher.ENCRYPT_MODE, secretKey); byte[] encryptedData = cipher.doFinal(dataBytes); return encryptedData; } /** * DES解密 * @param encodedKey generateDESKey生成的密钥 * @param encryptedData byte[]形式的待解密数据 * @return byte[] * @throws Exception */ public static byte[] decryptByDES(byte[] encodedKey, byte[] encryptedData) throws Exception { SecretKey secretKey = new SecretKeySpec(encodedKey, "DES"); Cipher cipher = Cipher.getInstance("DES"); cipher.init(Cipher.DECRYPT_MODE, secretKey); byte[] decryptedData = cipher.doFinal(encryptedData); return decryptedData; }

基础版本使用方法如下:

@Test public void testDES_1() throws Exception { byte[] encodedKey = SecurityUtil.generateDESKey(); String data = "this is a good boy"; byte[] encryptedData = SecurityUtil.encryptByDES(encodedKey, data.getBytes()); byte[] decryptedData = SecurityUtil.decryptByDES(encodedKey, encryptedData); Assert.assertEquals(data, new String(decryptedData)); }

可以看到,以上的方法使用起来并不友好,参数、返回等大量存在byte[],不便于理解,中间结果不便于查看和传输,比如如果需要将encryptedData返回给下游系统,那么还得使用Base64进行处理,基于此,我对在上述接口基础上进一步进行封装,使其使用起来更贴近日常使用场景。


优化版本:

/** * 生成 DES 算法密钥 * @return 经过Base64编码的字符串密钥 * @throws Exception */ public static String generateDESKeyStr() throws Exception { return Base64.encodeBase64String(generateDESKey()); } /** * DES加密 * @param key 经过Base64编码的字符串密钥 * @param data String形式的待加密数据 * @return 经过Base64编码的加密数据 * @throws Exception */ public static String encryptByDES(String key, String data) throws Exception { byte[] encodedKey = Base64.decodeBase64(key); byte[] dataBytes = data.getBytes(); byte[] encryptedData = encryptByDES(encodedKey, dataBytes); return Base64.encodeBase64String(encryptedData); } /** * DES解密 * @param key 经过Base64编码的字符串密钥 * @param data String形式的待解密数据 * @return 原始数据 * @throws Exception */ public static String decryptByDES(String key, String data) throws Exception { byte[] encodedKey = Base64.decodeBase64(key); byte[] dataBytes = Base64.decodeBase64(data); byte[] decryptedData = decryptByDES(encodedKey, dataBytes); return new String(decryptedData); }

优化版本使用方法如下:

@Test public void testDES_2() throws Exception { String key = SecurityUtil.generateDESKeyStr(); String data = "this is a good boy"; String encryptedData = SecurityUtil.encryptByDES(key, data); String decryptedData = SecurityUtil.decryptByDES(key, encryptedData); Assert.assertEquals(data, decryptedData); }

这里补充一下,在实际项目开发过程中,还真遇见不少同学对Base64理解有误的情况,对于以上处理和转换过程理解有难度的同学,[2]


3DES

/** * 生成 3DES 算法密钥 * @return byte[] * @throws Exception */ public static byte[] generate3DESKey() throws Exception { KeyGenerator keyGenerator = KeyGenerator.getInstance("DESede"); // must be equal to 112 or 168 keyGenerator.init(168); SecretKey secretKey = keyGenerator.generateKey(); byte[] encodedKey = secretKey.getEncoded(); return encodedKey; } /** * 3DES加密 * @param encodedKey generate3DESKey生成的密钥 * @param dataBytes byte[]形式的待加密数据 * @return byte[] * @throws Exception */ public static byte[] encryptBy3DES(byte[] encodedKey, byte[] dataBytes) throws Exception { SecretKey secretKey = new SecretKeySpec(encodedKey, "DESede"); Cipher cipher = Cipher.getInstance("DESede"); cipher.init(Cipher.ENCRYPT_MODE, secretKey); byte[] encryptedData = cipher.doFinal(dataBytes); return encryptedData; } /** * 3DES解密 * @param encodedKey generate3DESKey生成的密钥 * @param encryptedData byte[]形式的待解密数据 * @return byte[] * @throws Exception */ public static byte[] decryptBy3DES(byte[] encodedKey, byte[] encryptedData) throws Exception { SecretKey secretKey = new SecretKeySpec(encodedKey, "DESede"); Cipher cipher = Cipher.getInstance("DESede"); cipher.init(Cipher.DECRYPT_MODE, secretKey); byte[] decryptedData = cipher.doFinal(encryptedData); return decryptedData; }

使用方法如下:

@Test public void test3DES() throws Exception { byte[] encodedKey = SecurityUtil.generate3DESKey(); String data = "this is a good boy"; byte[] encryptedData = SecurityUtil.encryptBy3DES(encodedKey, data.getBytes()); byte[] decryptedData = SecurityUtil.decryptBy3DES(encodedKey, encryptedData); Assert.assertEquals(data, new String(decryptedData)); }

AES

/** * 生成 AES 算法密钥 * @return byte[] * @throws Exception */ public static byte[] generateAESKey() throws Exception { KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); // must be equal to 128, 192 or 256 // 但是当你使用 192/256 时,会收到: // java.security.InvalidKeyException: Illegal key size or default parameters keyGenerator.init(128); SecretKey secretKey = keyGenerator.generateKey(); byte[] encodedKey = secretKey.getEncoded(); return encodedKey; } /** * AES加密 * @param encodedKey generateAESKey生成的密钥 * @param dataBytes byte[]形式的待加密数据 * @return byte[] * @throws Exception */ public static byte[] encryptByAES(byte[] encodedKey, byte[] dataBytes) throws Exception { SecretKey secretKey = new SecretKeySpec(encodedKey, "AES"); Cipher cipher = Cipher.getInstance("AES"); cipher.init(Cipher.ENCRYPT_MODE, secretKey); byte[] encryptedData = cipher.doFinal(dataBytes); return encryptedData; } /** * AES密 * @param encodedKey generateAESSKey生成的密钥 * @param encryptedData byte[]形式的待解密数据 * @return byte[] * @throws Exception */ public static byte[] decryptByAES(byte[] encodedKey, byte[] encryptedData) throws Exception { SecretKey secretKey = new SecretKeySpec(encodedKey, "AES"); Cipher cipher = Cipher.getInstance("AES"); cipher.init(Cipher.DECRYPT_MODE, secretKey); byte[] decryptedData = cipher.doFinal(encryptedData); return decryptedData; }

使用方法如下:

@Test public void testAES() throws Exception { byte[] encodedKey = SecurityUtil.generateAESKey(); String data = "this is a good boy"; byte[] encryptedData = SecurityUtil.encryptByAES(encodedKey, data.getBytes()); byte[] decryptedData = SecurityUtil.decryptByAES(encodedKey, encryptedData); Assert.assertEquals(data, new String(decryptedData)); }

虽然AES支持128、192或 256的密钥长度,但是当我们使用192或256位长度的密钥时,会收到这个异常:java.security.InvalidKeyException: Illegal key size or default parameters


java.security.InvalidKeyException: Illegal key size or default parameters at javax.crypto.Cipher.checkCryptoPerm(Cipher.java:1026) at javax.crypto.Cipher.implInit(Cipher.java:801) at javax.crypto.Cipher.chooseProvider(Cipher.java:864) at javax.crypto.Cipher.init(Cipher.java:1249) at javax.crypto.Cipher.init(Cipher.java:1186) at com.example.architecture.util.SecurityUtil.encryptByAES(SecurityUtil.java:161) at com.example.architecture.util.SecurityUtilTest.testAES(SecurityUtilTest.java:97) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)

原因是JRE中自带的local_policy.jarUS_export_policy.jar是支持128位密钥的加密算法,而当我们要使用192或256位密钥算法的时候,已经超出它支持的范围。


解决方案:去官方下载JCE无限制权限策略文件。


下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt


  • 如果安装了JRE,将两个jar文件放到%JRE_HOME%libsecurity目录下覆盖原来的文件。
  • 如果安装了JDK,还要将两个jar文件也放到%JDK_HOME%jrelibsecurity目录下覆盖原来文件。

AES128和AES256主要区别是密钥长度不同(分别是128bits,256bits)、加密处理轮数不同(分别是10轮,14轮),后者强度高于前者,当前AES是公认的较为安全的对称加密算法。



非对称加密


非对称加密算法需要两个密钥:公开密钥(publickey:简称公钥)和私有密钥(privatekey:简称私钥)。公钥与私钥是一对,如果用公钥对数据进行加密,只有用对应的私钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。 ——百度百科


非对称加密算法的特点

  • 也称公开密钥加密,算法需要两个密钥,其中一个可以公开,并且通过公开的密钥无法推导出对应的私钥
  • 算法复杂度相对对称加密算法高,所以计算相对较慢
  • 密钥的保密性较好,因为公钥可以公开,免去了交换密钥的需求

非对称加密算法的使用场景

由于安全性较好,并且密钥可以公开,无交换过程泄密的风险,因此非对此密钥算法被广泛使用,比如SSH、HTTPS、电子证书、数字签名、加密通讯等领域。


数据加密传输


报文签名验签



常见的非对称加密算法

1977年,三位数学家Rivest、Shamir 和 Adleman 设计了一种算法,可以实现非对称加密。这种算法用他们三个人的名字命名,叫做RSA算法,RSA算法从被发明至今一直是最广为使用的"非对称加密算法"。其他场景的算法还有Elgamal、背包算法、Rabin、D-H、ECC(椭圆曲线加密算法)。


OpenSSL实现RSA

在开始介绍具体使用之前,补充两个知识点:


  1. 如果你对非对称密钥的编码规则和数据格式定义不清楚,可以先看一下这篇文章[3],这里摘抄重点:


openssl有多种形式的密钥,openssl提供PEM和DER两种编码方式对这些密钥进行编码,并提供相关指令可以使用户在这两种格式之间进行转换。


DER


DER就是密钥的二进制表述格式


PEM


PEM格式就是对DER编码转码为base64字符格式。通过base64解码可以还原DER格式。


PEM 是明文格式,可以包含证书或者是密钥;其内容通常是以类似 “—–BEGIN …—–” 开头 “—–END …—–” 为结尾的这样的格式进行描述的。 因为DER是纯二进制格式,对人不友好,所以一般都用PEM进行存储。


  1. RSA非对称加解密算法填充方式(Padding)[4],同样摘抄重点:


RSA加密常用的填充模式有三种:RSA_PKCS1_PADDING, RSA_PKCS1_OAEP_PADDING, RSA_NO_PADDING。


与对称加密算法DES,AES一样,RSA算法也是一个块加密算法( block cipher algorithm),总是在一个固定长度的块上进行操作。但跟AES等不同的是,block length是跟key length有关的。


每次RSA加密的明文的长度是受RSA填充模式限制的,但是RSA每次加密的块长度就是key length。


常用指令


genrsa命令


OpenSSL> genrsa --help
usage: genrsa [args] [numbits]
-des encrypt the generated key with DES in cbc mode
-des3 encrypt the generated key with DES in ede cbc mode (168 bit key)
-aes128, -aes192, -aes256
encrypt PEM output with cbc aes
-camellia128, -camellia192, -camellia256
encrypt PEM output with cbc camellia
-out file output the key to file
-passout arg output file pass phrase source
-f4 use F4 (0x10001) for the E value
-3 use 3 for the E value



示例:


//生成2048位的RSA私钥
openssl genrsa -out pkcs1_private.pem 2048
rsa命令 usage: rsa [-ciphername] [-check] [-in file] [-inform fmt]
[-modulus] [-noout] [-out file] [-outform fmt] [-passin src]
[-passout src] [-pubin] [-pubout] [-sgckey] [-text]

-check Check consistency of RSA private key
-in file Input file (default stdin)
-inform format Input format (DER, NET or PEM (default))
-modulus Print the RSA key modulus
-noout Do not print encoded version of the key
-out file Output file (default stdout)
-outform format Output format (DER, NET or PEM (default PEM))
-passin src Input file passphrase source
-passout src Output file passphrase source
-pubin Expect a public key (default private key)
-pubout Output a public key (default private key)
-sgckey Use modified NET algorithm for IIS and SGC keys
-text Print in plain text in addition to encoded



示例:

// 通过私钥pem文件生成对应的公钥 openssl rsa -in pkcs1_private.pem -pubout -out pkcs1_public.pem

在Java环境中,需要注意两个问题,需要借助上述命令来解决,我们以两个小例子说明。


首先在 /tmp 目录下简单生成一个私钥:


// 生成pem格式的密钥 openssl genrsa -out RSA.pem

尝试读取RSA.pem并生成PrivateKey对象


@Test public void testGeneratePrivateKeyWithPEM() throws Exception { byte[] bytes = Files.readAllBytes(Paths.get("/tmp/RSA.pem" )); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PrivateKey privateKey = keyFactory.generatePrivate(keySpec); System.out.println(privateKey.getFormat()); }

运行之后发现会报这个异常:java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: invalid key format


java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: invalid key format at sun.security.rsa.RSAKeyFactory.engineGeneratePrivate(RSAKeyFactory.java:217) at java.security.KeyFactory.generatePrivate(KeyFactory.java:372) at com.example.architecture.util.RSAUtilTest.testGeneratePrivateKeyWithPEM(RSAUtilTest.java:32)

问题原因:Java自带的security包不支持直接读取PEM格式文件。


解决方法:需要将PEM格式转为DER格式再进行读取。


// 把pem格式转化成der格式,使用outform指定der格式 openssl rsa -in RSA.pem -outform der -out RSA.der

好了,继续读取RSA.der并尝试生成PrivateKey对象


@Test public void testGeneratePrivateKeyWithDER() throws Exception { byte[] bytes = Files.readAllBytes(Paths.get("/tmp/RSA.der" )); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PrivateKey privateKey = keyFactory.generatePrivate(keySpec); System.out.println(privateKey.getFormat()); }

运行之后发现会报另一个异常:java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: IOException : algid parse error, not a sequence


java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: IOException : algid parse error, not a sequence at sun.security.rsa.RSAKeyFactory.engineGeneratePrivate(RSAKeyFactory.java:217) at java.security.KeyFactory.generatePrivate(KeyFactory.java:372) at com.example.architecture.util.RSAUtilTest.testGeneratePrivateKeyWithDER(RSAUtilTest.java:24) ...

问题原因:OpenSSL生成的私钥是PKCS#1格式的,而Java自带的security包使用PKCS8EncodedKeySpec来实现私钥,私钥信息是以PKCS#8标准定义的,我们从下面这个类的构造方法说明可以很明显看到。


/** * Creates a new PKCS8EncodedKeySpec with the given encoded key. * * @param encodedKey the key, which is assumed to be * encoded according to the PKCS #8 standard. The contents of * the array are copied to protect against subsequent modification. * @exception NullPointerException if {@code encodedKey} * is null. */ public PKCS8EncodedKeySpec(byte[] encodedKey) { super(encodedKey); }

解决方法:OpenSSL生成密钥之后在Java环境中使用要先转为PKCS#8格式。


关于PKCS#1与PKCS#8,简单理解两者都是非对称加密私钥信息的标准定义,区别是PKCS#1是针对RSA算法的,而PKCS#8是通用的,两者在格式定义上有些许区别。


//提取PCKS8格式的私钥 openssl pkcs8 -topk8 -inform DER -in RSA.der -outform DER -nocrypt -out RSA_pkcs8.der

继续读取RSA_pkcs8.der并尝试生成PrivateKey对象


@Test public void testGeneratePrivateKeyWithPKCS8DER() throws Exception { byte[] bytes = Files.readAllBytes(Paths.get("/tmp/RSA_pkcs8.der" )); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PrivateKey privateKey = keyFactory.generatePrivate(keySpec); System.out.println(privateKey.getFormat()); } 输出: PKCS#8

搞定!


rsautl命令

OpenSSL> rsautl --help Usage: rsautl [options] -in file input file -out file output file -inkey file input key -keyform arg private key format - default PEM -pubin input is an RSA public -certin input is a certificate carrying an RSA public key -ssl use SSL v2 padding -raw use no padding -pkcs use PKCS#1 v1.5 padding (default) -oaep use PKCS#1 OAEP -sign sign with private key -verify verify with public key -encrypt encrypt with public key -decrypt decrypt with private key -hexdump hex dump output


##### 加密解密示例: shell // 生成RSA密钥 openssl genrsa -out RSA.pem // 提取公钥 openssl rsa -in RSA.pem -pubout -out pub.pem // 使用RSA公钥进行加密,输入的是私钥,实际上使用其对应的公钥进行加密,以下两个等价 openssl rsautl -encrypt -in plain.txt -inkey RSA.pem -out encrypted.txt openssl rsautl -encrypt -in plain.txt -inkey pub.pem -pubin -out encrypted.txt // 使用RSA私钥进行解密 openssl rsautl -decrypt -in encrypted.txt -inkey RSA.pem -out plain2.txt

##### 加签验签示例: shell // 生成RSA密钥 openssl genrsa -out RSA.pem // 提取公钥 openssl rsa -in RSA.pem -pubout -out pub.pem // 使用私钥进行签名 openssl rsautl -sign -in plain.txt -inkey RSA.pem -out sign.txt // 使用公钥进行验证 openssl rsautl -verify -in sign.txt -pubin -inkey pub.pem -out plain2.txt

加密、解密、签名、验签,完整代码示例:

import org.apache.commons.codec.binary.Base64; import javax.crypto.Cipher; import java.nio.file.Files; import java.nio.file.Paths; import java.security.*; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; public class RSAUtil { /** * RSA 算法 **/ private static final String ALGORITHM_RSA = "RSA"; /** * 签名算法 **/ private static final String ALGORITHM_SIGNATURE = "SHA1WithRSA"; /** * Cipher类提供了加密和解密的功能 * <p> * Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1PADDING"); * RSA是算法,ECB是分块模式,PKCS1Padding是填充模式,整个构成一个完整的加密算法 * * <pre> * 有以下的参数: * * AES/CBC/NoPadding (128) * * AES/CBC/PKCS5Padding (128) * * AES/ECB/NoPadding (128) * * AES/ECB/PKCS5Padding (128) * * DES/CBC/NoPadding (56) * * DES/CBC/PKCS5Padding (56) * * DES/ECB/NoPadding (56) * * DES/ECB/PKCS5Padding (56) * * DESede/CBC/NoPadding (168) * * DESede/CBC/PKCS5Padding (168) * * DESede/ECB/NoPadding (168) * * DESede/ECB/PKCS5Padding (168) * * RSA/ECB/PKCS1Padding (1024, 2048) * * RSA/ECB/OAEPWithSHA-1AndMGF1Padding (1024, 2048) * * RSA/ECB/OAEPWithSHA-256AndMGF1Padding (1024, 2048) * </pre> */ private static final String PADDING = "RSA/ECB/PKCS1PADDING"; /** * 生成非对称密钥对,默认使用RSA * * @return * @throws Exception */ public static String[] generateRSAKeyPair() throws Exception { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM_RSA); keyPairGenerator.initialize(2048); KeyPair keyPair = keyPairGenerator.generateKeyPair(); PublicKey publicKey = keyPair.getPublic(); PrivateKey privateKey = keyPair.getPrivate(); String publicKeyStr = Base64.encodeBase64String(publicKey.getEncoded()); String privateKeyStr = Base64.encodeBase64String(privateKey.getEncoded()); return new String[]{publicKeyStr, privateKeyStr}; } /** * 使用数据接收方的公钥加密 * * @param publicKeyStr BASE64编码格式的公钥 * @param data 待加密数据 * @return * @throws Exception */ public static String encryptByPublicKeyStr(String publicKeyStr, String data) throws Exception { PublicKey publicKey = getPublicKeyFromString(publicKeyStr); byte[] bytes = doCipher(Cipher.ENCRYPT_MODE, publicKey, data.getBytes()); return Base64.encodeBase64String(bytes); } /** * 数据接收方使用自己的私钥解密 * * @param privateKeyStr BASE64编码格式的私钥 * @param encryptedData 待解密数据 * @return * @throws Exception */ public static String decryptByPrivateKeyStr(String privateKeyStr, String encryptedData) throws Exception { PrivateKey privateKey = getPrivateKeyFromString(privateKeyStr); byte[] bytes = doCipher(Cipher.DECRYPT_MODE, privateKey, Base64.decodeBase64(encryptedData)); return new String(bytes); } /** * 使用数据接收方的公钥加密 * * @param publicKeyPath 公钥文件路径 * @param data 待加密数据 * @return * @throws Exception */ public static String encryptByPublicKeyFile(String publicKeyPath, String data) throws Exception { PublicKey publicKey = getPublicKeyFromFile(publicKeyPath); byte[] bytes = doCipher(Cipher.ENCRYPT_MODE, publicKey, data.getBytes()); return Base64.encodeBase64String(bytes); } /** * 数据接收方使用自己的私钥解密 * * @param privateKeyPath 私钥文件路径 * @param encryptedData 待解密数据 * @return * @throws Exception */ public static String decryptByPrivateKeyFile(String privateKeyPath, String encryptedData) throws Exception { PrivateKey privateKey = getPrivateKeyFromFile(privateKeyPath); byte[] bytes = doCipher(Cipher.DECRYPT_MODE, privateKey, Base64.decodeBase64(encryptedData)); return new String(bytes); } /** * 数据发送方使用自己的私钥对数据签名 * * @param privateKeyStr BASE64编码格式的私钥 * @param data 待签名数据 * @return 签名数据,一般进行BASE64编码后发送给对方 * @throws Exception */ public static String signByPrivateKeyStr(String privateKeyStr, String data) throws Exception { PrivateKey privateKey = getPrivateKeyFromString(privateKeyStr); byte[] bytes = sign(privateKey, data.getBytes()); return Base64.encodeBase64String(bytes); } /** * 数据接收方使用对方公钥验证签名 * * @param publicKeyStr BASE64编码格式的私钥 * @param data 待验签数据 * @param sign 签名数据,进行BASE64解码后验证签名 * @return * @throws Exception */ public static boolean verifyByPublicKeyStr(String publicKeyStr, String data, String sign) throws Exception { PublicKey publicKey = getPublicKeyFromString(publicKeyStr); return verify(publicKey, data.getBytes(), Base64.decodeBase64(sign)); } private static PublicKey getPublicKeyFromString(String publicKeyStr) throws Exception { byte[] encodedKey = Base64.decodeBase64(publicKeyStr); return generatePublic(encodedKey); } private static PublicKey getPublicKeyFromFile(String publicKeyPath) throws Exception { byte[] encodedKey = Files.readAllBytes(Paths.get(publicKeyPath)); return generatePublic(encodedKey); } private static PublicKey generatePublic(byte[] encodedKey) throws Exception { X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encodedKey); KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM_RSA); PublicKey publicKey = keyFactory.generatePublic(keySpec); return publicKey; } private static PrivateKey getPrivateKeyFromString(String privateKeyStr) throws Exception { byte[] encodedKey = Base64.decodeBase64(privateKeyStr); return generatePrivate(encodedKey); } private static PrivateKey getPrivateKeyFromFile(String privateKeyPath) throws Exception { byte[] encodedKey = Files.readAllBytes(Paths.get(privateKeyPath)); return generatePrivate(encodedKey); } private static PrivateKey generatePrivate(byte[] encodedKey) throws Exception { PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encodedKey); KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM_RSA); PrivateKey privateKey = keyFactory.generatePrivate(keySpec); return privateKey; } private static byte[] doCipher(int mode, Key key, byte[] bytes) throws Exception { Cipher cipher = Cipher.getInstance(PADDING); cipher.init(mode, key); return cipher.doFinal(bytes); } private static byte[] sign(PrivateKey privateKey, byte[] data) throws Exception { Signature signature = Signature.getInstance(ALGORITHM_SIGNATURE); signature.initSign(privateKey); signature.update(data); return signature.sign(); } private static boolean verify(PublicKey publicKey, byte[] data, byte[] sign) throws Exception { Signature signature = Signature.getInstance(ALGORITHM_SIGNATURE); signature.initVerify(publicKey); signature.update(data); return signature.verify(sign); } }

测试代码示例:

@Test public void test_generateRSAKeyPair() throws Exception { String data = "password:123456"; String[] keyPair = RSAUtil.generateRSAKeyPair(); String publicKeyStr = keyPair[0]; String privateKeyStr = keyPair[1]; String encryptedData = RSAUtil.encryptByPublicKeyStr(publicKeyStr, data); String decryptedData = RSAUtil.decryptByPrivateKeyStr(privateKeyStr, encryptedData); Assert.assertEquals(data, decryptedData); } /** * 在 /tmp 目录下执行以下命令 * * openssl genrsa -out private.pem * * openssl rsa -in private.pem -outform der -out private.der * openssl pkcs8 -topk8 -inform DER -in private.der -outform DER -nocrypt -out private_pkcs8.der * * openssl rsa -in private.pem -pubout -out public.pem * openssl rsa -pubin -in public.pem -outform der -out public.der */ @Test public void test_openssl() throws Exception { String data = "password:123456"; String encryptedData = RSAUtil.encryptByPublicKeyFile("/tmp/public.der", data); String decryptedData = RSAUtil.decryptByPrivateKeyFile("/tmp/private_pkcs8.der", encryptedData); Assert.assertEquals(data, decryptedData); } @Test public void test_signature() throws Exception { String data = "balance=1000000"; String[] keyPair = RSAUtil.generateRSAKeyPair(); String publicKeyStr = keyPair[0]; String privateKeyStr = keyPair[1]; String sign = RSAUtil.signByPrivateKeyStr(privateKeyStr, data); boolean pass = RSAUtil.verifyByPublicKeyStr(publicKeyStr, data, sign); Assert.assertTrue(pass); }

证书和keytool

上面我们使用了openssl工具生成RSA非对称密钥对,以字符串或标准文件格式存储密钥并处理加解密,在实际Java项目开发中,经常会使用keystore来管理密钥,对此我们可以使用 keytool 工具来进行密钥生成,或将其他文件格式的密钥导入keystore


正式介绍keytool之前我们先来解释几个概念,对下文keytool的使用理解有帮助。


证书

我们先来看一个小场景,A要告诉B一个秘密:



上面流程在正常情况下没问题,可以保证数据加密传输,即使被第三方截获,没有B的私钥也解密不了。然而在复杂的网络环境下,情况有可能是这样的:



上述过程就是典型的中间人攻击场景,A和B的通讯链路中多了一个中间人X,而AB却完全无感知。在上述流程第4步之后,A持有的是X的公钥,X持有的是B的公钥,而实际上A需要的是B的公钥,但A却不知道,并且它以为自己持有的就是B的公钥,所以接下来A还是正常的使用这个公钥加密并发送数据,这样A和B之间的通讯信息就完全被X获取并且可以随意篡改。


在实际的网络环境中,我们无法阻止X的存在,那如何解决上述中间人攻击问题?通过分析上述流程,我们不难发现,A要获取B的公钥,但实际却收到了X的公钥,然鹅,它却无法辨识出来这个公钥不是B的,而证书,可以帮助我们解决这个关键问题。


CER-数字证书

数字证书是数字凭据,它提供有关实体标识的信息以及其他支持信息。数字证书是由权威证书颁发机构(Certificate Authority,简称CA)颁发的,由该权威机构担保证书信息的有效性。数字证书包含证书中所标识的实体的公钥,由于证书将公钥与申请者匹配,并且该证书的真实性由颁发机构保证,因此,数字证书为如何找到用户的公钥并知道它是否有效这一问题提供了解决方案。


常见的证书有三种:


  1. 带有私钥的证书:由PublicKey Cryptography Standards #12,即PKCS#12标准定义,包含了公钥和私钥的二进制格式的证书形式,以 pfx 作为证书文件后缀名,PKCS#12可以增加加密保护,有助于传输证书及对应的私钥。
  2. 二进制编码的证书:由X.509公钥证书格式标准定义,证书中没有私钥,包含DER编码二进制格式的公钥,以 cer 作为证书文件后缀名。
  3. Base64编码的证书:由X.509公钥证书格式标准定义,证书中没有私钥,包含BASE64编码格式的公钥,也是以 cer 作为证书文件后缀名。

keystore 密钥库

密钥库是存储一个或多个密钥条目的文件,每个密钥条目以一个别名标识,它包含密钥和证书相关信息。


在Java中,keystore中每种类型的条目都实现 KeyStore.Entry 接口,主要有三种基本的实现:


  1. KeyStore.PrivateKeyEntry

此类型的条目保存一个加密的PrivateKey,可以选择用受保护格式存储该私钥,以防止未授权访问。它还随附一个相应公钥的证书链。 2. KeyStore.SecretKeyEntry 此类型的条目保存一个加密的SecretKey,可以选择用受保护格式存储该密钥,以防止未授权访问。 3. KeyStore.TrustedCertificateEntry 此类型的条目包含一个属于另一方的单个公钥证书(Certificate)。它被称为可信证书,因为keystore的所有者相信证书中的公钥确实属于该证书所标识的身份。


keystore 密钥库文件格式



truststore

另外,平时我们有时也会看到truststore,从其文件格式来看它和keystore其实是一个东西,只是为了方便管理将其分开,keystore一般保存的是私钥,用来加解密或者做签名,而truststore中保存的是一些可信任的证书,主要是在Java在代码中以HTTPS方式调用时对被访问者进行认证的,以确保它是可信任的。


keytool

接下来我们使用keytool工具来生成密钥和证书。


keytool命令

keytool 密钥和证书管理工具 命令: -certreq 生成证书请求 -changealias 更改条目的别名 -delete 删除条目 -exportcert 导出证书 -genkeypair 生成密钥对 -genseckey 生成密钥 -gencert 根据证书请求生成证书 -importcert 导入证书或证书链 -importpass 导入口令 -importkeystore 从其他密钥库导入一个或所有条目 -keypasswd 更改条目的密钥口令 -list 列出密钥库中的条目 -printcert 打印证书内容 -printcertreq 打印证书请求的内容 -printcrl 打印 CRL 文件的内容 -storepasswd 更改密钥库的存储口令

keytool -genkeypair 命令

keytool -genkeypair -help keytool -genkeypair [OPTION]... 生成密钥对 选项: -alias <alias> 要处理的条目的别名 -keyalg <keyalg> 密钥算法名称 -keysize <keysize> 密钥位大小 -sigalg <sigalg> 签名算法名称 -destalias <destalias> 目标别名 -dname <dname> 唯一判别名 -startdate <startdate> 证书有效期开始日期/时间 -ext <value> X.509 扩展 -validity <valDays> 有效天数 -keypass <arg> 密钥口令 -keystore <keystore> 密钥库名称 -storepass <arg> 密钥库口令 -storetype <storetype> 密钥库类型 -providername <providername> 提供方名称 -providerclass <providerclass> 提供方类名 -providerarg <arg> 提供方参数 -providerpath <pathlist> 提供方类路径 -v 详细输出 -protected 通过受保护的机制的口令

下面我们使用该命令生成一对非对称密钥并将公钥包装到X.509 V3自签名证书中,密钥条目别名和密钥库文件名均为java-and-more,密钥库类型为pkcs12,并按提示输入对应内容:


keytool -genkeypair -alias java-and-more -keyalg RSA -keystore /tmp/java-and-more.keystore -storetype pkcs12 输入密钥库口令: 再次输入新口令: 您的名字与姓氏是什么? [Unknown]: javaandmore 您的组织单位名称是什么? [Unknown]: javaandmore 您的组织名称是什么? [Unknown]: javaandmore 您所在的城市或区域名称是什么? [Unknown]: sz 您所在的省/市/自治区名称是什么? [Unknown]: sz 该单位的双字母国家/地区代码是什么? [Unknown]: China CN=javaandmore, OU=javaandmore, O=javaandmore, L=sz, ST=sz, C=China是否正确? [否]: Y

注意PKCS12不支持设置密钥库条目密码,默认它与密钥库密码一致,如果创建默认类型(JKS)的密钥库,可以通过-keypass参数指定密钥条目密码。


keytool -list 命令 查看密钥库内容:


keytool -list -v -keystore /tmp/java-and-more.keystore 输入密钥库口令: 密钥库类型: JKS 密钥库提供方: SUN 您的密钥库包含 1 个条目 别名: java-and-more 创建日期: 2020-10-8 条目类型: PrivateKeyEntry 证书链长度: 1 证书[1]: 所有者: CN=javaandmore, OU=javaandmore, O=javaandmore, L=sz, ST=sz, C=China 发布者: CN=javaandmore, OU=javaandmore, O=javaandmore, L=sz, ST=sz, C=China 序列号: 80e7f12 有效期开始日期: Thu Oct 08 22:11:59 CST 2020, 截止日期: Wed Jan 06 22:11:59 CST 2021 证书指纹: MD5: 21:C6:CE:BE:D8:38:42:F6:FF:EF:78:D4:E3:AF:B3:57 SHA1: E2:DF:6A:8B:86:CC:65:D4:19:C7:22:B5:06:FC:58:F2:75:FE:8A:D2 SHA256: A1:28:22:B7:13:4F:77:50:0E:BE:F4:D4:40:19:2D:B3:94:88:47:1D:13:3A:13:75:DE:07:33:3B:39:A1:B0:40 签名算法名称: SHA256withRSA 版本: 3 扩展: #1: ObjectId: 2.5.29.14 Criticality=false SubjectKeyIdentifier [ KeyIdentifier [ 0000: 70 76 4E 22 F6 6B 57 68 B5 F6 92 93 26 D4 A8 80 pvN".kWh....&... 0010: 52 88 1E 74 R..t ] ] ******************************************* *******************************************

keytool -exportcert 命令 导出密钥库条目证书


keytool -exportcert -keystore /tmp/java-and-more.keystore -alias java-and-more -file /tmp/java-and-more.crt 输入密钥库口令: 存储在文件 </tmp/java-and-more.crt> 中的证书

keytool -printcert 命令 打印证书内容


keytool -printcert -v -file /tmp/java-and-more.crt 所有者: CN=javaandmore, OU=javaandmore, O=javaandmore, L=sz, ST=sz, C=China 发布者: CN=javaandmore, OU=javaandmore, O=javaandmore, L=sz, ST=sz, C=China 序列号: 80e7f12 有效期开始日期: Thu Oct 08 22:11:59 CST 2020, 截止日期: Wed Jan 06 22:11:59 CST 2021 证书指纹: MD5: 21:C6:CE:BE:D8:38:42:F6:FF:EF:78:D4:E3:AF:B3:57 SHA1: E2:DF:6A:8B:86:CC:65:D4:19:C7:22:B5:06:FC:58:F2:75:FE:8A:D2 SHA256: A1:28:22:B7:13:4F:77:50:0E:BE:F4:D4:40:19:2D:B3:94:88:47:1D:13:3A:13:75:DE:07:33:3B:39:A1:B0:40 签名算法名称: SHA256withRSA 版本: 3 扩展: #1: ObjectId: 2.5.29.14 Criticality=false SubjectKeyIdentifier [ KeyIdentifier [ 0000: 70 76 4E 22 F6 6B 57 68 B5 F6 92 93 26 D4 A8 80 pvN".kWh....&... 0010: 52 88 1E 74 R..t ] ]

从keystore文件导出的证书、密钥都是DER格式,可以使用openssl工具转换成PEM格式


openssl x509 -inform der -in /tmp/java-and-more.crt -out /tmp/java-and-more.pem

再从证书中导出公钥信息


openssl x509 -in /tmp/java-and-more.pem -pubkey -out public-key.pem -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtkPH2M8uL4T0Lu4g2qYt zclIkMhNHkfkL5rOqaZKSwYYwgG/NH9V4 Byf2Of5/1GfXQU8dFKwfXSm7rlL4/Y jKj5uy2xKBq2H2d2m7XoNqWzPOc8swkFraGEhxf4swbU6O/sRIxJJuelruPUh51o SN3LeLRQBDxkX2irEGICQZGcFshckBvH13PJtHKf0ZnXw3FzFde7IswiVrrwiEXP ttLQm0hM/xh3/Pcav7u0ademaVppSc8zC6sTxIxwyeyl2J8FXytdUakxixDqm/du nNBTfO7 dSNGLJ ATztluhfZcIeE2 t66Ya/6PKK6DSBxZwXnErKcuLvLVPnMX d 5wIDAQAB -----END PUBLIC KEY-----

注意:无法使用命令直接从keystore导出私钥信息,需要使用代码实现。以下示例使用Java从 PKCS12证书中提取私钥和公钥


@Test public void test_keystore() throws Exception { String keyStoreFile = "/tmp/java-and-more.keystore"; char[] password = "123456".toCharArray(); KeyStore keyStore = KeyStore.getInstance("pkcs12"); keyStore.load(Files.newInputStream(Paths.get(keyStoreFile)), password); Enumeration<String> aliases = keyStore.aliases(); while (aliases.hasMoreElements()) { String alias = aliases.nextElement(); System.out.println(alias); X509CertImpl certImpl = (X509CertImpl) keyStore.getCertificate(alias); PublicKey publicKey = certImpl.getPublicKey(); System.out.println(Base64.encodeBase64String(publicKey.getEncoded())); RSAPrivateCrtKeyImpl keyImpl = (RSAPrivateCrtKeyImpl) keyStore.getKey(alias, password); System.out.println(Base64.encodeBase64String(keyImpl.getEncoded())); } }

证书的签发与导入

这个过程涉及到3个命令:


  • keytool -certreq
  • keytool -gencert
  • keytool -importcert

分别对应以下3个步骤:


  1. 机构A使用certreq命令生成一个证书签名请求文件CSR (certificate sign request)并将其发送给机构B
  2. 机构B接收到这个请求后,使用gencert命令签发证书,会生成一个证书或者证书链
  3. 机构A接收到响应,使用importcert命令将签发证书导入到keystore中

下面以Alice.keystore签名的证书导入到密钥库Bob.keystore为例演示上述过程:


先生成两个keystore文件


keytool -genkeypair -alias Alice -keyalg RSA -keystore /tmp/Alice.keystore -storetype pkcs12 keytool -genkeypair -alias Bob -keyalg RSA -keystore /tmp/Bob.keystore -storetype pkcs12

生成证书签名请求文件CSR,即将条目别名为 Alice 的公钥和一些个人信息从密钥库 Alice.keystore 文件中导出,作为证书请求文件


keytool -certreq -alias Alice -keystore /tmp/Alice.keystore -file /tmp/cert.csr

签发证书,使用密钥库Bob.keystore中别名为 Bob 条目的私钥为 cert.csr 签发证书,并保存到 Bob-to-Alice.crt 文件中


keytool -gencert -infile /tmp/cert.csr -outfile /tmp/Bob-to-Alice.crt -alias Bob -keystore /tmp/Bob.keystore

导入签发证书到密钥库,将签发证书 Bob-to-Alice.crt 更新到已存在别名 Alice 的密钥库 Alice.keystore 文件中


keytool -importcert -file /tmp/Bob-to-Alice.crt -alias Alice -keystore /tmp/Alice.keystore 输入密钥库口令: keytool 错误: java.lang.Exception: 无法从回复中建立链

这是因为在更新被签发证书之前,一定要先将签发证书的机构的信任证书导入到密钥库文件,即将密钥库Bob.keystore的证书以其相应的别名导入到密钥库Alice.keystore中。


导出Bob.keystore的信任证书:


keytool -exportcert -keystore /tmp/Bob.keystore -alias Bob -file /tmp/Bob.crt

将信任证书Bob.crt以其别名Bob导入到密钥库Alice.keystore


keytool -importcert -file /tmp/Bob.crt -alias Bob -keystore /tmp/Alice.keystore

再将签发证书Bob-to-Alice.crt以别名Alice导入到密钥库Alice.keystore:


keytool -importcert -file /tmp/Bob-to-Alice.crt -alias Alice -keystore /tmp/Alice.keystore 输入密钥库口令: 证书回复已安装在密钥库中

对比最开始生成的密钥库Alice.keystore的证书信息可发现,别名为Alice条目的证书链已由单个Alice.keystore自签名的证书变为2个证书,分别是Bob.keystore签名的及Alice.keystore的自签名证书。


至此,我们已经基本把对称加密和非对称加密从原理、场景、工具和代码等多方面进行了介绍。


散列函数

接下来我们补充一个在日常开发中也会广泛应用到的算法:信息摘要算法,也叫散列函数、哈希函数。不同于上述两种算法,信息摘要算法并不算是一种加密算法,它有以下几个特点反而让它在某些场景下非常适用:


  1. 固定输入得到固定输出,且不同输入得到相同输出的概率极低
  2. 理论上不能从散列计算之后的值逆向推导出原始明文
  3. 无论输入的数据长度多少,得到的输出值长度是固定的(不同的哈希算法长度不一样)

设想这种场景,我们要在数据库中保存用户密码,首先肯定不能保存明文,既然要使用密文保存,那使用对称加密还是非对称加密呢?结合实际场景和安全性考虑,两种都不合适!因为这两种都要考虑密钥的管理,如果密钥泄漏那密码则有被破解的风险。在这种场景下,我们可以使用单向散列函数来解决这个问题,这样即使算法和密文都泄漏,也无法逆向计算出明文。


当然散列函数还有更多用途,比如文件一致性校验、数字签名等。


常见的哈希算法有:MD5、sha1、sha2(sha224、sha256、sha384、sha512),其中sha1加密后的长度是160字节,sha2加密之后的密文长度和shaxxx的数字相同,比如sha256加密之后,密文长度为256字节。


关于哈希算法的更详细介绍可以看一下这篇文章,最常用的三种哈希算法[5]


openssl dgst 命令

openssl dgst [-md5|-md4|-md2|-sha1|-sha|-mdc2|-ripemd160|-dss1] [-c] [-d] [-hex] [-binary] [-out filename] [-sign filename] [-keyform arg] [-passin arg] [-verify filename] [-prverify filename] [-signature filename] [-hmac key] [file…] -md5|-md4|-md2|-sha1|-sha|-mdc2|-ripemd160|-dss1:哈希算法的名称,默认值为md5。 -c:打印出哈希结果的时候用冒号来分隔开。 -d:详细打印出调试信息 -hex:以十六进制的形式输出结果,这是默认形式。 –binary:以二进制的形式输出结果。 -out filename:输出文件名,如果没有指定就采用标准输出。 -sign filename:从指定文件中读出私钥来对摘要值签名

openssl dgst 示

openssl dgst -sha1 -c /tmp/java-and-more.pem SHA1(/tmp/java-and-more.pem)= 7e:2a:0b:1e:2e:ec:f8:c2:f7:6a:b3:bf:25:98:64:26:f5:66:ab:38

Java代码实现MD5计算

@Test public void test_md5() throws Exception { MessageDigest messageDigest = MessageDigest.getInstance("MD5"); byte[] fileBytes = Files.readAllBytes(Paths.get("/tmp/java-and-more.pem")); messageDigest.update(fileBytes); byte[] result = messageDigest.digest(); System.out.println(Base64.encodeBase64String(result)); }

终于写到这里了,良心力作,码字不易,欢迎收藏。


参考资料

[1]


https://baike.baidu.com/item/Blowfish/1677776: https://link.juejin.cn?target=https://baike.baidu.com/item/Blowfish/1677776


[2]


https://www.jianshu.com/p/c5147a3eaf07: https://link.juejin.cn?target=https://www.jianshu.com/p/c5147a3eaf07


[3]


https://www.jianshu.com/p/78886e480bef: https://link.juejin.cn?target=https://www.jianshu.com/p/78886e480bef


[4]


https://blog.csdn.net/makenothing/article/details/88429511: https://link.juejin.cn?target=https://blog.csdn.net/makenothing/article/details/88429511


[5]


https://blog.csdn.net/zhanxiao5287/article/details/90300222: https://link.juejin.cn?target=https://blog.csdn.net/zhanxiao5287/article/details/90300222


版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至123456@qq.com 举报,一经查实,本站将立刻删除。

联系我们

工作日:9:30-18:30,节假日休息