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());
}
}