密钥协商算法Diffie-Hellman的Java实现

目录

  • 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类的文档,截图如下:

密钥协商算法Diffie-Hellman的Java实现_第1张图片

 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部分:

密钥协商算法Diffie-Hellman的Java实现_第2张图片

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类中去看:

密钥协商算法Diffie-Hellman的Java实现_第3张图片

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

密钥协商算法Diffie-Hellman的Java实现_第4张图片

这个系统变量默认是没有设置的,你通过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)

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

密钥协商算法Diffie-Hellman的Java实现_第5张图片

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

密钥协商算法Diffie-Hellman的Java实现_第6张图片

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)
        //
        do {
            // 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
     * sharedSecret, beginning at offset.
     *
     * 

If the sharedSecret buffer is too small to hold the * result, a ShortBufferException is thrown. * In this case, this call should be repeated with a larger output buffer. * *

This method resets this KeyAgreementSpi object, * so that it * can be reused for further key agreements. Unless this key agreement is * reinitialized with one of the engineInit 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 sharedSecret where the * shared secret will be stored * * @return the number of bytes placed into sharedSecret * * @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的入参不一样,注意这个细节就行了。

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

你可能感兴趣的:(J2SE,Diffie-Hellman,密钥协商算法)