You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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是字节编码格式不是数值进制。正确导入步骤:

    1. 调用base64.decode(privateKeyStr)得到32字节私钥原始字节
    2. 用上面提供的_bytesToBigInt工具函数把字节转成BigInt
    3. 传入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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 03:09:38