import 'dart:convert'; import 'dart:typed_data'; import 'package:asn1lib/asn1lib.dart'; import 'package:pointycastle/api.dart'; import 'package:pointycastle/asymmetric/api.dart'; import 'package:pointycastle/asymmetric/pkcs1.dart'; import 'package:pointycastle/asymmetric/rsa.dart'; /// RSA 公钥加密(对应 iOS BasicModule/Util/RSAEncryption.swift)。 /// /// - 算法:RSA / PKCS#1 v1.5 padding(与 iOS `SecKeyCreateEncryptedData` + /// `.rsaEncryptionPKCS1` 完全对齐) /// - 公钥格式:DER-SPKI Base64(X.509 SubjectPublicKeyInfo) /// - 输出:标准 Base64(与服务端 `Base64.getDecoder()` 兼容) /// /// 用法: /// ```dart /// final cipher = RsaHelper.encrypt( /// 'myPassword', /// publicKeyDerBase64: Env.rsaPublicKey, /// ); /// ``` /// /// 真实公钥 = `Env.rsaPublicKey`(--dart-define 注入;当前 Env 默认值 = iOS dev 公钥)。 class RsaHelper { RsaHelper._(); /// 用公钥加密明文,返回 Base64 密文。 /// /// **R-ROB-2 / 2026-05-10**:失败必须抛 [RsaEncryptionException],**不允许返回 null** — /// 健康类 App 加密失败必须显式中断业务流,不允许调用方 fallback 到明文(合规底线)。 /// 之前的 `String?` 签名依赖调用方人工 null 检查,新调用方易漏检导致请求送 "null" 串。 static String encrypt( String plaintext, { required String publicKeyDerBase64, }) { // R-ROB-2 / 2026-05-10:拒绝空明文 — _processInBlocks 对 0 字节输入会返回空密文 // (旧行为是 cipher='' 被 isNotNull 测试掩盖的 bug);业务侧密码也不应为空 if (plaintext.isEmpty) { throw RsaEncryptionException('明文为空,拒绝加密'); } final RSAPublicKey pubKey; try { pubKey = _parseSpkiBase64(publicKeyDerBase64); } on Object catch (e, st) { throw RsaEncryptionException( 'RSA 公钥解析失败:${e.runtimeType}', cause: e, stackTrace: st, ); } try { final padding = PKCS1Encoding(RSAEngine()) ..init(true, PublicKeyParameter(pubKey)); final input = Uint8List.fromList(utf8.encode(plaintext)); final output = _processInBlocks(padding, input); return base64.encode(output); } on Object catch (e, st) { throw RsaEncryptionException( 'RSA 加密失败:${e.runtimeType}', cause: e, stackTrace: st, ); } } /// 把 SPKI Base64 公钥(X.509 SubjectPublicKeyInfo)解析为 [RSAPublicKey]。 /// /// 失败抛错(不再 swallow)— 上层 [encrypt] 统一转换为 [RsaEncryptionException]。 static RSAPublicKey _parseSpkiBase64(String spkiBase64) { final bytes = base64.decode(spkiBase64.replaceAll(RegExp(r'\s+'), '')); final asn1Parser = ASN1Parser(bytes); final topLevelSeq = asn1Parser.nextObject() as ASN1Sequence; // SPKI: SEQUENCE { algorithm SEQUENCE { OID, NULL }, subjectPublicKey BIT STRING } final publicKeyBitString = topLevelSeq.elements[1] as ASN1BitString; final publicKeyAsn = ASN1Parser(publicKeyBitString.contentBytes()); final publicKeySeq = publicKeyAsn.nextObject() as ASN1Sequence; final modulus = publicKeySeq.elements[0] as ASN1Integer; final exponent = publicKeySeq.elements[1] as ASN1Integer; return RSAPublicKey( modulus.valueAsBigInteger, exponent.valueAsBigInteger, ); } static Uint8List _processInBlocks( AsymmetricBlockCipher engine, Uint8List input, ) { final numBlocks = (input.length / engine.inputBlockSize).ceil(); final output = Uint8List(numBlocks * engine.outputBlockSize); var inputOffset = 0; var outputOffset = 0; while (inputOffset < input.length) { final size = (inputOffset + engine.inputBlockSize <= input.length) ? engine.inputBlockSize : input.length - inputOffset; final processed = engine.process( Uint8List.sublistView(input, inputOffset, inputOffset + size), ); output.setRange( outputOffset, outputOffset + processed.length, processed, ); inputOffset += size; outputOffset += processed.length; } return Uint8List.sublistView(output, 0, outputOffset); } } /// RSA 加密失败异常 — 调用方应 catch 并转为业务层 [Failure](如 AuthFailure(cryptoFailed)), /// **绝不允许** 把明文继续上送服务端。 class RsaEncryptionException implements Exception { RsaEncryptionException(this.message, {this.cause, this.stackTrace}); final String message; final Object? cause; final StackTrace? stackTrace; @override String toString() => 'RsaEncryptionException: $message'; }