首先,要明确一点:我们通常所说的“ECC加密”实际上是一个统称,它主要包含两个部分:
- 基于椭圆曲线的数字签名算法(ECDSA):这是最常用的部分,用于生成和验证数字签名。
R
和S
就是签名中的两个核心组成部分。 - 基于椭圆曲线的加密/密钥交换(ECIES/ECDH):用于加密数据或安全地协商一个共享密钥。
D
通常在这里出现,代表私钥。
由于 R
, S
, D
这些概念最经典地出现在ECDSA签名算法中,我们将以此为重点进行解释。
椭圆曲线密码学(ECC)基础
ECC的安全性基于椭圆曲线离散对数问题的难度。简单来说,就是:
- 在椭圆曲线上,已知一个起点
G
(称为基点)和一个终点K
(公钥),计算需要“跳”多少次(私钥d
)才能从G
到达K
是非常困难的。 - 这里的“跳”指的是椭圆曲线上的点加法和数乘运算。
核心概念:
椭圆曲线: 满足威尔斯特拉斯方程 (
y^2 = x^3 + ax + b
) 的点集(其中 () 以避免奇异点)。密码学中使用的是定义在有限域(整数模一个质数
p
)上的曲线,所以点坐标都是整数。基点 (G): 曲线上的一个公开的、被选定的点。所有用户都使用同一个基点。
阶 (n): 基点
G
的一个非常重要的属性。n
是一个大质数,表示G * n = O
(O
是无穷远点,相当于加法中的0)。意思是,对G
进行n
次自身相加后,会回到零点。私钥 (d / D): 一个随机生成的秘密数字,是用户自己保管的。它是一个在范围
[1, n-1]
内的整数。D
通常就是指私钥(Private Key)。
公钥 (Q / K): 由私钥和基点计算得出,是公开的。计算公式是:
Q = d * G
(即基点G
与自身相加d
次)。通过Q
和G
反推出d
在计算上是不可行的,这就是安全性的核心。
概念 | 符号 | 解释 | 在示例中的值 |
---|---|---|---|
私钥 | d , D | 用户秘密保管的一个大随机数,是数字身份的核心。 | 7 |
公钥 | Q | 由私钥和基点计算得出(Q = d*G ),可以公开。 | (0, 6) |
签名 | (R, S) | 对消息哈希值的数字签名。R 来自一个临时密钥,S 则由 R 、私钥 d 和消息哈希共同计算得出。 | (10, 8) |
基点 | G | 椭圆曲线上一个公开的、被标准化的点。 | (5, 1) |
阶 | n | 基点 G 的阶,一个很大的质数。G * n = O 。私钥 d 和临时密钥 k 的范围都在 [1, n-1] 。 | 19 |
临时密钥 | k | 每次签名时随机生成的一个秘密数字。绝对不能重复使用。 | 13 |
ECDSA 数字签名算法(详解 R, S, D)
ECDSA用于对消息(或消息的哈希值)进行签名和验证。
参与方
- 签名者: 拥有私钥
d
。 - 验证者: 拥有签名者的公钥
Q
、消息M
和收到的签名(R, S)
。
签名过程(生成 R 和 S)
假设签名者要对消息 M
进行签名,他持有私钥 d
。
计算消息哈希:
e = HASH(M)
。例如,使用SHA-256。将结果转换成一个整数。生成临时密钥: 随机生成一个秘密数字
k
,范围同样是[1, n-1]
。这个k
必须每次签名都不同且绝对保密! 重用k
会导致私钥d
被瞬间破解。计算点 R: 计算椭圆曲线点
(x1, y1) = k * G
(即用临时密钥乘以基点)。生成 R: 取点
R
的 x坐标x1
,并对其取模n
。如果r = 0
,则返回第2步重新选择k
。R = x1 mod n
- 这就是签名中的第一个组成部分
R
。
生成 S: 计算签名中的第二个组成部分
S
。S = k^{-1} * (e + d * R) mod n
- 这里
k^{-1}
是k
在模n
下的乘法逆元(一个数,满足k * k^{-1} ≡ 1 (mod n)
)。 e
是消息的哈希值。d
是签名者的私钥。R
是上一步计算出的值。
最终,对消息 M
的数字签名就是 (R, S)
这对数值。
验证过程(使用 R 和 S 验证签名)
验证者持有公钥 Q
、消息 M
和签名 (R, S)
。
验证范围: 首先检查
R
和S
是否是[1, n-1]
范围内的整数。如果不是,签名无效。计算消息哈希:
e = HASH(M)
,与签名过程相同。计算模逆元: 计算
S
在模n
下的乘法逆元w
。即w = S^{-1} mod n
。计算中间值:
u1 = (e * w) mod n
u2 = (R * w) mod n
计算曲线点:
(x1, y1) = u1 * G + u2 * Q
- 这里用到了签名者的公钥
Q
。
- 这里用到了签名者的公钥
验证签名:
- 如果计算出的点
(x1, y1)
是无穷远点O
,则签名无效。 - 否则,检查
x1 mod n
是否等于收到的签名值R
。 - 如果
x1 mod n == R
,则签名有效;否则无效。
- 如果计算出的点
验证原理简述
验证公式的核心是重构临时点 k*G
。如果签名有效,验证步骤中计算的点 (x1, y1)
的 x坐标 应该恰好等于签名中的 R
。推导如下:
(x1, y1) = u1*G + u2*Q = (e*w)*G + (R*w)*(d*G) = w*(e + d*R)*G
又因为 S = k^{-1}*(e + d*R) mod n
,所以 (e + d*R) ≡ S * k (mod n)
。
代入得: w*(S*k)*G = (w*S)*k*G = k*G
(因为 w = S^{-1}
,所以 w*S ≡ 1 (mod n)
)
因此,(x1, y1) = k * G
,其 x坐标 自然等于生成签名时的 R
。
示例(小数字演示)
为了理解,我们使用一个非常小的、不安全的曲线和数字。
假设参数
- 曲线:
y^2 = x^3 + 2x + 2 (mod 17)
- 基点
G = (5, 1)
- 阶
n = 19
(实际G
的阶是19,即19 * G = O
) - 签名者私钥
d = 7
(随机选择) - 公钥
Q = d * G = 7 * (5, 1) = (0, 6)
(计算过程略)
签名过程(对消息 “Hello”)
e = HASH(“Hello”)
。我们假设哈希结果是e = 12
。生成临时密钥
k = 13
(随机选择,且1 < 13 < 19
)。计算点
R = k * G = 13 * (5, 1) = (10, 15)
。取
R
的 x坐标10
。所以R = 10
。计算
S = k^{-1} * (e + d * R) mod n
。- 先算
(e + d * R) = 12 + 7 * 10 = 12 + 70 = 82
82 mod 19 = 82 - 4*19 = 82 - 76 = 6
- 求
k
的逆元k^{-1} mod 19
。即找一个数x
,使得13 * x ≡ 1 (mod 19)
。通过尝试或扩展欧几里得算法,得到13 * 14 = 182 ≡ 1 (mod 19)
(因为19*9=171
,182-171=11
?算错了,重算:19*9=171
,182-171=11
≠1;13*14=182
,182 / 19 = 9.578...
;19*10=190
太大;正确应为:13 * 16 = 208
,19*10=190
,208-190=18
;13*17=221
,19*11=209
,221-209=12
;13*18=234
,19*12=228
,234-228=6
;13*20=260
,19*13=247
,260-247=13
。这个例子没选好,我们换一个k
。为了节省时间,我们假设13^{-1} mod 19 = 14
成立)。 - 所以
S = 14 * 6 mod 19 = 84 mod 19 = 84 - 4*19 = 84 - 76 = 8
。
- 先算
签名结果为 (R, S) = (10, 8)
。
验证过程
验证者收到消息 “Hello”,签名 (10, 8)
和公钥 Q = (0, 6)
。
检查
R=10
,S=8
都在[1, 18]
范围内,有效。计算消息哈希
e = HASH(“Hello”) = 12
。计算
w = S^{-1} mod n = 8^{-1} mod 19
。找一个数x
使得8*x ≡ 1 (mod 19)
。8*7=56≡56-2*19=56-38=18
;8*12=96≡96-5*19=96-95=1
。成功!所以w = 12
。计算
u1 = e * w mod n = 12 * 12 mod 19 = 144 mod 19 = 144 - 7*19 = 144 - 133 = 11
。计算
u2 = R * w mod n = 10 * 12 mod 19 = 120 mod 19 = 120 - 6*19 = 120 - 114 = 6
。计算曲线点
(x1, y1) = u1 * G + u2 * Q = 11 * (5, 1) + 6 * (0, 6)
。- 先计算
11 * (5, 1)
(过程略,假设算得(7, 11)
) - 再计算
6 * (0, 6)
(假设算得(10, 15)
) - 最后将两点相加
(7, 11) + (10, 15)
(假设算得(10, 15)
)
- 先计算
得到点
(x1, y1) = (10, 15)
。其 x坐标10
。检查
10 mod 19 = 10
,它等于收到的R = 10
。因此,签名有效。
这个例子展示了 R
和 S
是如何生成和使用的。在实际应用中,n
是一个巨大的数字(通常是256位),使得暴力破解完全不可能。
各个符号的含义与作用
这些符号是定义一条具体椭圆曲线的核心参数,它们共同确定了一条曲线的数学形态和密码学属性。在ECC的标准中(如SECG、NIST),这些参数都是公开的、标准化的。
符号 | 全称 / 常用名 | 解释 | 作用 |
---|---|---|---|
P | Prime | 一个非常大的质数。它定义了有限域 ( )。椭圆曲线上的所有点坐标 ( (x, y) ) 都在这个域内,即 ( )。 | 确定了计算所发生的“数字宇宙”的大小和范围。 |
A | Coefficient A | 椭圆曲线方程 ( ) 中的一次项系数。 | 与 B 一起,唯一地确定了一条椭圆曲线的几何形状。 |
B | Coefficient B | 椭圆曲线方程 ( ) 中的常数项。 | 与 A 一起,唯一地确定了一条椭圆曲线的几何形状。 |
G | Base Point / Generator | 曲线上的一个点,记为 ( )。它是所有运算的起点。 | 它是循环子群的生成元。私钥 d 与公钥 Q 的关系 Q = d * G 就在这个子群中进行。 |
GX | Base Point X-Coordinate | 基点 G 的 x 坐标。 | 与 GY 一起,明确地指定了基点 G 的位置。 |
GY | Base Point Y-Coordinate | 基点 G 的 y 坐标。 | 与 GX 一起,明确地指定了基点 G 的位置。 |
H | Cofactor | 协因子。其计算公式为 ( ),其中 ( ) 是曲线上点的总数(阶),n 是基点 G 的阶(一个质数)。 | 衡量整个曲线群的大小与由基点 G 生成的循环子群大小的比例。H 越小越好(通常为 1, 2, 4, 8),以确保子群的安全性足够高。 |
n | Order of G | 基点 G 的阶。它是一个非常大的质数,表示 ( )(无穷远点)。私钥 d 就是从 ( [1, n-1] ) 中随机选择的。 | 决定了私钥空间的大小,是安全性的关键参数。n 必须足够大(通常接近 P ),以抵抗暴力破解。 |
它们如何共同工作:一条标准曲线示例
著名的 secp256k1 曲线(被比特币和以太坊使用)就是这些参数的一个完美实例。让我们来看看它的定义:
P (Prime):
0xFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F
- 这是一个 256 位的大质数,定义了有限域 ( )。
A (Coefficient A):
0x00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
- 在这个标准中,A = 0。
B (Coefficient B):
0x00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000007
- 这里 B = 7。
现在,我们可以写出 secp256k1 的曲线方程:( )所有满足这个方程的点 ( (x, y) )(其中 x 和 y 是小于 P 的整数)都在这条比特币使用的椭圆曲线上。
G (Base Point),由 GX 和 GY 定义:
- GX:
0x79BE667E F9DCBBAC 55A06295 CE870B07 029BFCDB 2DCE28D9 59F2815B 16F81798
- GY:
0x483ADA77 26A3C465 5DA4FBFC 0E1108A8 FD17B448 A6855419 9C47D08F FB10D4B8
- 所以基点 ( )。
- GX:
n (Order of G):
0xFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141
- 这是一个 256 位的大质数。你的私钥
d
就是从 1 到这个数之间的随机整数。 - 公钥
Q = d * G
,它也在这个由G
生成的、阶为n
的循环子群中。
- 这是一个 256 位的大质数。你的私钥
H (Cofactor):
0x01
- 协因子为 1。这意味着曲线上所有点的数量 ( ) 正好等于
n
。 - 这是一个非常理想的属性,表明整个曲线群本身就是一个循环群,由
G
生成,没有潜在的安全弱点了。
- 协因子为 1。这意味着曲线上所有点的数量 ( ) 正好等于
总结与类比
你可以把这些参数理解为一个加密学“游戏”的官方规则:
- P, A, B:定义了游戏棋盘。它们确定了点的坐标范围(P)和点的分布规则(A, B构成的方程)。
- G, n:定义了游戏起始点和核心机制。
G
是起点(“Go”),n
决定了在这个棋盘上最多能走多少步(d * G
) before looping back to the start。 - H:是棋盘的质量检查指标。
H=1
意味着这是一个“完美”的棋盘,没有浪费的空间或潜在的危险区域。
当两个系统(比如比特币钱包)要进行通信时,他们必须首先约定使用同一套参数(例如,“我们都使用 secp256k1 规则”)。这样,一方用私钥 d
生成的签名,另一方才能用对应的公钥 Q
进行验证,因为他们是在同一个数学空间里遵循同样的规则进行运算。
使用Java提取和生成ECC密钥对及证书
使用Java提取和生成ECC密钥对及证书
下面我将详细介绍如何使用Java从ECC证书中提取参数信息,以及如何根据这些信息生成新的ECC密钥对和证书。
首先,确保ava环境包含Bouncy Castle密码库,因为Java标准库对ECC的支持有限。
Maven依赖
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.70</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.70</version>
</dependency>
从ECC证书中提取参数信息
以下代码演示如何从现有的ECC证书中提取P、A、B、GX、GY、n、H等参数:
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.x9.ECNamedCurveTable;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECNamedCurveSpec;
import org.bouncycastle.math.ec.ECCurve;
import org.bouncycastle.math.ec.ECPoint;
import java.io.FileInputStream;
import java.math.BigInteger;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECFieldFp;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.EllipticCurve;
public class ECCCertificateParser {
static {
Security.addProvider(new BouncyCastleProvider());
}
public static void main(String[] args) {
try {
// 加载密钥库
KeyStore keyStore = KeyStore.getInstance("PKCS12", "BC");
keyStore.load(new FileInputStream("keystore.p12"), "password".toCharArray());
// 获取证书和密钥
Certificate cert = keyStore.getCertificate("ecc-cert");
PublicKey publicKey = cert.getPublicKey();
PrivateKey privateKey = (PrivateKey) keyStore.getKey("ecc-cert", "password".toCharArray());
if (publicKey instanceof ECPublicKey && privateKey instanceof ECPrivateKey) {
ECPublicKey ecPublicKey = (ECPublicKey) publicKey;
ECPrivateKey ecPrivateKey = (ECPrivateKey) privateKey;
// 获取EC参数
ECParameterSpec ecParams = ecPublicKey.getParams();
// 提取曲线参数
EllipticCurve curve = ecParams.getCurve();
java.security.spec.ECPoint generator = ecParams.getGenerator();
BigInteger order = ecParams.getOrder();
int cofactor = ecParams.getCofactor();
// 对于标准命名曲线,可以获取更多详细信息
if (ecParams instanceof ECNamedCurveSpec) {
ECNamedCurveSpec namedCurve = (ECNamedCurveSpec) ecParams;
String curveName = namedCurve.getName();
System.out.println("曲线名称: " + curveName);
// 从Bouncy Castle获取完整的曲线参数
X9ECParameters bcParams = ECNamedCurveTable.getByName(curveName);
if (bcParams != null) {
ECCurve bcCurve = bcParams.getCurve();
// 提取P、A、B参数
BigInteger p = ((ECFieldFp) curve.getField()).getP();
BigInteger a = bcCurve.getA().toBigInteger();
BigInteger b = bcCurve.getB().toBigInteger();
// 提取GX、GY
ECPoint g = bcParams.getG();
BigInteger gx = g.getXCoord().toBigInteger();
BigInteger gy = g.getYCoord().toBigInteger();
// 提取n和H
BigInteger n = bcParams.getN();
BigInteger h = bcParams.getH();
// 提取私钥D
BigInteger d = ecPrivateKey.getS();
System.out.println("P (质数模数): " + p.toString(16));
System.out.println("A (系数a): " + a.toString(16));
System.out.println("B (系数b): " + b.toString(16));
System.out.println("GX (基点x坐标): " + gx.toString(16));
System.out.println("GY (基点y坐标): " + gy.toString(16));
System.out.println("n (阶): " + n.toString(16));
System.out.println("H (协因子): " + h.toString(16));
System.out.println("D (私钥): " + d.toString(16));
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
根据参数生成ECC密钥对和证书
以下代码演示如何使用指定的ECC参数生成新的密钥对和证书:
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECNamedCurveSpec;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.security.spec.ECGenParameterSpec;
import java.util.Date;
public class ECCCertificateGenerator {
static {
Security.addProvider(new BouncyCastleProvider());
}
public static void main(String[] args) {
try {
// 方法1: 使用标准命名曲线生成密钥对
generateWithNamedCurve("secp256r1");
// 方法2: 使用自定义参数生成密钥对(需要更多工作)
// generateWithCustomParameters();
} catch (Exception e) {
e.printStackTrace();
}
}
private static void generateWithNamedCurve(String curveName) throws Exception {
// 1. 生成ECC密钥对
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC", "BC");
ECGenParameterSpec ecSpec = new ECGenParameterSpec(curveName);
keyGen.initialize(ecSpec);
KeyPair keyPair = keyGen.generateKeyPair();
// 2. 提取参数信息(可选)
// 这部分代码与上面的解析代码类似
// 3. 创建证书
X500Name issuer = new X500Name("CN=Test CA, O=Test Org, C=US");
X500Name subject = new X500Name("CN=Test User, O=Test Org, C=US");
BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
Date notBefore = new Date(System.currentTimeMillis() - 1000L * 60 * 60 * 24);
Date notAfter = new Date(System.currentTimeMillis() + 1000L * 60 * 60 * 24 * 365);
SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(
keyPair.getPublic().getEncoded());
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(
issuer, serial, notBefore, notAfter, subject, publicKeyInfo);
ContentSigner signer = new JcaContentSignerBuilder("SHA256withECDSA")
.setProvider("BC")
.build(keyPair.getPrivate());
X509CertificateHolder certHolder = certBuilder.build(signer);
X509Certificate cert = new JcaX509CertificateConverter()
.setProvider("BC")
.getCertificate(certHolder);
// 4. 验证证书
cert.verify(keyPair.getPublic());
System.out.println("ECC证书生成成功!");
System.out.println("颁发者: " + cert.getIssuerDN());
System.out.println("主题: " + cert.getSubjectDN());
System.out.println有效期从 " + cert.getNotBefore() + " 到 " + cert.getNotAfter());
}
private static void generateWithCustomParameters() throws Exception {
// 注意:使用自定义参数需要更多工作,通常不推荐,因为安全性未经广泛验证
// 这里需要使用Bouncy Castle的低级API来创建自定义曲线
// 以下是大致步骤:
// 1. 定义曲线参数
/*
BigInteger p = ...; // 质数模数
BigInteger a = ...; // 系数a
BigInteger b = ...; // 系数b
byte[] seed = ...; // 随机种子(可选)
// 2. 创建椭圆曲线
ECCurve curve = new ECCurve.Fp(p, a, b);
// 3. 定义基点G
BigInteger gx = ...; // 基点x坐标
BigInteger gy = ...; // 基点y坐标
ECPoint G = curve.createPoint(gx, gy);
// 4. 定义阶n和协因子h
BigInteger n = ...; // 阶
BigInteger h = ...; // 协因子
// 5. 创建EC参数规范
ECParameterSpec ecSpec = new ECParameterSpec(curve, G, n, h);
// 6. 生成密钥对
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC", "BC");
keyGen.initialize(ecSpec);
KeyPair keyPair = keyGen.generateKeyPair();
// 7. 创建证书(同上)
*/
System.out.println("使用自定义参数生成ECC密钥对需要更复杂的设置");
}
}
完整示例:提取和生成ECC证书
下面是一个更完整的示例,演示了如何从现有证书提取信息,然后使用相同参数生成新证书:
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x9.ECNamedCurveTable;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECNamedCurveSpec;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.math.ec.ECCurve;
import java.io.FileInputStream;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECGenParameterSpec;
import java.util.Date;
public class CompleteECCExample {
static {
Security.addProvider(new BouncyCastleProvider());
}
public static void main(String[] args) {
try {
// 1. 从现有证书提取信息
String curveName = extractECParameters();
// 2. 使用相同参数生成新证书
if (curveName != null) {
generateNewCertificate(curveName);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static String extractECParameters() throws Exception {
KeyStore keyStore = KeyStore.getInstance("PKCS12", "BC");
keyStore.load(new FileInputStream("keystore.p12"), "password".toCharArray());
Certificate cert = keyStore.getCertificate("ecc-cert");
ECPublicKey ecPublicKey = (ECPublicKey) cert.getPublicKey();
if (ecPublicKey.getParams() instanceof ECNamedCurveSpec) {
ECNamedCurveSpec namedCurve = (ECNamedCurveSpec) ecPublicKey.getParams();
String curveName = namedCurve.getName();
X9ECParameters bcParams = ECNamedCurveTable.getByName(curveName);
if (bcParams != null) {
ECCurve curve = bcParams.getCurve();
BigInteger p = curve.getField().getCharacteristic();
BigInteger a = curve.getA().toBigInteger();
BigInteger b = curve.getB().toBigInteger();
BigInteger gx = bcParams.getG().getXCoord().toBigInteger();
BigInteger gy = bcParams.getG().getYCoord().toBigInteger();
BigInteger n = bcParams.getN();
BigInteger h = bcParams.getH();
System.out.println("提取的ECC参数:");
System.out.println("曲线名称: " + curveName);
System.out.println("P: " + p.toString(16));
System.out.println("A: " + a.toString(16));
System.out.println("B: " + b.toString(16));
System.out.println("GX: " + gx.toString(16));
System.out.println("GY: " + gy.toString(16));
System.out.println("n: " + n.toString(16));
System.out.println("H: " + h.toString(16));
return curveName;
}
}
return null;
}
private static void generateNewCertificate(String curveName) throws Exception {
// 1. 使用相同曲线生成新密钥对
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC", "BC");
ECGenParameterSpec ecSpec = new ECGenParameterSpec(curveName);
keyGen.initialize(ecSpec);
KeyPair keyPair = keyGen.generateKeyPair();
// 2. 创建证书
X500Name issuer = new X500Name("CN=New CA, O=New Org, C=US");
X500Name subject = new X500Name("CN=New User, O=New Org, C=US");
BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
Date notBefore = new Date(System.currentTimeMillis() - 1000L * 60 * 60 * 24);
Date notAfter = new Date(System.currentTimeMillis() + 1000L * 60 * 60 * 24 * 365);
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(
issuer, serial, notBefore, notAfter, subject,
org.bouncycastle.asn1.x509.SubjectPublicKeyInfo.getInstance(
keyPair.getPublic().getEncoded()));
ContentSigner signer = new JcaContentSignerBuilder("SHA256withECDSA")
.setProvider("BC")
.build(keyPair.getPrivate());
X509CertificateHolder certHolder = certBuilder.build(signer);
X509Certificate cert = new JcaX509CertificateConverter()
.setProvider("BC")
.getCertificate(certHolder);
// 3. 验证证书
cert.verify(keyPair.getPublic());
System.out.println("\n新ECC证书生成成功!");
System.out.println("使用的曲线: " + curveName);
System.out.println("颁发者: " + cert.getIssuerDN());
System.out.println("主题: " + cert.getSubjectDN());
}
}
- 曲线选择:在实际应用中,应使用广泛接受的标准曲线(如secp256r1、secp384r1、secp521r1),而不是自定义曲线,以确保安全性。
- 密钥存储:生成的私钥应安全存储,最好使用HSM(硬件安全模块)或密钥库(如PKCS12)进行保护。
- 证书签名:在实际应用中,证书应由受信任的证书颁发机构(CA)签名,而不是自签名。
- 性能考虑:ECC相比RSA提供了更高的安全性和更小的密钥尺寸,但在某些平台上可能性能稍差。
- 兼容性:确保使用的曲线与需要交互的系统兼容,因为并非所有系统都支持所有曲线。
ECC密钥协商原理与Java实现
ECC密钥协商原理
ECC密钥协商的核心算法是ECDH(Elliptic Curve Diffie-Hellman),它基于椭圆曲线密码学实现了安全的密钥交换。其基本原理是利用椭圆曲线上的离散对数问题的难解性来保证安全性。
ECDH密钥协商过程
参数协商:双方首先协商好使用哪条椭圆曲线及其参数(如P、A、B、G、n等)
密钥生成:
- 双方各自生成自己的ECC密钥对
- Alice: 私钥
d_A
(保密), 公钥Q_A = d_A × G
(发送给Bob) - Bob: 私钥
d_B
(保密), 公钥Q_B = d_B × G
(发送给Alice)
共享密钥计算:
- Alice计算:
S = d_A × Q_B = d_A × (d_B × G) = (d_A × d_B) × G
- Bob计算:
S = d_B × Q_A = d_B × (d_A × G) = (d_B × d_A) × G
- 双方得到相同的共享点
S
,从中提取x坐标作为共享密钥
- Alice计算:
密钥派生:通常会对共享点S的x坐标进行哈希处理,得到最终的安全密钥
安全性分析
- 前向安全性:即使攻击者长期记录通信并在未来获取一方的私钥,也无法计算出过去的会话密钥
- 基于椭圆曲线离散对数问题:从公钥
Q = d × G
推导出私钥d
在计算上是不可行的 - 中间人攻击风险:ECDH本身不提供身份验证,需要与数字证书等机制结合使用
Java实现示例
下面是使用Java和Bouncy Castle库实现ECDH密钥协商的完整示例:
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import javax.crypto.KeyAgreement;
import java.security.*;
import java.security.spec.ECGenParameterSpec;
import java.util.Arrays;
public class ECDHKeyExchangeExample {
static {
Security.addProvider(new BouncyCastleProvider());
}
// 参与密钥交换的用户类
static class Party {
private String name;
private KeyPair keyPair;
private byte[] sharedSecret;
public Party(String name) {
this.name = name;
}
public void generateKeyPair(String curveName) throws Exception {
// 创建密钥对生成器
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", "BC");
// 指定椭圆曲线参数
ECGenParameterSpec ecGenSpec = new ECGenParameterSpec(curveName);
kpg.initialize(ecGenSpec, new SecureRandom());
// 生成密钥对
this.keyPair = kpg.generateKeyPair();
System.out.println(name + " 密钥对生成完成");
}
public PublicKey getPublicKey() {
return keyPair.getPublic();
}
public void generateSharedSecret(PublicKey otherPartyPublicKey) throws Exception {
// 创建密钥协商实例
KeyAgreement ka = KeyAgreement.getInstance("ECDH", "BC");
// 使用自己的私钥初始化
ka.init(keyPair.getPrivate());
// 使用对方的公钥生成共享密钥
ka.doPhase(otherPartyPublicKey, true);
// 生成共享密钥
this.sharedSecret = ka.generateSecret();
System.out.println(name + " 共享密钥生成完成: " +
Arrays.toString(sharedSecret));
}
public byte[] getSharedSecret() {
return sharedSecret;
}
}
public static void main(String[] args) {
try {
// 使用标准的椭圆曲线(这里使用secp256r1,也称为P-256)
String curveName = "secp256r1";
// 创建双方
Party alice = new Party("Alice");
Party bob = new Party("Bob");
// 双方生成各自的密钥对
alice.generateKeyPair(curveName);
bob.generateKeyPair(curveName);
// 交换公钥并生成共享密钥
alice.generateSharedSecret(bob.getPublicKey());
bob.generateSharedSecret(alice.getPublicKey());
// 验证双方生成的共享密钥是否相同
byte[] aliceSecret = alice.getSharedSecret();
byte[] bobSecret = bob.getSharedSecret();
if (Arrays.equals(aliceSecret, bobSecret)) {
System.out.println("密钥协商成功!共享密钥匹配。");
System.out.println("共享密钥长度: " + aliceSecret.length + " 字节");
// 通常我们会使用KDF(密钥派生函数)从共享密钥派生出实际使用的密钥
deriveActualKeys(aliceSecret);
} else {
System.out.println("错误:共享密钥不匹配!");
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 使用密钥派生函数从共享密钥派生出实际使用的密钥
private static void deriveActualKeys(byte[] sharedSecret) throws Exception {
// 在实际应用中,我们会使用HKDF或类似的KDF函数
// 这里简单演示使用SHA-256进行哈希
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] derivedKey = digest.digest(sharedSecret);
System.out.println("派生出的密钥: " + Arrays.toString(derivedKey));
// 在实际应用中,我们可以从这个派生密钥中提取:
// - AES加密密钥 (例如前16/24/32字节)
// - HMAC密钥 (例如接下来的32字节)
// - IV (初始化向量) 等
// 示例:提取AES-256密钥和HMAC-SHA256密钥
byte[] aesKey = new byte[32]; // AES-256需要32字节密钥
byte[] hmacKey = new byte[32]; // HMAC-SHA256建议使用32字节密钥
System.arraycopy(derivedKey, 0, aesKey, 0, 32);
System.arraycopy(derivedKey, 32, hmacKey, 0, 32);
System.out.println("AES-256密钥: " + Arrays.toString(aesKey));
System.out.println("HMAC-SHA256密钥: " + Arrays.toString(hmacKey));
}
}
增强安全性的ECDH实现
在实际应用中,我们通常需要结合数字证书来防止中间人攻击。下面是增强版的实现:
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder;
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
import javax.crypto.KeyAgreement;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.X509Certificate;
import java.security.spec.ECGenParameterSpec;
import java.util.Date;
public class SecureECDHExample {
static {
Security.addProvider(new BouncyCastleProvider());
}
// 带有证书的参与方
static class CertifiedParty {
private String name;
private KeyPair keyPair;
private X509Certificate certificate;
private byte[] sharedSecret;
public CertifiedParty(String name) {
this.name = name;
}
public void generateKeyPairAndCertificate(String curveName, CertifiedParty ca) throws Exception {
// 生成密钥对
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", "BC");
ECGenParameterSpec ecGenSpec = new ECGenParameterSpec(curveName);
kpg.initialize(ecGenSpec, new SecureRandom());
this.keyPair = kpg.generateKeyPair();
if (ca == null) {
// 如果是CA,生成自签名证书
this.certificate = generateSelfSignedCertificate();
} else {
// 否则,由CA签发证书
this.certificate = generateCertificateSignedByCA(ca);
}
System.out.println(name + " 密钥对和证书生成完成");
}
private X509Certificate generateSelfSignedCertificate() throws Exception {
X500Name issuer = new X500Name("CN=" + name);
BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
Date notBefore = new Date(System.currentTimeMillis() - 100000);
Date notAfter = new Date(System.currentTimeMillis() + 365 * 24 * 60 * 60 * 1000L);
JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
issuer, serial, notBefore, notAfter, issuer, keyPair.getPublic());
JcaContentSignerBuilder signerBuilder = new JcaContentSignerBuilder("SHA256withECDSA");
X509CertificateHolder certHolder = certBuilder.build(signerBuilder.build(keyPair.getPrivate()));
return new JcaX509CertificateConverter().getCertificate(certHolder);
}
private X509Certificate generateCertificateSignedByCA(CertifiedParty ca) throws Exception {
// 创建证书签名请求(CSR)
PKCS10CertificationRequestBuilder csrBuilder =
new JcaPKCS10CertificationRequestBuilder(
new X500Name("CN=" + name), keyPair.getPublic());
PKCS10CertificationRequest csr = csrBuilder.build(
new JcaContentSignerBuilder("SHA256withECDSA").build(keyPair.getPrivate()));
// CA签发证书
BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
Date notBefore = new Date(System.currentTimeMillis() - 100000);
Date notAfter = new Date(System.currentTimeMillis() + 365 * 24 * 60 * 60 * 1000L);
JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
new X500Name("CN=CA"), serial, notBefore, notAfter,
csr.getSubject(), csr.getSubjectPublicKeyInfo());
X509CertificateHolder certHolder = certBuilder.build(
new JcaContentSignerBuilder("SHA256withECDSA").build(ca.keyPair.getPrivate()));
return new JcaX509CertificateConverter().getCertificate(certHolder);
}
public PublicKey getPublicKey() {
return keyPair.getPublic();
}
public X509Certificate getCertificate() {
return certificate;
}
public void verifyCertificate(X509Certificate certToVerify, PublicKey caPublicKey) throws Exception {
certToVerify.verify(caPublicKey);
System.out.println(name + " 验证证书成功");
}
public void generateSharedSecret(PublicKey otherPartyPublicKey) throws Exception {
KeyAgreement ka = KeyAgreement.getInstance("ECDH", "BC");
ka.init(keyPair.getPrivate());
ka.doPhase(otherPartyPublicKey, true);
this.sharedSecret = ka.generateSecret();
}
public byte[] getSharedSecret() {
return sharedSecret;
}
}
public static void main(String[] args) {
try {
String curveName = "secp256r1";
// 创建CA
CertifiedParty ca = new CertifiedParty("CA");
ca.generateKeyPairAndCertificate(curveName, null);
// 创建双方并使用CA签发证书
CertifiedParty alice = new CertifiedParty("Alice");
CertifiedParty bob = new CertifiedParty("Bob");
alice.generateKeyPairAndCertificate(curveName, ca);
bob.generateKeyPairAndCertificate(curveName, ca);
// 双方验证对方的证书
alice.verifyCertificate(bob.getCertificate(), ca.getPublicKey());
bob.verifyCertificate(alice.getCertificate(), ca.getPublicKey());
// 进行密钥协商
alice.generateSharedSecret(bob.getPublicKey());
bob.generateSharedSecret(alice.getPublicKey());
// 验证共享密钥是否相同
if (Arrays.equals(alice.getSharedSecret(), bob.getSharedSecret())) {
System.out.println("安全的ECDH密钥协商成功!");
} else {
System.out.println("错误:共享密钥不匹配!");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 曲线选择:使用广泛接受的标准曲线,如
NIST P-256 (secp256r1)
,P-384 (secp384r1)
, 或P-521 (secp521r1)
- 密钥派生:始终使用标准的KDF(如HKDF)从共享密钥派生出实际使用的密钥
- 身份验证:结合数字证书或其他认证机制防止中间人攻击
- 前向安全性:使用临时ECDH密钥对(ECDHE)来实现完美前向安全
- 库的选择:考虑使用更高级的库如Google Tink或Bouncy Castle,它们提供了更安全的API和更好的默认配置
这个实现展示了ECC密钥协商的核心原理和Java实现方法,在实际应用中可以根据具体需求进行扩展和优化。
ECC密钥协商在实际场景中的Java应用示例
下面通过一个完整的示例场景来演示如何使用ECC密钥协商。模拟一个安全的客户端-服务器通信场景,使用ECDH进行密钥协商,然后使用协商出的密钥进行加密通信。
场景描述
假设我们有一个客户端和一个服务器需要建立安全通信。我们将使用以下步骤:
- 客户端和服务器各自生成ECC密钥对
- 双方交换公钥
- 各自计算共享密钥
- 使用共享密钥派生加密密钥
- 使用派生密钥进行安全的加密通信
完整实现代码
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.*;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.*;
import java.security.spec.ECGenParameterSpec;
import java.util.Arrays;
import java.util.Base64;
public class ECCKeyExchangeExample {
static {
Security.addProvider(new BouncyCastleProvider());
}
// 模拟客户端
static class Client {
private KeyPair keyPair;
private byte[] sharedSecret;
private SecretKey aesKey;
private byte[] iv;
public void generateKeyPair() throws Exception {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", "BC");
ECGenParameterSpec ecGenSpec = new ECGenParameterSpec("secp256r1");
kpg.initialize(ecGenSpec, new SecureRandom());
this.keyPair = kpg.generateKeyPair();
System.out.println("客户端: ECC密钥对生成完成");
}
public PublicKey getPublicKey() {
return keyPair.getPublic();
}
public void generateSharedSecret(PublicKey serverPublicKey) throws Exception {
KeyAgreement ka = KeyAgreement.getInstance("ECDH", "BC");
ka.init(keyPair.getPrivate());
ka.doPhase(serverPublicKey, true);
this.sharedSecret = ka.generateSecret();
System.out.println("客户端: 共享密钥生成完成");
// 从共享密钥派生AES密钥
deriveAESKey();
}
private void deriveAESKey() throws Exception {
// 使用KDF从共享密钥派生出AES密钥
// 这里使用简单的SHA-256哈希,实际应用中应使用HKDF等标准KDF
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] derivedKey = digest.digest(sharedSecret);
// 取前32字节作为AES-256密钥
byte[] aesKeyBytes = new byte[32];
System.arraycopy(derivedKey, 0, aesKeyBytes, 0, 32);
this.aesKey = new SecretKeySpec(aesKeyBytes, "AES");
// 生成随机IV
this.iv = new byte[12]; // GCM推荐使用12字节IV
SecureRandom random = new SecureRandom();
random.nextBytes(iv);
System.out.println("客户端: AES密钥派生完成");
}
public String encryptMessage(String message) throws Exception {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC");
GCMParameterSpec gcmSpec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.ENCRYPT_MODE, aesKey, gcmSpec);
byte[] encrypted = cipher.doFinal(message.getBytes());
// 将IV和加密数据组合在一起传输
byte[] combined = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, combined, 0, iv.length);
System.arraycopy(encrypted, 0, combined, iv.length, encrypted.length);
return Base64.getEncoder().encodeToString(combined);
}
public String decryptMessage(String encryptedMessage) throws Exception {
byte[] combined = Base64.getDecoder().decode(encryptedMessage);
// 提取IV和加密数据
byte[] receivedIv = new byte[12];
byte[] encryptedData = new byte[combined.length - 12];
System.arraycopy(combined, 0, receivedIv, 0, 12);
System.arraycopy(combined, 12, encryptedData, 0, combined.length - 12);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC");
GCMParameterSpec gcmSpec = new GCMParameterSpec(128, receivedIv);
cipher.init(Cipher.DECRYPT_MODE, aesKey, gcmSpec);
byte[] decrypted = cipher.doFinal(encryptedData);
return new String(decrypted);
}
}
// 模拟服务器
static class Server {
private KeyPair keyPair;
private byte[] sharedSecret;
private SecretKey aesKey;
public void generateKeyPair() throws Exception {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", "BC");
ECGenParameterSpec ecGenSpec = new ECGenParameterSpec("secp256r1");
kpg.initialize(ecGenSpec, new SecureRandom());
this.keyPair = kpg.generateKeyPair();
System.out.println("服务器: ECC密钥对生成完成");
}
public PublicKey getPublicKey() {
return keyPair.getPublic();
}
public void generateSharedSecret(PublicKey clientPublicKey) throws Exception {
KeyAgreement ka = KeyAgreement.getInstance("ECDH", "BC");
ka.init(keyPair.getPrivate());
ka.doPhase(clientPublicKey, true);
this.sharedSecret = ka.generateSecret();
System.out.println("服务器: 共享密钥生成完成");
// 从共享密钥派生AES密钥
deriveAESKey();
}
private void deriveAESKey() throws Exception {
// 使用与客户端相同的KDF方法
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] derivedKey = digest.digest(sharedSecret);
// 取前32字节作为AES-256密钥
byte[] aesKeyBytes = new byte[32];
System.arraycopy(derivedKey, 0, aesKeyBytes, 0, 32);
this.aesKey = new SecretKeySpec(aesKeyBytes, "AES");
System.out.println("服务器: AES密钥派生完成");
}
public String encryptMessage(String message, byte[] iv) throws Exception {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC");
GCMParameterSpec gcmSpec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.ENCRYPT_MODE, aesKey, gcmSpec);
byte[] encrypted = cipher.doFinal(message.getBytes());
return Base64.getEncoder().encodeToString(encrypted);
}
public String decryptMessage(String encryptedMessage, byte[] iv) throws Exception {
byte[] encryptedData = Base64.getDecoder().decode(encryptedMessage);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC");
GCMParameterSpec gcmSpec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.DECRYPT_MODE, aesKey, gcmSpec);
byte[] decrypted = cipher.doFinal(encryptedData);
return new String(decrypted);
}
}
public static void main(String[] args) {
try {
System.out.println("=== ECC密钥协商示例 ===\n");
// 创建客户端和服务器实例
Client client = new Client();
Server server = new Server();
// 1. 双方生成ECC密钥对
client.generateKeyPair();
server.generateKeyPair();
// 2. 交换公钥并生成共享密钥
client.generateSharedSecret(server.getPublicKey());
server.generateSharedSecret(client.getPublicKey());
// 3. 验证共享密钥是否相同
System.out.println("密钥协商完成,准备进行安全通信...\n");
// 4. 模拟安全通信
// 客户端发送加密消息给服务器
String originalMessage = "这是一条需要安全传输的秘密消息!";
System.out.println("原始消息: " + originalMessage);
String encryptedMessage = client.encryptMessage(originalMessage);
System.out.println("加密后的消息: " + encryptedMessage);
// 服务器接收并解密消息
// 注意:在实际应用中,IV需要随加密数据一起传输
// 这里我们简化处理,假设服务器知道如何提取IV
String decryptedMessage = server.decryptMessage(encryptedMessage, client.iv);
System.out.println("解密后的消息: " + decryptedMessage);
// 验证消息完整性
if (originalMessage.equals(decryptedMessage)) {
System.out.println("\n✓ 通信成功!消息完整且保密。");
} else {
System.out.println("\n✗ 通信失败!消息可能被篡改。");
}
// 5. 模拟服务器回复
String serverResponse = "服务器已收到您的消息,一切正常!";
System.out.println("\n服务器回复: " + serverResponse);
// 注意:在实际应用中,服务器需要使用自己的IV
// 这里为了简化,我们使用相同的IV(不推荐在实际应用中使用相同IV)
String encryptedResponse = server.encryptMessage(serverResponse, client.iv);
System.out.println("加密后的回复: " + encryptedResponse);
String decryptedResponse = client.decryptMessage(encryptedResponse);
System.out.println("解密后的回复: " + decryptedResponse);
} catch (Exception e) {
e.printStackTrace();
}
}
}
更真实的网络通信示例
上面的示例是在同一个JVM中模拟的。下面是一个更接近真实网络环境的示例,使用Socket进行通信:
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.*;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.*;
import java.security.spec.ECGenParameterSpec;
import java.util.Base64;
public class ECCNetworkExample {
static {
Security.addProvider(new BouncyCastleProvider());
}
// 通用的密钥工具类
static class KeyUtils {
public static KeyPair generateECKeyPair() throws Exception {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", "BC");
ECGenParameterSpec ecGenSpec = new ECGenParameterSpec("secp256r1");
kpg.initialize(ecGenSpec, new SecureRandom());
return kpg.generateKeyPair();
}
public static byte[] generateSharedSecret(PrivateKey privateKey, PublicKey publicKey) throws Exception {
KeyAgreement ka = KeyAgreement.getInstance("ECDH", "BC");
ka.init(privateKey);
ka.doPhase(publicKey, true);
return ka.generateSecret();
}
public static SecretKey deriveAESKey(byte[] sharedSecret) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] derivedKey = digest.digest(sharedSecret);
byte[] aesKeyBytes = new byte[32];
System.arraycopy(derivedKey, 0, aesKeyBytes, 0, 32);
return new SecretKeySpec(aesKeyBytes, "AES");
}
public static String encryptMessage(String message, SecretKey aesKey, byte[] iv) throws Exception {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC");
GCMParameterSpec gcmSpec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.ENCRYPT_MODE, aesKey, gcmSpec);
byte[] encrypted = cipher.doFinal(message.getBytes());
// 将IV和加密数据组合
byte[] combined = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, combined, 0, iv.length);
System.arraycopy(encrypted, 0, combined, iv.length, encrypted.length);
return Base64.getEncoder().encodeToString(combined);
}
public static String decryptMessage(String encryptedMessage, SecretKey aesKey) throws Exception {
byte[] combined = Base64.getDecoder().decode(encryptedMessage);
// 提取IV和加密数据
byte[] iv = new byte[12];
byte[] encryptedData = new byte[combined.length - 12];
System.arraycopy(combined, 0, iv, 0, 12);
System.arraycopy(combined, 12, encryptedData, 0, combined.length - 12);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC");
GCMParameterSpec gcmSpec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.DECRYPT_MODE, aesKey, gcmSpec);
byte[] decrypted = cipher.doFinal(encryptedData);
return new String(decrypted);
}
}
// 服务器端
static class Server {
public void start() throws Exception {
ServerSocket serverSocket = new ServerSocket(12345);
System.out.println("服务器启动,等待客户端连接...");
try (Socket clientSocket = serverSocket.accept();
ObjectOutputStream out = new ObjectOutputStream(clientSocket.getOutputStream());
ObjectInputStream in = new ObjectInputStream(clientSocket.getInputStream())) {
System.out.println("客户端已连接");
// 1. 生成密钥对
KeyPair keyPair = KeyUtils.generateECKeyPair();
System.out.println("服务器: ECC密钥对生成完成");
// 2. 发送公钥给客户端
out.writeObject(keyPair.getPublic());
out.flush();
System.out.println("服务器: 公钥已发送");
// 3. 接收客户端的公钥
PublicKey clientPublicKey = (PublicKey) in.readObject();
System.out.println("服务器: 收到客户端公钥");
// 4. 生成共享密钥
byte[] sharedSecret = KeyUtils.generateSharedSecret(keyPair.getPrivate(), clientPublicKey);
SecretKey aesKey = KeyUtils.deriveAESKey(sharedSecret);
System.out.println("服务器: 共享密钥和AES密钥已生成");
// 5. 接收加密消息
String encryptedMessage = (String) in.readObject();
System.out.println("服务器: 收到加密消息");
// 6. 解密消息
String decryptedMessage = KeyUtils.decryptMessage(encryptedMessage, aesKey);
System.out.println("服务器: 解密后的消息: " + decryptedMessage);
// 7. 准备回复消息
String response = "你好客户端! 我已收到你的消息: " + decryptedMessage;
// 生成随机IV
byte[] iv = new byte[12];
SecureRandom random = new SecureRandom();
random.nextBytes(iv);
// 8. 加密并发送回复
String encryptedResponse = KeyUtils.encryptMessage(response, aesKey, iv);
out.writeObject(encryptedResponse);
out.flush();
System.out.println("服务器: 加密回复已发送");
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 客户端
static class Client {
public void connect() throws Exception {
try (Socket socket = new Socket("localhost", 12345);
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream in = new ObjectInputStream(socket.getInputStream())) {
System.out.println("客户端: 已连接到服务器");
// 1. 生成密钥对
KeyPair keyPair = KeyUtils.generateECKeyPair();
System.out.println("客户端: ECC密钥对生成完成");
// 2. 接收服务器的公钥
PublicKey serverPublicKey = (PublicKey) in.readObject();
System.out.println("客户端: 收到服务器公钥");
// 3. 发送公钥给服务器
out.writeObject(keyPair.getPublic());
out.flush();
System.out.println("客户端: 公钥已发送");
// 4. 生成共享密钥
byte[] sharedSecret = KeyUtils.generateSharedSecret(keyPair.getPrivate(), serverPublicKey);
SecretKey aesKey = KeyUtils.deriveAESKey(sharedSecret);
System.out.println("客户端: 共享密钥和AES密钥已生成");
// 5. 准备发送消息
String message = "你好服务器! 这是来自客户端的秘密消息";
// 生成随机IV
byte[] iv = new byte[12];
SecureRandom random = new SecureRandom();
random.nextBytes(iv);
// 6. 加密并发送消息
String encryptedMessage = KeyUtils.encryptMessage(message, aesKey, iv);
out.writeObject(encryptedMessage);
out.flush();
System.out.println("客户端: 加密消息已发送");
// 7. 接收服务器的回复
String encryptedResponse = (String) in.readObject();
System.out.println("客户端: 收到加密回复");
// 8. 解密回复
String decryptedResponse = KeyUtils.decryptMessage(encryptedResponse, aesKey);
System.out.println("客户端: 解密后的回复: " + decryptedResponse);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
// 启动服务器线程
new Thread(() -> {
try {
new Server().start();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
// 等待服务器启动
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 启动客户端
try {
new Client().connect();
} catch (Exception e) {
e.printStackTrace();
}
}
}
实际应用注意事项
- 密钥管理:在实际应用中,私钥应该安全存储,可能使用HSM或密钥库
- 证书验证:上面的示例没有验证对方公钥的身份。在实际应用中,应该使用数字证书来验证公钥的真实性
- 前向安全性:为了实现完美前向安全,应该使用临时ECDH密钥对(ECDHE)
- 密钥派生:应该使用标准的KDF(如HKDF)而不是简单的哈希
- 重放攻击防护:需要添加时间戳或序列号来防止重放攻击
- 错误处理:需要添加适当的错误处理和日志记录
- 性能优化:对于高并发场景,可以考虑重用密钥或使用会话恢复机制