相关文章推荐
俊逸的青蛙  ·  MSBUILD ...·  1 月前    · 
  • Diffie-Hellman的流程原理
  • 流程原理在Java中的对应
  • Java应用代码
  • JDK源码中封装的原理细节
    • 生成密钥对
    • 通过对方公钥和自己的私钥生成相同的对称密钥

本文的思路:

  • 先了解Diffie-Hellman的流程原理,然后将其流程和Java的实现对应起来;理解了原理和Java实现的流程,再写应用代码进一步辅助验证;最后走一走源码流程中的相关细节,作最终验证。
  • 文末了解一下性能更好、安全性更高的ECDH(基于椭圆曲线来实现的Diffie-Hellman)

一、Diffie-Hellman的流程原理

DH的流程中涉及三个角色,主要是通信双方Alice、Bobby,和可能存在窃听的中间人Eaves。

场景:Alice要和Bobby通信,发送的信息可能会被中间人Eaves窃听到,Alice和Bobby如何通过DH算法来保证通信的机密性?

DH是密钥协商(key agreement)算法,通信双方并没有真正的交换密钥,而是通过交换部分可以公开的信息来计算生成出相同的对称密钥,然后通过对称密钥对消息进行加密,从而保证消息的机密性。

DH的流程原理如下:

  • 1.发送方Alice:
    • 选择两个质数P和G——大质数P和生成元G
    • 然后生成一个随机数A作为自己的私钥
    • 接着计算(G^A)modP作为自己的公钥
    • 最后把P、G和公钥(G^A)modP通过可能被监听的网络发送给接收方Bobby
    • 注意:私钥A是不公开、不发送的,Alice自己保留
  • 2.接收方Bobby:
    • 收到Alice的消息后,首先选择一个随机数B作为自己的私钥
    • 然后计算(G^B)modP作为自己的公钥
    • 最后Bobby将自己的公钥(G^B)modP发送给Alice
    • 注意:私钥B是不公开、不发送的,Bobby自己保留
  • 3.双方计算出相同的对称密钥
    • Alice通过自己的私钥A和对方的公钥(G^B)modP计算出相同的对称密钥:((G^BmodP)^A)modP,即 (G^(B*A))modP
    • Bobby通过自己的私钥B和对方的公钥(G^A)modP计算出相同的对称密钥:((G^AmodP)^B)modP,即 (G^(A*B))modP
  • 4.最后算双方通过计算出的、相同的对称密钥进行加密通信

再做进一步的抽象,四小步骤实际上可以抽象成两个阶段:

  1. 第一阶段(第1~3步):通过DH算法进行密钥协商,协商出对称密钥
  2. 第二阶段(第4步):通过协商出的对称密钥进行加密通信

我们分析一下在Alice、Bobby协商密钥的过程中,中间人Eaves通过窃听到的信息能否计算出密钥:

  • Alice拥有的信息:大质数P、生成元G、自己的公钥(G^A)modP和私钥A、对方Bobby的公钥(G^B)modP,然后通过自己的私钥和对方的公钥计算出对称密钥
  • Bobby拥有的信息:大质数P、生成元G、自己的公钥(G^B)modP和私钥B、对方Alice的公钥(G^A)modP,然后通过自己的私钥和对方的公钥计算出对称密钥
  • Eaves能窃听到的公开信息:大质数P、生成元G、Alice的公钥(G^A)modP、Bobby的公钥公钥(G^B)modP, 因为Eaves没有私钥A或B,所以无法计算出密钥

二、流程原理在Java中的对应

JDK的API封装了很多细节(大质数P、生成元G、私钥A/B,公钥(G^A)modP/(G^B)modP都封装到了PublicKey或PrivateKey对象中),对外程序员能看到的类就是公钥、私钥,DH流程原理的细节对应放在第四部分再剖析,这一部分只做大概的、整体上的流程对应,目的是为了顺利写出第三部分的应用代码。

1.密钥协商阶段,这一部分主要是DH算法:

  • Alice通过KeyPairGenerator生成自己的密钥对,保留私钥PrivateKey,然后将公钥PublicKey发送给Bobby,通过网络一般不直接发送Java对象,而是发送公钥的字节数组;
  • Bobby收到Alice发过来的公钥数组:
    • 先通过X509EncodedKeySpec解析公钥规范,然后通过KeyFactory还原出公钥对象PublicKey(实际上公钥对象包含了大质数P、生成元G和Alice的公钥Y)
    • 接着通过还原出的公钥对象生成自己的密钥对
    • 然后将自己的公钥数组回送给Alice
  • 最后,Alice和Bobby都通过自己的私钥和对方的公钥经由KeyAgreement类计算出相同的共享密钥SecretKey(这里要重点关注)

2.加密通信阶段,这一部分主要是对称加密算法 ,本文选择现役、流行的AES算法为例:

  • 通过Cipher.getInstance工厂方法拿到加解密对象
  • 设置加密的参数:cipher_mode、密钥协商阶段得到的SecretKey、分组密码分组的模式所需的初始化向量IvParameterSpec
  • doFinal完成加解密

三、应用代码

我们按照第二部分中的流程来。

1.Alice生成密钥对

	// 密钥协商算法密钥对生成入参,查看KeyPairGenerator.getInstance文档
	private static String DH_KEY = "DH";
	 * Alice生成密钥对
	 * @return Alice的密钥对
	 * @throws Exception
	public static KeyPair generateKeyPair() throws Exception {
		KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(DH_KEY);
		return keyPairGenerator.generateKeyPair();

代码很简洁,几乎都是固定格式,你需要关注的是KeyPairGenerator.getInstance方法的入参,这个字符串从哪里去找?

JDK中都在一个页面找,后面的getInstance都在相同的页面,只是要在页面找不同的类的文档,页面地址:​​​​​​Java Security Standard Algorithm Names

找到KeyPairGenerator类的文档,截图如下:

 2.Bobby根据Alice的公钥生成自己的密钥对

* Bobby收到Alice的公钥数组: * 1.先将公钥数组解析成公钥对象 * KeyFactory.getInstance的入参查看KeyFactory的文档 * 文档地址:https://docs.oracle.com/en/java/javase/15/docs/specs/security/standard-names.html * 2.根据Alice的公钥对象生成自己的密钥对 * @param publicKeyArray 收到的、对方的公钥数组 * @return * @throws Exception public static KeyPair generateKeyPair(byte[] publicKeyArray) throws Exception { // 将Alice发过来的公钥数组解析成公钥对象 X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyArray); KeyFactory keyFactory = KeyFactory.getInstance(DH_KEY); PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec); // Bobby根据Alice的公钥生成自己的密钥对 DHParameterSpec dhParameterSpec = ((DHPublicKey)publicKey).getParams(); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(DH_KEY); keyPairGenerator.initialize(dhParameterSpec); return keyPairGenerator.generateKeyPair();

Alice将自己的公钥通过网络传送给Bobby,一般传送的是公钥数组,所以当Bobby收到Alice的公钥数组以后,通过X509EncodedKeySpec封装公钥数组,再通过KeyFactory还原出公钥对象,这里要关注两个问题:

  • 公钥私钥规范;
  • KeyFactory.getInstance的入参

a.公钥私钥规范

Java中公钥数组通过X509EncodedKeySpec规范来解析,私钥数组通过PKCS8EncodedKeySpec规范来解析,代码都是一样的。

X509EncodedKeySpec:This class represents the ASN.1 encoding of a public key, encoded according to the ASN.1 type SubjectPublicKeyInfo.

PKCS8EncodedKeySpec:This class represents the ASN.1 encoding of a private key, encoded according to the ASN.1 type PrivateKeyInfo.

ASN.1是一种数据格式标准;PKCS(The Public-Key Cryptography Standards)是公钥密码标准,目前有15个标准,其中PKCS8是私钥格式标准,这一部分你可以自行拓展。

b.KeyFactory.getInstance的入参

需要查询的JDK文档和KeyPairGenerator是一样的,只是要查页面KeyFactory部分:

3.Alice和Bobby根据自己的私钥和对方的公钥生成相同的对称密钥

这一部分代码不多,但是你需要重点关注,因为这是将密钥协商阶段和加密通信阶段连接起来的部分,两个阶段通过这一部分计算出来的对称密钥衔接在一起。

该部分需要关注JDK的版本,在JDK 8u161前后的写法是不一样的。我们先看JDK 8u161之前的写法,然后过一下JDK 8u161中的变化,最后再看看现在的写法。

在JDK 8u161之前:

* Alice和Bobby通过自己的私钥和对方的公钥计算生成相同的对称密钥 * KeyAgreement.getInstance入参查看相同的文档 * 文档地址:https://docs.oracle.com/en/java/javase/15/docs/specs/security/standard-names.html * @param publicKey 对方的公钥 * @param privateKey 自己的私钥 * @return 用于信息加密的对称密钥 * @throws Exception @Deprecated public static SecretKey generateSecretKey(PublicKey publicKey, PrivateKey privateKey) throws Exception { KeyAgreement keyAgreement = KeyAgreement.getInstance(DH_KEY); keyAgreement.init(privateKey); keyAgreement.doPhase(publicKey, true); return keyAgreement.generateSecret(AES_KEY);

KeyAgreement.gtInstance的入参找法和上面都是一样的,关注点在KeyAgreement.generateSecret方法。

keyAgreement.generateSecret(AES_KEY)在JDK 8u161前是可以正常实用,之后不再推荐使用了,直接报异常:

这个异常信息很迷啊,实际上并不是不支持AES算法,根据异常链的信息,你直接点到DHKeyAgreement类中去看:

这个AllowKDF.VALUE就是一个系统变量:

这个系统变量默认是没有设置的,你通过System.out.println(System.getProperty("jdk.crypto.KeyAgreement.legacyKDF"))打印出来值是null,所以在JDK 8u161之后你想要用这个方法,你要先设置这个系统变量:

System.setProperty("jdk.crypto.KeyAgreement.legacyKDF", "true");

但是Oracle官方并不推荐这样做,我们转去看看JDK 8u161的更新文档。

JDK 8u161 Update Release Notes:Java™ SE Development Kit 8, Update 161 (oracle.com)

不推荐使用的原因截图如下:

那现在应该怎么做呢,官方给了三种方式,截图如下:

A是根据相关密钥规范实现密钥派生功能,这对密码学素养要求比较高,对绝大多数程序员来说不现实,直接pass;

C就是设置System.getProperty("jdk.crypto.KeyAgreement.legacyKDF")系统变量,启用keyAgreement.generateSecret(AES_KEY)方法,但是不安全,直接pass;

那就只有B了,实现简单,也足够安全,代码也几乎是固定格式:

* Alice和Bobby通过自己的私钥和对方的公钥计算生成相同的对称密钥 * 注意:当前推荐的用法 * @param publicKey 对方的公钥 * @param privateKey 自己的私钥 * @return 生成的对称密钥 * @throws Exception public static SecretKey generateSecretKeyBySHA256(PublicKey publicKey, PrivateKey privateKey) throws Exception { KeyAgreement keyAgreement = KeyAgreement.getInstance(DH_KEY); keyAgreement.init(privateKey); keyAgreement.doPhase(publicKey, true); // 当前推荐的做法 byte[] keyArray = keyAgreement.generateSecret(); MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); byte[] digest = messageDigest.digest(keyArray); return new SecretKeySpec(digest, AES_KEY);

同样,MessageDigest.getInstance入参查询的也是同样的页面。

Java对单向散列函数的支持是最容易上手的了,就是一个类MessageDigest和一个方法digest,然后你只要知道不再使用退役算法,熟悉现役、常见的算法就行了。例如本例中SHA-256,现役、常用的单向散列函数,安全强度128bit,散列值长度256bit。

单向散列函数的命名是有规律的,安全强度是其输出的散列值的一半,输出的散列值长度就在算法名中,例如SHA256,安全强度128bit、散列值256bit;SHA384,安全强度192bit、散列值384bit;SHA512,安全强度256bit、散列值512bit。

其他还需要知道的:

  • MD系列已经退役了,MD5散列值128bit,安全强度才仅仅18bit,只需要26W次计算就能破解,按照当前的算力分分钟就无了;
  • SHA1系列已经退役了;
  • SHA2系列,SHA-256往上都是现役;
  • SHA3系列,SHA3-256以上都是现役;
  • 单向散列函数一定要注意避免长度延展攻击,SHA-512/224和SHA-512/256已经避免了该问题,如果选用其他现役算法,那就要注意公开消息和私密消息的顺序问题;

4.Alice和Bobby通过对称密钥对消息进行加解密后通信

* 生成分组密码分组模式的初始化向量 * 本例中,AES的分组128bit,16字节 * @param length * @return public static byte[] initIV(int length) { return random.generateSeed(length); * @param secretKey 密钥协商阶段协商的对称密钥 * @param iv 分组密码分组模式的初始化向量,加密和解密需要使用相同的iv * @param data 需要加密的明文字节数组 * @return 密文 * @throws Exception public static byte[] encryption(SecretKey secretKey, byte[] iv, byte[] data) throws Exception { // 密钥和算法有关系,和加解密模式没有关系,所以上面分成了AES_KEY和CIPHER_MODE Cipher cipher = Cipher.getInstance(CIPHER_MODE); // 分组密码分组模式的初始化向量参数,本例中是CBC模式 IvParameterSpec ivp = new IvParameterSpec(iv); cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivp); return cipher.doFinal(data); * @param secretKey 密钥协商阶段协商的对称密钥 * @param iv 分组密码分组模式的初始化向量,加密和解密需要使用相同的iv * @param data 需要解密的密文的字节数组 * @return 解密后的明文 * @throws Exception public static byte[] decryption(SecretKey secretKey, byte[] iv, byte[] data) throws Exception { Cipher cipher = Cipher.getInstance(CIPHER_MODE); // 分组密码分组模式的初始化向量参数,本例中是CBC模式 IvParameterSpec ivp = new IvParameterSpec(iv); cipher.init(Cipher.DECRYPT_MODE, secretKey, ivp); return cipher.doFinal(data);

Cipher.getInstance的入参查询方法和上面一样。

分组密码的模式参看java DES_厚积薄发者,轻舟万重山-CSDN博客

5.最后看一下完整的代码和简单的测试用例

package wxy.secret;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
import javax.crypto.SecretKey;
import javax.crypto.interfaces.DHPublicKey;
import javax.crypto.spec.DHParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class DiffieHellman {
	// JDK文档地址:https://docs.oracle.com/en/java/javase/15/docs/specs/security/standard-names.html
	// 密钥协商算法密钥对生成入参,查看KeyPairGenerator.getInstance文档
	private static String DH_KEY = "DH";
	// 对称加密算法密钥生成入参,参看KeyGenerator.getInstance文档
	private static String AES_KEY = "AES";
	// 分组密码的模式入参,参看Cipher.getInstance文档 
	private static String CIPHER_MODE = "AES/CBC/PKCS5Padding";
	private static SecureRandom random = new SecureRandom();
	 * Alice生成密钥对
	 * KeyPairGenerator.getInstance的入参查看KeyPairGenerator的文档
	 *  文档地址:https://docs.oracle.com/en/java/javase/15/docs/specs/security/standard-names.html
	 * @return Alice的密钥对
	 * @throws Exception
	public static KeyPair generateKeyPair() throws Exception {
		KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(DH_KEY);
		return keyPairGenerator.generateKeyPair();
	 * Bobby收到Alice的公钥数组:
	 * 1.先将公钥数组解析成公钥对象
	 * 	KeyFactory.getInstance的入参查看KeyFactory的文档
	 *  文档地址:https://docs.oracle.com/en/java/javase/15/docs/specs/security/standard-names.html
	 * 2.根据Alice的公钥对象生成自己的密钥对
	 * @param publicKeyArray
	 * @return
	 * @throws Exception
	public static KeyPair generateKeyPair(byte[] publicKeyArray) throws Exception {
		// 将Alice发过来的公钥数组解析成公钥对象
		X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyArray);
		KeyFactory keyFactory = KeyFactory.getInstance(DH_KEY);
		PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
		// Bobby根据Alice的公钥生成自己的密钥对
		DHParameterSpec dhParameterSpec = ((DHPublicKey)publicKey).getParams();
		KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(DH_KEY);
		keyPairGenerator.initialize(dhParameterSpec);
		return keyPairGenerator.generateKeyPair();
	 * Alice和Bobby通过自己的私钥和对方的公钥计算生成相同的对称密钥
	 *  KeyAgreement.getInstance入参查看相同的文档
	 *  文档地址:https://docs.oracle.com/en/java/javase/15/docs/specs/security/standard-names.html
	 * 注意:这种实现方式不安全,已不推荐使用,参看JDK 8u161的更新文档
	 * 	文档地址:https://www.oracle.com/java/technologies/javase/8u161-relnotes.html
	 * @param publicKey 对方的公钥
	 * @param privateKey 自己的私钥
	 * @return 用于信息加密的对称密钥
	 * @throws Exception
	@Deprecated
	public static SecretKey generateSecretKey(PublicKey publicKey, PrivateKey privateKey) throws Exception {
		KeyAgreement keyAgreement = KeyAgreement.getInstance(DH_KEY);
		keyAgreement.init(privateKey);
		keyAgreement.doPhase(publicKey, true);
		// 关注keyAgreement.generateSecret(AES_KEY),在JDK 8u161前后有很大差异
		return keyAgreement.generateSecret(AES_KEY);
	 * Alice和Bobby通过自己的私钥和对方的公钥计算生成相同的对称密钥
	 * 注意:当前推荐的用法
	 * @param publicKey 对方的公钥
	 * @param privateKey 自己的私钥
	 * @return 生成的对称密钥
	 * @throws Exception
	public static SecretKey generateSecretKeyBySHA256(PublicKey publicKey, PrivateKey privateKey) throws Exception {
		KeyAgreement keyAgreement = KeyAgreement.getInstance(DH_KEY);
		keyAgreement.init(privateKey);
		keyAgreement.doPhase(publicKey, true);
		// 当前推荐的做法
		byte[] keyArray = keyAgreement.generateSecret();
		MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
		byte[] digest = messageDigest.digest(keyArray);
		return new SecretKeySpec(digest, AES_KEY);
	 * 生成分组密码分组模式的初始化向量
	 * 本例中,AES的分组128bit,16字节
	 * @param length
	 * @return
	public static byte[] initIV(int length) {
		return random.generateSeed(length);
	 * @param secretKey 密钥协商阶段协商的对称密钥
	 * @param iv 分组密码分组模式的初始化向量,加密和解密需要使用相同的iv
	 * @param data 需要加密的明文字节数组
	 * @return 密文
	 * @throws Exception
	public static byte[] encryption(SecretKey secretKey, byte[] iv, byte[] data) throws Exception {
		// 密钥和算法有关系,和加解密模式没有关系,所以上面分成了AES_KEY和CIPHER_MODE
		Cipher cipher = Cipher.getInstance(CIPHER_MODE);
		// 分组密码分组模式的初始化向量参数,本例中是CBC模式
		IvParameterSpec ivp = new IvParameterSpec(iv);
		cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivp);
		return cipher.doFinal(data);
	 * @param secretKey 密钥协商阶段协商的对称密钥
	 * @param iv 分组密码分组模式的初始化向量,加密和解密需要使用相同的iv
	 * @param data 需要解密的密文的字节数组
	 * @return 解密后的明文
	 * @throws Exception
	public static byte[] decryption(SecretKey secretKey, byte[] iv, byte[] data) throws Exception {
		Cipher cipher = Cipher.getInstance(CIPHER_MODE);
		// 分组密码分组模式的初始化向量参数,本例中是CBC模式
		IvParameterSpec ivp = new IvParameterSpec(iv);
		cipher.init(Cipher.DECRYPT_MODE, secretKey, ivp);
		return cipher.doFinal(data);
		// 不推荐通过该方式启用keyAgreement.generateSecret(AES_KEY)方法
		//System.setProperty("jdk.crypto.KeyAgreement.legacyKDF", "true");
		// 1.Alice生成密钥对
		KeyPair keyPairA = DiffieHellman.generateKeyPair();
		// 2.Alice将自己的公钥发送给Bobby,实际上公钥对象中封装了大质数P、生成元G和Alice的公钥Y
		PublicKey publicKeyA = keyPairA.getPublic();
		byte[] publicKeyArray = publicKeyA.getEncoded();
		// 3.Bobby根据Alice的公钥对象生成自己的密钥对
		KeyPair keyPairB = DiffieHellman.generateKeyPair(publicKeyArray);
		// 4.Bobby将自己的公钥回送给Alice
		PublicKey publicKeyB = keyPairB.getPublic();
		// 5.Alice根据自己的私钥和Bobby的公钥生成对称密钥
		SecretKey secretKeyA = DiffieHellman.generateSecretKeyBySHA256(publicKeyB, keyPairA.getPrivate());
		// 6.Bobby根据自己的私钥和Alice的公钥生成对称密钥
		SecretKey secretKeyB = DiffieHellman.generateSecretKeyBySHA256(publicKeyA, keyPairB.getPrivate());
		System.out.print("Alice生成的对称密钥:");
		HexStringTool.print(secretKeyA.getEncoded());
		System.out.print("Bobby生成的对称密钥:");
		HexStringTool.print(secretKeyB.getEncoded());
		String message = "Diffie-Hellman密钥协商算法简介";
		byte[] data = message.getBytes(Charset.forName("UTF-8"));
		// 初始化分组密码分组模式的初始化向量,本例AES CBC模式,分组长度128bit,所以IV也是128bit
		byte[] iv = DiffieHellman.initIV(16);
		// 加密
		byte[] ciphertext = DiffieHellman.encryption(secretKeyA, iv, data);
		System.out.print("加密后的密文:");
		HexStringTool.print(ciphertext);
		// 解密
		byte[] plaintext = DiffieHellman.decryption(secretKeyB, iv, ciphertext);
		System.out.print("解密后的明文:");
		HexStringTool.print(plaintext);
		// 将解密后的字节数组还原成UTF8编码的字符
		System.out.println("解密得到的明文数组还原成UTF8编码:"+new String(plaintext, Charset.forName("UTF-8")));
Alice生成的对称密钥:3735c170f563519c92f80f89548bac53f68607f5a89a2a6d348b0f77a9fb6228
Bobby生成的对称密钥:3735c170f563519c92f80f89548bac53f68607f5a89a2a6d348b0f77a9fb6228
加密后的密文:4270b6ed0b6cea65bb927a6738ff1db96cb40960ac326eb7a0ee2e32a71accacd3df2612e205450ead6ef222d7e302b2
解密后的明文:4469666669652d48656c6c6d616ee5af86e992a5e58d8fe59586e7ae97e6b395e7ae80e4bb8b
解密得到的明文数组还原成UTF8编码:Diffie-Hellman密钥协商算法简介

四、源码分析简述

这一部分是将JDK API中封装的原理细节剖析出来,Diffie-Hellman原理参看第一部分,对应的Java API流程参看第二部分。

1.生成密钥对

入口:keyPairGenerator.generateKeyPair()

实现:com.sun.crypto.provider.DHKeyPairGenerator.generateKeyPair()

* Generates a key pair. * @return the new key pair public KeyPair generateKeyPair() { if (random == null) { random = SunJCE.getRandom(); if (params == null) { try { params = ParameterCache.getDHParameterSpec(pSize, random); } catch (GeneralSecurityException e) { // should never happen throw new ProviderException(e); BigInteger p = params.getP(); BigInteger g = params.getG(); if (lSize <= 0) { lSize = pSize >> 1; // use an exponent size of (pSize / 2) but at least 384 bits if (lSize < 384) { lSize = 384; BigInteger x; BigInteger pMinus2 = p.subtract(BigInteger.TWO); // PKCS#3 section 7.1 "Private-value generation" // Repeat if either of the followings does not hold: // 0 < x < p-1 // 2^(lSize-1) <= x < 2^(lSize) // generate random x up to 2^lSize bits long x = new BigInteger(lSize, random); } while ((x.compareTo(BigInteger.ONE) < 0) || ((x.compareTo(pMinus2) > 0)) || (x.bitLength() != lSize)); // calculate public value y BigInteger y = g.modPow(x, p); DHPublicKey pubKey = new DHPublicKey(y, p, g, lSize); DHPrivateKey privKey = new DHPrivateKey(x, p, g, lSize); return new KeyPair(pubKey, privKey);

其中,大质数P、生成元G,x = new BigInteger(lSize, random)生成的随机数x作为私钥,BigInteger y = g.modPow(x, p)计算出的y作为公钥(g的x次方mod p)。

然后私钥对象封装了大质数P、生成元G和私钥X;公钥对象封装了大质数P、生成元G和公钥Y。

2.通过自己的私钥和对方的公钥生成相同的对称密钥

入口:keyAgreement.generateSecret();

实现:com.sun.crypto.provider.DHKeyAgreement.engineGenerateSecret(byte[], int)

* Generates the shared secret, and places it into the buffer * <code>sharedSecret</code>, beginning at <code>offset</code>. * <p>If the <code>sharedSecret</code> buffer is too small to hold the * result, a <code>ShortBufferException</code> is thrown. * In this case, this call should be repeated with a larger output buffer. * <p>This method resets this <code>KeyAgreementSpi</code> object, * so that it * can be reused for further key agreements. Unless this key agreement is * reinitialized with one of the <code>engineInit</code> methods, the same * private information and algorithm parameters will be used for * subsequent key agreements. * @param sharedSecret the buffer for the shared secret * @param offset the offset in <code>sharedSecret</code> where the * shared secret will be stored * @return the number of bytes placed into <code>sharedSecret</code> * @exception IllegalStateException if this key agreement has not been * completed yet * @exception ShortBufferException if the given output buffer is too small * to hold the secret protected int engineGenerateSecret(byte[] sharedSecret, int offset) throws IllegalStateException, ShortBufferException if (generateSecret == false) { throw new IllegalStateException ("Key agreement has not been completed yet"); if (sharedSecret == null) { throw new ShortBufferException ("No buffer provided for shared secret"); BigInteger modulus = init_p; int expectedLen = (modulus.bitLength() + 7) >>> 3; if ((sharedSecret.length - offset) < expectedLen) { throw new ShortBufferException ("Buffer too short for shared secret"); // Reset the key agreement after checking for ShortBufferException // above, so user can recover w/o losing internal state generateSecret = false; * NOTE: BigInteger.toByteArray() returns a byte array containing * the two's-complement representation of this BigInteger with * the most significant byte is in the zeroth element. This * contains the minimum number of bytes required to represent * this BigInteger, including at least one sign bit whose value * is always 0. * Keys are always positive, and the above sign bit isn't * actually used when representing keys. (i.e. key = new * BigInteger(1, byteArray)) To obtain an array containing * exactly expectedLen bytes of magnitude, we strip any extra * leading 0's, or pad with 0's in case of a "short" secret. byte[] secret = this.y.modPow(this.x, modulus).toByteArray(); if (secret.length == expectedLen) { System.arraycopy(secret, 0, sharedSecret, offset, secret.length); } else { // Array too short, pad it w/ leading 0s if (secret.length < expectedLen) { System.arraycopy(secret, 0, sharedSecret, offset + (expectedLen - secret.length), secret.length); } else { // Array too long, check and trim off the excess if ((secret.length == (expectedLen+1)) && secret[0] == 0) { // ignore the leading sign byte System.arraycopy(secret, 1, sharedSecret, offset, expectedLen); } else { throw new ProviderException("Generated secret is out-of-range"); return expectedLen;

重点就关注两句代码:

byte[] secret = this.y.modPow(this.x, modulus).toByteArray();

其中modulus就是大质数P:

BigInteger modulus = init_p;

所以this.y.modPow(this.x, modulus).toByteArray()这句代码中,y是对方的公钥,x是自己的私钥,modulus是大质数P,即y的x次方mod p。

这些源码封装了的实现细节和第一部分的原理一模一样。

鉴于篇幅问题,ECDH就不再写出来了,你可以自己试一试,本质都是DH密钥协商算法,只是通过ECC椭圆曲线来实现。实现代码几乎都是一致的,只是由于历史原因,ECDH中KeyPairGenerator.getInstance和KeyAgreement.getInstance的入参不一样,注意这个细节就行了。

写文章确实很费时间,就到这里吧,感谢阅读。

本文的思路是这样的:先了解Diffie-Hellman的流程原理,然后将其流程和Java的实现对应起来;理解了原理和Java实现的流程,再写应用代码进一步辅助验证,最后走一走源码流程中的相关细节,做最终验证;最后,再了解一下性能更好、安全性更高的ECDH(基于椭圆曲线来实现的Diffie-Hellman)
diffie-Hellman(DH)算法原理 Diffie-Hellman算法是Whitefield Diffie和Martin Hellman在1976年公布的一种秘交换算法,它是一种建立秘的方法,而不是加方法,所以秘必须和其他一种加算法结合使用。这种秘交换技术的目的在于使两个用户安全的交换一个秘一遍后面的报文加Diffie-Hellman交换算法的有效性依赖于计算离散对数的难度。 1)由消息发送的一方构建,这里由甲方构建。 2)由构建的一方向对方公布其公
公开码学(英语:Public-key cryptography)也称非对称式码学(英语:Asymmetric cryptography)是码学的一种算法,它需要两个,一个是公开,另一个是私有;公用作加,私则用作解。使用公把明文加后所得的文,只能用相对应的私才能解并得到原本的明文,最初用来加的公不能用作解。由于加和解需要两个不同的,故被称为非对称加;不同于加和解都使用同一个的对称加。公可以公开,可任意向外发布;私不可以公开,必须由用户自行严
Bob利用对称秘K对信息进行加并将加结果发送给Alice,Alice收到信息之后,用同样的秘进行解。 问题1:Alice是如何知道对称秘K的?------即,Bob首先需要将对称秘K发送给Alice,Alice才能对信息进行解。即:通信双方需要达成共识,也就是用什么样的秘进行加。 由此推出:传递信息的前提是需要关于秘达成共识,也就是传递秘。 问题2:...
对称加算法在加和解时使用的是同一个秘;而非对称加算法需要两个来进行加和解,这两个秘是公开(public key,简称公)和私有(private key,简称私)。 是一种 高级的双保险加方式,一般的实现方式有DH交换算法,RSA基于因子分解算法,ElGamal离散对数算法及ECC椭圆曲线加等。 ------------------DH加--
import javax.crypto.Cipher; import javax.crypto.KeyAgreement; import javax.crypto.SecretKey; import javax.crypto.interfaces.DHPublicKey; import javax.crypto.spec.DHParameterSpec; import java.securi
<br /><br /> <br />Diffie-Hellman交换代码如下:<br />import java.util.ArrayList;   <br />import java.util.List;   <br />import java.util.Random;<br />import java.math.*;<br /> <br />public class DHtest {<br /> <br />    public static void main(String[] IM消息自定义消息体设计时要考虑到可能产生的安全问题,如果消息体没有进行加措施,很容易被抓包截取,消息加要考虑到各个流程的可能出现的安全性问题! 下面是IM消息加设计的协商过程: C S (public key)P1,K1(AES) p1(k1)---&gt; (private k...
Diffie-Hellman算法是一种交换协议,用于在不安全的通信渠道上安全地交换Java中可以使用javax.crypto包中的KeyAgreement类来实现Diffie-Hellman算法。具体步骤如下: 1. 创建KeyPairGenerator对象,指定算法为DiffieHellman。 2. 生成对,包括公和私。 3. 创建KeyAgreement对象,指定算法为DiffieHellman。 4. 初始化KeyAgreement对象,传入自己的私。 5. 使用对方的公,执行KeyAgreement对象的doPhase方法,生成共享。 6. 将共享用于加通信。 需要注意的是,Diffie-Hellman算法只能用于交换,不能用于加和解数据。在实际应用中,通常会使用共享来加数据,例如使用AES算法。 示例代码如下: ```java import javax.crypto.KeyAgreement; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; public class DiffieHellmanExample { public static void main(String[] args) throws NoSuchAlgorithmException { // 创建KeyPairGenerator对象,指定算法为DiffieHellman KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DiffieHellman"); // 生成对,包括公和私 KeyPair keyPair = keyPairGenerator.generateKeyPair(); byte[] publicKey = keyPair.getPublic().getEncoded(); byte[] privateKey = keyPair.getPrivate().getEncoded(); // 创建KeyAgreement对象,指定算法为DiffieHellman KeyAgreement keyAgreement = KeyAgreement.getInstance("DiffieHellman"); // 初始化KeyAgreement对象,传入自己的私 keyAgreement.init(keyPair.getPrivate()); // 假设对方的公为publicKey2 byte[] publicKey2 = ...; // 使用对方的公,执行KeyAgreement对象的doPhase方法,生成共享 keyAgreement.doPhase(publicKey2, true); SecretKey sharedSecretKey = keyAgreement.generateSecret("AES"); // 将共享用于加通信 byte[] plaintext = "Hello, world!".getBytes(); byte[] ciphertext = encrypt(plaintext, sharedSecretKey); byte[] decryptedPlaintext = decrypt(ciphertext, sharedSecretKey); System.out.println(new String(decryptedPlaintext)); private static byte[] encrypt(byte[] plaintext, SecretKey key) { // 使用共享key加plaintext return null; private static byte[] decrypt(byte[] ciphertext, SecretKey key) { // 使用共享key解ciphertext return null;