Dart中PointyCastle实现ECC密钥Base64转换及ECDSA签名验签
ECC密钥对Base64导出(匹配JS侧格式)
你现有密钥生成逻辑没问题,JS侧输出的密钥是原始字节直接Base64编码的结果,没有做ASN.1包装:
- 公钥:65字节非压缩格式椭圆曲线点,固定以
0x04开头,对应PointyCastle中ECPublicKey.Q.getEncoded(false)的返回值 - 私钥:32字节大端序存储的私钥数值d,固定长度,不足32字节时高位补0
导出代码直接加在你生成密钥对之后即可:
import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; import 'package:pointycastle/export.dart'; // 密钥对导出为JS兼容的Base64格式 Map<String, String> exportKeyPairToBase64(AsymmetricKeyPair<ECPublicKey, ECPrivateKey> pair) { // 导出公钥 final pubBytes = pair.publicKey.Q!.getEncoded(false); final pubKey = base64.encode(pubBytes); // 导出私钥:BigInt转32字节大端字节数组 final d = pair.privateKey.d!; final priBytes = _bigIntToFixedLengthBytes(d, 32); final priKey = base64.encode(priBytes); return {'priKey': priKey, 'pubKey': pubKey}; } // 辅助函数:BigInt转固定长度大端字节数组 Uint8List _bigIntToFixedLengthBytes(BigInt value, int length) { final rawBytes = value.toUnsigned(8 * length).toRadixString(16); final paddedHex = rawBytes.padLeft(length * 2, '0'); return Uint8List.fromList(List.generate(length, (i) { final byteHex = paddedHex.substring(i*2, i*2+2); return int.parse(byteHex, radix: 16); })); } // 辅助函数:字节数组转BigInt(导入密钥用) BigInt _bytesToBigInt(Uint8List bytes) { BigInt result = BigInt.zero; for (final byte in bytes) { result = (result << 8) | BigInt.from(byte); } return result; }
签名验签问题解答
明文字符串转签名所需字节
直接使用UTF-8编码转换即可,你用的
SHA-256/ECDSA签名器内部会自动完成SHA-256哈希计算,不需要手动提前哈希明文:final Uint8List msgBytes = Uint8List.fromList(utf8.encode(txt));Base64格式私钥导入问题
绝对不能给
BigInt.parse传radix:64,radix参数仅支持数值进制(2/8/10/16),Base64是字节编码格式不是数值进制。正确导入步骤:- 调用
base64.decode(privateKeyStr)得到32字节私钥原始字节 - 用上面提供的
_bytesToBigInt工具函数把字节转成BigInt - 传入
ECPrivateKey构造函数,同时传入对应曲线的域参数
示例代码:
ECPrivateKey importPrivateKeyFromBase64(String b64Key, ECDomainParameters domain) { final keyBytes = base64.decode(b64Key); final d = _bytesToBigInt(keyBytes); return ECPrivateKey(d, domain); } ECPublicKey importPublicKeyFromBase64(String b64Key, ECDomainParameters domain) { final keyBytes = base64.decode(b64Key); final q = domain.curve.decodePoint(keyBytes); return ECPublicKey(q, domain); }- 调用
签名随机数生成器的作用
ECDSA算法规范要求每次签名必须使用一个全局唯一、不可预测的保密随机数k,这是算法本身的安全要求,不是库额外加的逻辑——历史上索尼PS3、比特币钱包都出现过因为随机数k重复/可预测导致私钥直接泄露的安全事故。
你之前写的_setRadom函数硬编码了固定的key和iv,属于严重安全漏洞,生成的随机数完全可预测,直接复用你密钥生成阶段用的FortunaRandom即可,不要写死固定种子值。
完整签名验签实现
// 全局复用域参数和签名器,避免重复初始化 final ECDomainParameters _secp256r1 = ECCurve_secp256r1(); final Signer _signer = Signer('SHA-256/ECDSA'); // 生成安全随机数生成器,禁止硬编码种子 SecureRandom _getSecureRandom() { final random = FortunaRandom(); final seed = KeyParameter(Uint8List.fromList( List.generate(32, (_) => Random.secure().nextInt(256)) )); random.seed(seed); return random; } // 签名:传入Base64私钥、明文字符串,返回Base64格式签名(r+s拼接64字节,和主流JS库默认格式对齐) String sign(String b64PrivateKey, String txt) { final privKey = importPrivateKeyFromBase64(b64PrivateKey, _secp256r1); final msgBytes = Uint8List.fromList(utf8.encode(txt)); final random = _getSecureRandom(); _signer.init( true, ParametersWithRandom(PrivateKeyParameter(privKey), random) ); final ECSignature sig = _signer.generateSignature(msgBytes) as ECSignature; final rBytes = _bigIntToFixedLengthBytes(sig.r, 32); final sBytes = _bigIntToFixedLengthBytes(sig.s, 32); return base64.encode([...rBytes, ...sBytes]); } // 验签:传入明文字符串、Base64签名、Base64公钥 bool verify(String txt, String b64Signature, String b64PublicKey) { final pubKey = importPublicKeyFromBase64(b64PublicKey, _secp256r1); final msgBytes = Uint8List.fromList(utf8.encode(txt)); // 解析r|s格式签名 final sigBytes = base64.decode(b64Signature); final r = _bytesToBigInt(sigBytes.sublist(0, 32)); final s = _bytesToBigInt(sigBytes.sublist(32, 64)); final sig = ECSignature(r, s); _signer.init(false, PublicKeyParameter(pubKey)); return _signer.verifySignature(msgBytes, sig); }
注意:如果你的JS侧签名是DER/ASN.1编码格式,只需要调整签名的编解码逻辑即可,核心签名验签流程不需要改动。不建议在Flutter中尝试直接运行JS bundle实现加密逻辑,互操作层的性能和稳定性都远低于原生Dart实现。
内容的提问来源于stack exchange,提问作者John
相关产品推荐
相关产品推荐

