Files
SkyJourney 61017f1c39 chore: 初始化脚手架 — sunny_mochi 企业级 Flutter 模板
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
2026-05-14 12:51:05 +08:00

129 lines
4.6 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 Base64X.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';
}