using System.Buffers.Binary; using System.Security.Cryptography; using System.Text; namespace Rmsys.Infrastructure; /// /// 数据包加密封装(方案 A:机构固定密钥)。 /// /// 文件格式(.pkg,大端): /// 0 8 magic "RMSPKG01" /// 8 1 version 0x01 /// 9 1 flags bit0=1 表示导出时设了口令(方案 B 预留,方案 A 恒为 0) /// 10 2 reserved 0 /// 12 16 salt 每次导出随机,用于派生两把子密钥 /// 28 16 iv SM4-CBC 初始向量 /// 44 8 plainSize 明文(SQLite)字节数 /// 52 32 hmac HMAC-SM3( bytes[0..52) || ciphertext ) /// 84 N ciphertext SM4-CBC-PKCS7( SQLite 文件 ) /// /// 为什么 CBC 之外还要 HMAC: /// SM4-CBC 只保机密性,不保完整性。错误密钥会解出乱码、SQLite 打不开,用户只看到 /// 「文件损坏」。加 HMAC-SM3 后一次解决三件事:防篡改(改一个字节就拒绝)、 /// 密钥校验(对不上就是密钥错)、且不泄漏任何明文结构。 /// /// 密钥派生(域分离,避免同一把密钥同时当加密与认证用): /// sm4Key = HMAC-SM3(master, salt || 0x01)[0..16] /// macKey = HMAC-SM3(master, salt || 0x02) /// public static class PackageCrypto { public static readonly byte[] Magic = Encoding.ASCII.GetBytes("RMSPKG01"); public const byte Version = 0x01; public const int HeaderSize = 84; public const int SaltSize = 16; public const int IvSize = 16; public const int HmacSize = 32; public const int MasterKeySize = 32; /// 生成新的主密钥(base64,44 字符),写入 sysconfigs 用 public static string NewMasterKeyBase64() => Convert.ToBase64String(RandomNumberGenerator.GetBytes(MasterKeySize)); /// /// 内置数据包主密钥(base64,32 字节)。 /// /// 为什么必须固定这一把: /// PAD 端 APK 把密钥编译进 BuildConfig.PKG_MASTER_KEY(来自 gradle.properties 的 RMSYS_PKG_KEY), /// 装到客户机上无法再改。若服务端首次初始化时「随机生成一把写库」, /// 就会形成「库里的密钥 ≠ APK 内置密钥」,PAD 打开数据包一律报 /// 「校验失败:密钥不匹配或数据包已被篡改」。 /// /// 该常量必须与 android/gradle.properties 的 RMSYS_PKG_KEY 完全一致; /// 打包前由 tools/check_pkg_key.py 校验,改一处不改另一处会被当场拦下。 /// public const string BuiltinMasterKeyBase64 = "Or3oyXe+so9kllBFmOGaphRTzi7KYuNWM661OPVHT3o="; public static bool IsEncryptedPackage(byte[] head) { if (head.Length < Magic.Length) return false; for (var i = 0; i < Magic.Length; i++) if (head[i] != Magic[i]) return false; return true; } /// 不解密、只看头部信息(用于提示与排障) public static (bool ok, byte version, byte flags, long plainSize) ReadHeader(byte[] pkg) { if (pkg.Length < HeaderSize || !IsEncryptedPackage(pkg)) return (false, 0, 0, 0); var ver = pkg[8]; var flags = pkg[9]; var size = BinaryPrimitives.ReadInt64BigEndian(pkg.AsSpan(44, 8)); return (true, ver, flags, size); } /// 加密:SQLite 明文 → .pkg 字节 public static byte[] Encode(byte[] plain, byte[] masterKey, byte flags = 0) { if (masterKey.Length != MasterKeySize) throw new ArgumentException($"主密钥必须 {MasterKeySize} 字节,实际 {masterKey.Length}"); var salt = RandomNumberGenerator.GetBytes(SaltSize); var iv = RandomNumberGenerator.GetBytes(IvSize); var (sm4Key, macKey) = DeriveKeys(masterKey, salt); var cipher = Sm4.EncryptCbc(sm4Key, iv, plain); var outp = new byte[HeaderSize + cipher.Length]; Buffer.BlockCopy(Magic, 0, outp, 0, Magic.Length); outp[8] = Version; outp[9] = flags; // 10..11 reserved 保持 0 Buffer.BlockCopy(salt, 0, outp, 12, SaltSize); Buffer.BlockCopy(iv, 0, outp, 28, IvSize); BinaryPrimitives.WriteInt64BigEndian(outp.AsSpan(44, 8), plain.LongLength); Buffer.BlockCopy(cipher, 0, outp, HeaderSize, cipher.Length); // HMAC 覆盖 头部(0..52) + 密文 var macInput = new byte[52 + cipher.Length]; Buffer.BlockCopy(outp, 0, macInput, 0, 52); Buffer.BlockCopy(cipher, 0, macInput, 52, cipher.Length); var mac = Sm3.Hmac(macKey, macInput); Buffer.BlockCopy(mac, 0, outp, 52, HmacSize); return outp; } /// 解密:.pkg 字节 → SQLite 明文。密钥错或被篡改都会抛异常。 public static byte[] Decode(byte[] pkg, byte[] masterKey) { if (masterKey.Length != MasterKeySize) throw new ArgumentException("主密钥长度不正确"); if (pkg.Length < HeaderSize || !IsEncryptedPackage(pkg)) throw new CryptographicException("不是有效的数据包文件(魔数不匹配)"); var ver = pkg[8]; if (ver != Version) throw new CryptographicException($"数据包版本 {ver} 不受支持(需要 {Version})"); var plainSize = BinaryPrimitives.ReadInt64BigEndian(pkg.AsSpan(44, 8)); if (plainSize <= 0 || plainSize > 512L * 1024 * 1024) throw new CryptographicException("头部记录的长度异常,文件可能已损坏"); var salt = pkg.AsSpan(12, SaltSize).ToArray(); var iv = pkg.AsSpan(28, IvSize).ToArray(); var storedMac = pkg.AsSpan(52, HmacSize).ToArray(); var cipher = pkg.AsSpan(HeaderSize).ToArray(); var (sm4Key, macKey) = DeriveKeys(masterKey, salt); var macInput = new byte[52 + cipher.Length]; Buffer.BlockCopy(pkg, 0, macInput, 0, 52); Buffer.BlockCopy(cipher, 0, macInput, 52, cipher.Length); var mac = Sm3.Hmac(macKey, macInput); if (!CryptographicOperations.FixedTimeEquals(mac, storedMac)) throw new CryptographicException("校验失败:密钥不匹配或文件已被篡改"); var plain = Sm4.DecryptCbc(sm4Key, iv, cipher); if (plain.LongLength != plainSize) throw new CryptographicException($"长度校验失败:头部 {plainSize},实际 {plain.LongLength}"); return plain; } private static (byte[] sm4Key, byte[] macKey) DeriveKeys(byte[] masterKey, byte[] salt) { var d1 = new byte[salt.Length + 1]; Buffer.BlockCopy(salt, 0, d1, 0, salt.Length); d1[^1] = 0x01; var k1 = Sm3.Hmac(masterKey, d1); var d2 = new byte[salt.Length + 1]; Buffer.BlockCopy(salt, 0, d2, 0, salt.Length); d2[^1] = 0x02; var k2 = Sm3.Hmac(masterKey, d2); return (k1[..16], k2); } /// 封装格式自检:往返、篡改检测、错误密钥、头部解析 public static (bool ok, string report) SelfTest() { var sb = new StringBuilder(); var ok = true; var master = RandomNumberGenerator.GetBytes(MasterKeySize); var plain = new byte[300]; RandomNumberGenerator.Fill(plain); var pkg = Encode(plain, master); // 1) 往返 var back = Decode(pkg, master); var r1 = back.AsSpan().SequenceEqual(plain); ok &= r1; sb.AppendLine($" 包格式 往返 {(r1 ? "✔" : "✖")} {plain.Length}B → {pkg.Length}B(头部 {HeaderSize}B)"); // 2) 头部可读 var (hok, ver, flags, size) = ReadHeader(pkg); var r2 = hok && ver == Version && size == plain.Length; ok &= r2; sb.AppendLine($" 包格式 头部 {(r2 ? "✔" : "✖")} version={ver} flags={flags} plainSize={size}"); // 3) 篡改 1 字节必须被拒 var bad = (byte[])pkg.Clone(); bad[HeaderSize + 10] ^= 0x01; var rejected = false; try { Decode(bad, master); } catch (CryptographicException) { rejected = true; } ok &= rejected; sb.AppendLine($" 包格式 篡改检测 {(rejected ? "✔ 已拒绝" : "✖ 未拒绝")}"); // 4) 错误密钥必须被拒 var wrongKey = RandomNumberGenerator.GetBytes(MasterKeySize); var rejected2 = false; try { Decode(pkg, wrongKey); } catch (CryptographicException) { rejected2 = true; } ok &= rejected2; sb.AppendLine($" 包格式 错误密钥 {(rejected2 ? "✔ 已拒绝" : "✖ 未拒绝")}"); // 5) 随机 salt/iv:同样明文两次加密结果必须不同 var pkg2 = Encode(plain, master); var r5 = !pkg.AsSpan().SequenceEqual(pkg2); ok &= r5; sb.AppendLine($" 包格式 随机化 {(r5 ? "✔ 两次密文不同" : "✖ 相同(salt/iv 固定了)")}"); return (ok, sb.ToString()); } }