Java ECC算法实战:从原理到应用场景与避坑指南
1. 项目概述:为什么要在Java里搞懂ECC算法?
如果你是一个Java开发者,最近在接触加密、签名或者区块链相关的项目,那么“ECC算法”这个词大概率已经在你眼前晃过好几次了。它不像RSA那样“家喻户晓”,但却是现代密码学里一个极其重要且高效的基石。简单来说,ECC(Elliptic Curve Cryptography,椭圆曲线密码学)是一种基于椭圆曲线数学的公钥密码体系。它的核心魅力在于,在同等安全强度下,ECC所需的密钥长度远小于RSA。举个例子,一个256位的ECC密钥,其安全强度大致相当于一个3072位的RSA密钥。这意味着更小的存储空间、更快的计算速度和更低的网络带宽消耗。
对于Java开发者而言,理解并能在项目中应用ECC,已经从一个加分项变成了许多场景下的必备技能。无论是为移动App实现安全的登录签名,为微服务间的通信提供轻量级TLS证书支持,还是开发区块链钱包或智能合约,ECC都扮演着关键角色。网上关于ECC的数学理论浩如烟海,但能把原理、Java实现、应用场景和实际踩坑经验讲透的中文资料并不多。很多人看了半天公式,还是不知道在Java里怎么生成一个密钥对,或者如何用Signature类完成一次ECDSA签名验证。这篇文章,我就从一个一线开发者的角度,带你绕过那些晦涩的数学推导,直击ECC在Java中的核心应用,手把手给出可运行的示例代码,并分享几个我实际项目中遇到的“坑”和解决技巧。
2. ECC核心原理与优势:不只是“更短的密钥”
在深入代码之前,我们有必要花几分钟理解ECC为什么强,以及它和RSA的根本区别。这能帮助你在未来做技术选型时,做出更合理的决策。
2.1 椭圆曲线数学的直观理解
你可以暂时忘掉那些复杂的韦尔斯特拉斯方程。我们可以用一个不太严谨但非常直观的类比来理解:想象一个台球桌(椭圆曲线),桌上有一个白球(基点G)。我们定义一种特殊的“击球”规则(椭圆曲线上的点加运算)。当你用球杆以某个力度和角度击打白球一次(私钥d),白球会经过一系列碰撞最终停在某个位置(公钥Q)。这个“击球”过程是相对容易的(由私钥计算公钥)。但是,如果我只告诉你白球最终停在了哪里(公钥Q),让你倒推出我是用多大的力气、什么角度击打的(私钥d),这在计算上是极其困难的。这就是椭圆曲线离散对数问题(ECDLP),是ECC安全性的基石。
2.2 与RSA的对比:为何选择ECC?
选择ECC通常不是因为它“更高级”,而是因为它更适应现代计算环境的需求。我们通过一个表格来直观对比:
| 特性维度 | RSA | ECC | 对开发者的意义 |
|---|---|---|---|
| 安全强度/密钥长度 | 较低。2048位是当前最低安全要求。 | 极高。256位即可达到相当高的安全水平。 | ECC证书和密钥体积更小,特别适合存储空间受限的IoT设备或移动端。 |
| 计算速度 | 加解密、签名验证较慢,尤其是密钥长度增大后。 | 更快。在相同安全强度下,ECC的运算速度远超RSA。 | 意味着更低的服务器CPU开销,更高的TPS(每秒事务处理数),对高性能服务至关重要。 |
| 带宽消耗 | 密钥和签名数据较大。 | 更省流量。公钥、签名长度显著缩短。 | 对于移动网络、API频繁交互的场景,能有效减少网络传输负载,提升响应速度。 |
| 标准化与支持 | 极其成熟,历史久,所有系统、语言支持完美。 | 非常成熟,现代系统(TLS 1.3优先使用ECC)、语言和库均已提供良好支持。 | Java从早期版本就通过JCE支持ECC,现在使用上已无门槛。 |
| 适用场景 | 传统SSL/TLS证书、数字签名、数据加密。 | 移动安全、物联网、区块链、数字货币、轻量级TLS。 | 在新兴和资源受限领域,ECC几乎是默认选择。 |
注意:虽然ECC优势明显,但RSA并未过时。在需要与大量遗留系统交互,或者某些特定硬件只支持RSA的场景下,RSA仍是可靠选择。不过,在新项目,尤其是对性能和资源有要求的项目中,我通常会优先评估ECC。
3. Java中的ECC实现:从标准库到实战
Java通过Java Cryptography Architecture (JCA) 和 Java Cryptography Extension (JCE) 提供了对ECC的完整支持。我们不需要引入第三方加密库(如Bouncy Castle)就能完成大部分操作,这降低了依赖复杂度。下面,我将分步骤拆解核心操作。
3.1 密钥对生成:安全性的起点
生成ECC密钥对是第一步。你需要指定使用的椭圆曲线标准。目前最常用的是secp256r1(也称为prime256v1,被TLS 1.3和许多区块链项目广泛采用)和secp256k1(比特币和以太坊使用的曲线)。
import java.security.*; import java.security.spec.ECGenParameterSpec; public class ECCKeyGenerator { public static KeyPair generateKeyPair(String curveName) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { // 1. 获取KeyPairGenerator实例,指定算法为EC KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("EC"); // 2. 初始化,指定椭圆曲线参数 ECGenParameterSpec ecSpec = new ECGenParameterSpec(curveName); // 例如 "secp256r1" keyPairGen.initialize(ecSpec, new SecureRandom()); // 使用强随机数源 // 3. 生成密钥对 return keyPairGen.generateKeyPair(); } public static void main(String[] args) throws Exception { // 生成 secp256r1 曲线密钥对 KeyPair keyPair = generateKeyPair("secp256r1"); PrivateKey privateKey = keyPair.getPrivate(); PublicKey publicKey = keyPair.getPublic(); System.out.println("私钥算法: " + privateKey.getAlgorithm()); System.out.println("私钥格式: " + privateKey.getFormat()); // 通常是 PKCS#8 System.out.println("公钥算法: " + publicKey.getAlgorithm()); System.out.println("公钥格式: " + publicKey.getFormat()); // 通常是 X.509 // 如果需要查看具体的曲线参数或坐标点,需要进一步转换 // java.security.interfaces.ECPublicKey ecPubKey = (ECPublicKey) publicKey; // System.out.println("曲线: " + ecPubKey.getParams()); } }实操要点与避坑指南:
- 曲线选择:除非有明确要求(如对接比特币系统),否则在通用商业应用中,优先使用
secp256r1。它经过更长时间的广泛审查,被NIST等标准机构推荐。secp256k1主要在加密货币领域。 - 随机数源:
SecureRandom是关键。在生产环境中,务必确保JVM有足够的熵源(如/dev/random或/dev/urandom),否则密钥生成会阻塞或变得可预测。在Linux服务器上,检查熵池大小是上线前的一个必要步骤。 - 密钥存储:生成的
PrivateKey和PublicKey对象是易失的。实际项目中,你必须将它们安全地持久化。私钥通常用密码加密后存储为PKCS#12(.p12或.pfx)或JKS格式。公钥可以导出为X.509证书或裸的SPKI格式。
3.2 数字签名与验证 (ECDSA)
这是ECC最经典的应用。发送方用私钥对消息摘要签名,接收方用公钥验证签名,以此确认消息的完整性和来源真实性。
import java.security.*; import java.util.Base64; public class ECDSASignatureDemo { public static byte[] signData(byte[] data, PrivateKey privateKey) throws Exception { // 1. 获取Signature实例,指定算法为 SHA256withECDSA Signature signature = Signature.getInstance("SHA256withECDSA"); // 2. 用私钥初始化,进入签名模式 signature.initSign(privateKey); // 3. 传入要签名的数据 signature.update(data); // 4. 生成签名 return signature.sign(); } public static boolean verifySignature(byte[] data, byte[] signatureBytes, PublicKey publicKey) throws Exception { // 1. 获取Signature实例(算法必须与签名时一致) Signature signature = Signature.getInstance("SHA256withECDSA"); // 2. 用公钥初始化,进入验证模式 signature.initVerify(publicKey); // 3. 传入原始数据 signature.update(data); // 4. 验证签名 return signature.verify(signatureBytes); } public static void main(String[] args) throws Exception { String originalMessage = "这是一条需要确保完整性和来源的重要合同消息。"; byte[] data = originalMessage.getBytes("UTF-8"); // 假设我们已经有了密钥对 KeyPair keyPair = ECCKeyGenerator.generateKeyPair("secp256r1"); // 签名 byte[] digitalSignature = signData(data, keyPair.getPrivate()); System.out.println("签名(Base64): " + Base64.getEncoder().encodeToString(digitalSignature)); // 验证 (正常情况下) boolean isVerified = verifySignature(data, digitalSignature, keyPair.getPublic()); System.out.println("签名验证结果: " + isVerified); // 模拟篡改后验证 String tamperedMessage = "这是一条被篡改过的假消息。"; boolean isTamperedVerified = verifySignature(tamperedMessage.getBytes("UTF-8"), digitalSignature, keyPair.getPublic()); System.out.println("篡改后验证结果: " + isTamperedVerified); // 应为 false } }注意事项与深度解析:
- 算法字符串:
"SHA256withECDSA"是常见的组合。它意味着先使用SHA-256算法计算消息的哈希值,再对这个哈希值进行ECDSA签名。你也可以根据安全需求使用SHA384withECDSA或SHA512withECDSA。 - 签名结果的非确定性:标准的ECDSA签名过程中包含一个随机数
k。这意味着对同一份数据,用同一个私钥签名两次,得到的签名结果在极大概率上是不同的。这是正常现象,不影响验证。但这也要求验证方必须持有原始的签名结果,不能对其进行任何修改或比较字节是否相等。 - 签名长度:一个secp256r1的ECDSA签名,其DER编码后的长度大约是70-72字节,远小于同等强度的RSA签名(256字节以上)。
3.3 密钥交换 (ECDH)
ECDH(Elliptic Curve Diffie-Hellman)用于在不安全的信道上,让双方协商出一个只有他们俩知道的共享秘密,后续可用于派生对称加密的密钥。这是TLS握手过程中的核心步骤之一。
import javax.crypto.KeyAgreement; import java.security.*; import java.security.spec.ECGenParameterSpec; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.util.Arrays; public class ECDHKeyExchangeDemo { public static void main(String[] args) throws Exception { // 双方约定使用同一条曲线 String curveName = "secp256r1"; // 1. Alice方生成密钥对 KeyPairGenerator aliceKpg = KeyPairGenerator.getInstance("EC"); aliceKpg.initialize(new ECGenParameterSpec(curveName)); KeyPair aliceKeyPair = aliceKpg.generateKeyPair(); // 2. Bob方生成密钥对 KeyPairGenerator bobKpg = KeyPairGenerator.getInstance("EC"); bobKpg.initialize(new ECGenParameterSpec(curveName)); KeyPair bobKeyPair = bobKpg.generateKeyPair(); // 3. Alice用自己私钥和Bob的公钥生成共享秘密 KeyAgreement aliceKeyAgree = KeyAgreement.getInstance("ECDH"); aliceKeyAgree.init(aliceKeyPair.getPrivate()); aliceKeyAgree.doPhase(bobKeyPair.getPublic(), true); byte[] aliceSharedSecret = aliceKeyAgree.generateSecret(); // 这是一个原始的字节数组 // 4. Bob用自己私钥和Alice的公钥生成共享秘密 KeyAgreement bobKeyAgree = KeyAgreement.getInstance("ECDH"); bobKeyAgree.init(bobKeyPair.getPrivate()); bobKeyAgree.doPhase(aliceKeyPair.getPublic(), true); byte[] bobSharedSecret = bobKeyAgree.generateSecret(); // 5. 双方生成的共享秘密应该完全相同 System.out.println("共享秘密是否一致: " + Arrays.equals(aliceSharedSecret, bobSharedSecret)); System.out.println("共享秘密长度 (字节): " + aliceSharedSecret.length); // 6. (重要) 将原始共享秘密转换为安全的加密密钥 // 直接使用原始共享秘密是不安全的,需要经过密钥派生函数(KDF)处理 // 这里简单演示使用SHA-256哈希一次作为AES密钥(实际项目请使用HKDF等标准KDF) MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); byte[] derivedAesKey = sha256.digest(aliceSharedSecret); SecretKey aesKey = new SecretKeySpec(derivedAesKey, 0, 32, "AES"); // 取前256位作为AES-256密钥 System.out.println("派生出的AES密钥长度: " + aesKey.getEncoded().length * 8 + "位"); } }核心要点与安全警告:
- 共享秘密不是最终密钥:
generateSecret()返回的是椭圆曲线上的一个坐标点经过编码后的原始字节。绝对不能直接将其用作加密密钥。因为它可能不具备良好的随机性,并且长度可能不匹配加密算法要求。 - 必须使用KDF:必须使用密钥派生函数(KDF),如HKDF(RFC 5869),从共享秘密中派生出 cryptographically strong 的密钥。上面的示例中使用简单的SHA-256哈希仅用于演示,在生产环境中是严重的安全缺陷。你需要使用
javax.crypto的KeyGenerator或更安全的库(如Bouncy Castle)来执行标准的KDF。 - 前向保密:ECDH本身提供了前向保密(FS)。即使一方的长期私钥在未来泄露,过去通过ECDH协商出的会话密钥也不会被破解。这是现代安全通信协议(如TLS 1.3)的标配。
4. ECC在真实世界中的应用场景剖析
理解了基础操作,我们来看看ECC在哪些具体场景中大放异彩。这能帮助你将技术点与实际需求联系起来。
4.1 TLS/SSL证书与HTTPS
这是ECC应用最广泛的领域。相较于RSA证书,ECC证书体积小、握手速度快。在移动网络和物联网设备上,优势尤其明显。
- 服务端配置:现代Web服务器(如Nginx, Apache)都可以轻松配置ECC证书。你从证书颁发机构(CA)获取的通常是一个包含ECC公钥的X.509证书和一个对应的私钥文件。
- Java客户端:在Java中,使用
SSLContext初始化HttpsURLConnection或HTTP客户端(如OkHttp、Apache HttpClient)时,密钥库(Keystore)和信任库(Truststore)可以包含ECC密钥和证书,过程与RSA无异。JDK自带的keytool命令也支持生成ECC密钥对和证书请求。 - 性能收益:TLS握手阶段,使用ECC套件能显著减少CPU消耗和网络往返时间(RTT),提升用户体验和服务器并发能力。
4.2 区块链与数字货币
这是ECC的“成名作”。
- 比特币/以太坊地址:你的加密货币地址,本质上是由私钥(一个随机大整数)通过ECC(secp256k1曲线)推导出的公钥,再经过哈希和编码得到的。
java.security包原生不支持secp256k1,但你可以通过引入Bouncy Castle提供商来获得支持。 - 交易签名:每一笔比特币或以太坊交易,都需要用发送方的私钥进行ECDSA签名,网络中的节点用对应的公钥(或地址)来验证。这确保了只有资产所有者才能动用资金。
- 智能合约验证:某些链下操作或预言机报告,也需要通过ECC签名来证明其权威性。
4.3 物联网设备安全
物联网设备通常计算能力弱、存储空间小、功耗受限。
- 设备身份认证:在设备出厂时,为其烧录一个唯一的ECC私钥(存储在安全元件中)。设备与云端通信时,用该私钥进行签名,云端用预置的公钥验证,实现强身份认证。
- 安全固件升级:固件包发布前用厂商私钥签名,设备升级时用对应的ECC公钥验证签名,防止刷入恶意固件。
- 轻量级安全通信:设备与网关之间可以使用基于ECC的轻量级TLS(如DTLS)或者自定义的安全协议,在保障安全的同时最大限度节省资源。
4.4 代码/文档签名与软件供应链安全
类似于TLS证书,但对象是软件制品。
- JAR包签名:
jarsigner工具可以使用ECC密钥对JAR文件进行签名,确保代码来源可信且未被篡改。 - 容器镜像签名:Docker Notary、Cosign等工具支持使用ECC密钥对容器镜像进行签名,保障软件供应链从构建到部署的安全。
- API请求签名:在微服务架构中,服务间API调用可以使用ECC签名来验证请求方的身份和请求体的完整性,这是一种比简单API密钥更安全的方案。
5. 进阶话题与常见“坑”点实录
在实际集成ECC时,你会遇到一些标准教程里不会提的问题。这里分享几个我踩过的坑和解决方案。
5.1 曲线兼容性与“InvalidKeyException”
问题:你从外部系统(如一个用OpenSSL生成的PEM文件)导入一个ECC公钥,或者在Android与Java服务端交换密钥时,可能会遇到InvalidKeyException: Invalid key format或InvalidKeyException: EC parameters error。
排查与解决:
- 确认曲线名称:首先确保双方使用同一条命名的曲线(如
secp256r1)。不同系统对同一条曲线的称呼可能不同(例如,prime256v1就是secp256r1)。 - 检查密钥格式:Java默认期望公钥是X.509编码的,私钥是PKCS#8编码的。如果你从OpenSSL得到的PEM文件,可能需要先将其从PEM格式(
-----BEGIN PUBLIC KEY-----)解码为DER字节码,再用KeyFactory生成PublicKey对象。 - 使用Bouncy Castle作为Provider:当遇到不支持的曲线或格式时,注册Bouncy Castle提供商往往能解决问题。
import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.security.Security; public class FixKeyIssue { static { Security.addProvider(new BouncyCastleProvider()); } // 然后再尝试加载密钥 }
5.2 签名验证失败:编码与格式的“幽灵”
问题:你成功生成了签名,并在本地验证通过。但当你把签名和数据发送给另一个用不同语言(如Python、Go)写的服务验证时,却失败了。
根源分析:这几乎总是编码和格式不一致导致的。ECDSA签名本身由两个大整数(r, s)组成。在Java中,Signature.sign()返回的是这两个整数的DER编码序列。而其他平台可能期望的是简单的r||s(两个固定长度的整数拼接),或者反之。
解决方案:
- 协商统一的格式:团队内部或跨系统接口必须明确约定签名值的二进制格式。互联网上较常见的格式是“IEEE P1363格式”,即将
r和s分别编码为固定长度的字节数组(例如,对于secp256r1,各32字节),然后拼接成一个64字节的数组。 - 在Java中进行转换:你需要编写工具方法在DER编码和固定长度拼接格式之间转换。Bouncy Castle库提供了方便的类来帮助解析和构建签名。
// 示例:将Java Signature生成的DER签名转换为 64字节 R|S 格式 (secp256r1) import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.ASN1Integer; import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.DERSequence; public static byte[] convertDerSignatureToPlain(byte[] derSignature, int componentLength) throws IOException { ASN1Sequence seq = ASN1Sequence.getInstance(derSignature); ASN1Integer r = (ASN1Integer) seq.getObjectAt(0); ASN1Integer s = (ASN1Integer) seq.getObjectAt(1); byte[] rBytes = toFixedLengthBytes(r.getValue(), componentLength); byte[] sBytes = toFixedLengthBytes(s.getValue(), componentLength); byte[] plainSignature = new byte[componentLength * 2]; System.arraycopy(rBytes, 0, plainSignature, 0, componentLength); System.arraycopy(sBytes, 0, plainSignature, componentLength, componentLength); return plainSignature; } private static byte[] toFixedLengthBytes(BigInteger bigInt, int length) { byte[] bytes = bigInt.toByteArray(); if (bytes.length == length) { return bytes; } else if (bytes.length > length) { // 如果字节数组太长(可能包含符号位),取后length位 return Arrays.copyOfRange(bytes, bytes.length - length, bytes.length); } else { // 如果字节数组太短,前面补0 byte[] result = new byte[length]; System.arraycopy(bytes, 0, result, length - bytes.length, bytes.length); return result; } }
5.3 性能调优与线程安全
- KeyPairGenerator和Signature对象创建开销:
KeyPairGenerator.getInstance()和Signature.getInstance()涉及Provider查找,有一定开销。在需要频繁进行签名/验证的高并发场景(如API网关),应该将这些对象缓存起来,而不是每次操作都创建。 - 线程安全:
KeyPairGenerator和KeyFactory通常是线程安全的。但Signature对象不是线程安全的。你必须在每个线程中使用独立的Signature实例,或者进行外部同步。 - 使用硬件安全模块:对于最高安全级别的应用(如金融、CA),私钥不应存储在应用服务器的硬盘或内存中。应该使用HSM(硬件安全模块)或云KMS(密钥管理服务)来执行签名操作。Java可以通过PKCS#11接口或云服务商的SDK与HSM/KMS交互。
5.4 算法与曲线选择的安全考量
- 避免弱曲线:历史上一些椭圆曲线被发现有潜在弱点(如NIST P-256的某些实现可能存在侧信道攻击风险,或一些随机数生成器有缺陷导致私钥可被破解)。坚持使用被广泛审查和推荐的曲线:secp256r1, secp384r1, secp521r1。对于加密货币场景,secp256k1是既定标准。
- 算法标识符:在指定算法时,使用明确的字符串,如
SHA256withECDSA。避免使用较老的、可能不安全的算法,如SHA1withECDSA。 - 关注密码学进展:密码学是一个动态发展的领域。关注权威机构(如NIST)的公告,了解算法和曲线推荐的更新。例如,NIST正在推进后量子密码学标准,未来可能需要将ECC与后量子算法结合使用。
6. 完整示例:一个简单的安全消息传递模拟
最后,我们用一个相对完整的例子,把密钥生成、签名、验证和简单的密钥派生(演示用途,非生产级KDF)串起来,模拟一个端到端的场景。
import javax.crypto.Cipher; import javax.crypto.KeyAgreement; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.security.*; import java.security.spec.ECGenParameterSpec; import java.util.Base64; public class SecureMessageDemo { static class Party { String name; KeyPair keyPair; byte[] sharedSecret; Party(String name, String curve) throws Exception { this.name = name; KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); kpg.initialize(new ECGenParameterSpec(curve)); this.keyPair = kpg.generateKeyPair(); } // 执行ECDH密钥协商 void performKeyAgreement(PublicKey otherPartyPublicKey) throws Exception { KeyAgreement ka = KeyAgreement.getInstance("ECDH"); ka.init(this.keyPair.getPrivate()); ka.doPhase(otherPartyPublicKey, true); this.sharedSecret = ka.generateSecret(); // 注意:实际应用需用KDF处理 } // 使用共享秘密派生的密钥加密消息 (演示用AES-GCM) String encryptMessage(String plaintext) throws Exception { // 从sharedSecret派生一个AES密钥(简化演示,生产环境用HKDF) MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); byte[] aesKeyBytes = sha256.digest(this.sharedSecret); SecretKeySpec aesKey = new SecretKeySpec(aesKeyBytes, 0, 32, "AES"); Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); byte[] iv = new byte[12]; // GCM推荐12字节IV SecureRandom.getInstanceStrong().nextBytes(iv); GCMParameterSpec gcmSpec = new GCMParameterSpec(128, iv); // 128位认证标签 cipher.init(Cipher.ENCRYPT_MODE, aesKey, gcmSpec); byte[] ciphertext = cipher.doFinal(plaintext.getBytes("UTF-8")); byte[] encryptedData = new byte[iv.length + ciphertext.length]; System.arraycopy(iv, 0, encryptedData, 0, iv.length); System.arraycopy(ciphertext, 0, encryptedData, iv.length, ciphertext.length); return Base64.getEncoder().encodeToString(encryptedData); } // 解密消息 String decryptMessage(String base64Ciphertext) throws Exception { byte[] encryptedData = Base64.getDecoder().decode(base64Ciphertext); byte[] iv = new byte[12]; byte[] ciphertext = new byte[encryptedData.length - 12]; System.arraycopy(encryptedData, 0, iv, 0, 12); System.arraycopy(encryptedData, 12, ciphertext, 0, ciphertext.length); MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); byte[] aesKeyBytes = sha256.digest(this.sharedSecret); SecretKeySpec aesKey = new SecretKeySpec(aesKeyBytes, 0, 32, "AES"); Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); GCMParameterSpec gcmSpec = new GCMParameterSpec(128, iv); cipher.init(Cipher.DECRYPT_MODE, aesKey, gcmSpec); byte[] plaintextBytes = cipher.doFinal(ciphertext); return new String(plaintextBytes, "UTF-8"); } // 对消息签名 byte[] signMessage(String message) throws Exception { Signature sig = Signature.getInstance("SHA256withECDSA"); sig.initSign(this.keyPair.getPrivate()); sig.update(message.getBytes("UTF-8")); return sig.sign(); } // 验证对方消息的签名 boolean verifyMessage(String message, byte[] signature, PublicKey senderPublicKey) throws Exception { Signature sig = Signature.getInstance("SHA256withECDSA"); sig.initVerify(senderPublicKey); sig.update(message.getBytes("UTF-8")); return sig.verify(signature); } } public static void main(String[] args) throws Exception { String curve = "secp256r1"; // 1. 模拟通信双方:Alice和Bob Party alice = new Party("Alice", curve); Party bob = new Party("Bob", curve); System.out.println("1. 双方生成ECC密钥对完成。"); // 2. 交换公钥并协商共享秘密 alice.performKeyAgreement(bob.keyPair.getPublic()); bob.performKeyAgreement(alice.keyPair.getPublic()); System.out.println("2. ECDH密钥协商完成,双方共享秘密一致: " + MessageDigest.isEqual(alice.sharedSecret, bob.sharedSecret)); // 3. Alice准备一条消息,并签名 String messageFromAlice = "Bob,请明天上午10点转账100单位到账户X。"; byte[] aliceSignature = alice.signMessage(messageFromAlice); System.out.println("3. Alice对消息签名完成。"); // 4. Alice用共享密钥加密消息 String encryptedMessage = alice.encryptMessage(messageFromAlice); System.out.println("4. Alice加密后的消息: " + encryptedMessage.substring(0, 50) + "..."); // 5. Bob收到密文和签名,先解密 String decryptedMessage = bob.decryptMessage(encryptedMessage); System.out.println("5. Bob解密出的消息: \"" + decryptedMessage + "\""); // 6. Bob用Alice的公钥验证签名 boolean isSignatureValid = bob.verifyMessage(decryptedMessage, aliceSignature, alice.keyPair.getPublic()); System.out.println("6. Bob验证Alice的签名结果: " + (isSignatureValid ? "通过,消息可信" : "失败,消息可能被篡改或来源不可信")); // 7. 模拟中间人篡改密文 System.out.println("\n--- 模拟攻击场景:密文在传输中被篡改 ---"); byte[] tamperedCiphertext = Base64.getDecoder().decode(encryptedMessage); tamperedCiphertext[20] ^= 0x01; // 随意修改一个字节 String tamperedEncrypted = Base64.getEncoder().encodeToString(tamperedCiphertext); try { bob.decryptMessage(tamperedEncrypted); System.out.println("解密成功?这不应该发生!"); } catch (Exception e) { System.out.println("解密失败(预期中):" + e.getClass().getSimpleName() + " - " + e.getMessage()); // GCM模式会在解密时验证完整性,篡改会导致认证失败抛出异常 } } }这个示例涵盖了ECC在密钥协商、对称加密和数字签名中的综合应用。它清晰地展示了如何将非对称密码(ECC)的密钥协商和签名能力,与对称密码(AES-GCM)的高效加解密结合起来,构建一个具备保密性、完整性和身份认证的简单安全通信模型。记住,示例中的密钥派生是简化的,真实项目务必替换为标准的HKDF。通过这个从原理到实践,再到踩坑经验的全流程梳理,你应该对在Java中应用ECC算法有了扎实且可操作的理解。
