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
This commit is contained in:
SkyJourney
2026-05-14 12:51:05 +08:00
commit 61017f1c39
204 changed files with 15386 additions and 0 deletions
+128
View File
@@ -0,0 +1,128 @@
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';
}