Template
Core 基础设施:Flavor 三包体系、Drift+SQLCipher 加密数据库 7 拦截器 Dio 网络栈、CrashReporter 崩溃日志、Sealed Failure 错误体系 5 色板主题系统、Auth 认证骨架(Clean Architecture)、Dev Panel、Mock Adapter 通过验证:flutter analyze(0 errors)、flutter test(全绿)、staging APK 74.8MB
129 lines
4.6 KiB
Dart
129 lines
4.6 KiB
Dart
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<RSAPublicKey>(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';
|
||
}
|