这篇分享,记录我如何从“Base64 报错/平台不支持/解密失败”一路定位到“填充算法不一致”的根因,并给出两条稳定落地方案。同时整理了调试手册、代码片段和上线前自检清单,方便你复用。


背景

  • Unity 端用公钥加密一段紧凑 JSON(i/m/e/g),得到 Base64 token。
  • Android 端持有私钥,解密 token,解析 JSON,校验 iss/mode/exp 等。
  • 目标:让两端在不同运行时/实现下,稳定互通。

现象与主要错误

按出现顺序,踩过这些坑:

  1. Base64 无效
    The input is not a valid Base-64 string…
    把整段 PEM(含 BEGIN/END)直接喂给 Convert.FromBase64String 导致。

  2. 平台不支持导入公钥
    PlatformNotSupportedException: ImportSubjectPublicKeyInfo 不被 Unity 当前运行时支持。

  3. 填充模式不被支持
    CryptographicException: Specified padding mode is not valid for this algorithm.
    Unity 的 RSACryptoServiceProvider 不支持 OAEP‑SHA256。

  4. Android 解密 BadPadding/校验失败
    两端 OAEP 参数不一致:Unity 用的是 OAEP‑SHA1,而 Android 端在用 OAEPWithSHA‑256(还混合了 MGF1=SHA‑1)。此外,URL/ADB 传参有时会把 Base64 的 + 变成空格,导致密文损坏。

  5. 字段名和时间单位差异
    Unity 用 i/m/e/g,Android 用 iss/mode/exp;好在做了映射。时间戳单位是毫秒,两端一致。


根因总结

  • 核心:加密填充与参数不一致(OAEP 的消息哈希、MGF1 哈希、label 必须完全一致)。
  • 次要:PEM 清洗、平台 API 支持差异、Base64 在传输中被改形。

两条可落地的对齐方案

方案 A:统一 OAEP‑SHA1(最少改动)

  • 适用:你当前 Unity 端已使用 OAEP‑SHA1,想快速打通。
  • Android 端解密改成 SHA‑1(MGF1 也为 SHA‑1):
private fun decrypt(tokenB64: String?): String? {if (tokenB64.isNullOrBlank()) return nullval pri = getPrivateKey() ?: return nullreturn try {val cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding")cipher.init(Cipher.DECRYPT_MODE, pri) // 默认 MGF1(SHA-1), label=DEFAULTval raw = Base64.decode(tokenB64.trim().replace(" ", "+"), Base64.DEFAULT)val pt = cipher.doFinal(raw)String(pt, Charsets.UTF_8)} catch (e: Exception) {Log.w("AuthRsa", "decrypt fail(SHA1): ${e.message}")null}
}
  • Unity 保持:
var ct = rsa.Encrypt(plain, RSAEncryptionPadding.OaepSHA1);
  • 明文长度(2048 位):最大约 214 字节(k − 2×20 − 2)。

优点:改动最少,立即可用。
缺点:SHA‑1 已过时,长期建议迁到 SHA‑256。


方案 B:统一 OAEP‑SHA256(更优)

  • Android 保持 SHA‑256,并明确 MGF1=SHA‑256:
val cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")
val spec = OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT
)
cipher.init(Cipher.DECRYPT_MODE, pri, spec)
  • Unity 端两种实现路径:

    1. 使用 BouncyCastle(跨平台通用)
      // 引入 Portable.BouncyCastle.dll 到 Assets/Plugins
      using Org.BouncyCastle.Crypto;
      using Org.BouncyCastle.Crypto.Encodings;
      using Org.BouncyCastle.Crypto.Engines;
      using Org.BouncyCastle.Crypto.Digests;
      using Org.BouncyCastle.Security;string CleanPem(string pem) => Regex.Replace(pem, "-----BEGIN PUBLIC KEY-----|-----END PUBLIC KEY-----|\\s", "");
      string EncryptOaepSha256WithBC(string pemPublicKey, byte[] plain) {var der = Convert.FromBase64String(CleanPem(pemPublicKey));   // SPKIvar pub = PublicKeyFactory.CreateKey(der);                    // RsaKeyParametersvar engine = new OaepEncoding(new RsaEngine(), new Sha256Digest(), new Sha256Digest(), null);engine.Init(true, pub);var ct = engine.ProcessBlock(plain, 0, plain.Length);return Convert.ToBase64String(ct);
      }
      
    2. 下放到 Android Java 插件(如果只跑 Android)
      public static String encryptBase64(String spkiB64, String utf8) throws Exception {byte[] keyBytes = android.util.Base64.decode(spkiB64, Base64.DEFAULT);PublicKey pub = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(keyBytes));Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");cipher.init(Cipher.ENCRYPT_MODE, pub);return Base64.encodeToString(cipher.doFinal(utf8.getBytes(StandardCharsets.UTF_8)), Base64.NO_WRAP);
      }
      
  • 明文长度(2048 位):最大约 190 字节(k − 2×32 − 2)。

优点:更现代更安全。
注意:Unity 默认的 RSACryptoServiceProvider 不支持 OaepSHA256,所以需要 BC 或插件。


关键代码片段

  • Unity:SPKI(“BEGIN PUBLIC KEY”) → RSAParameters 解析(如需内置实现)
static byte[] ReadSpkiFromPem(string pem) => Convert.FromBase64String(pem.Replace("-----BEGIN PUBLIC KEY-----","").Replace("-----END PUBLIC KEY-----","").Replace("\r","").Replace("\n","").Trim());static int ReadLen(BinaryReader br) {int b = br.ReadByte();if ((b & 0x80) == 0) return b;int n = b & 0x7F, len = 0; for (int i=0;i<n;i++) len = (len<<8)|br.ReadByte();return len;
}static RSAParameters SpkiToRsaParams(byte[] spki) {using var ms = new MemoryStream(spki);using var br = new BinaryReader(ms);br.ReadByte(); ReadLen(br); // SEQbr.ReadByte(); int algLen = ReadLen(br); br.ReadBytes(algLen); // AlgIdbr.ReadByte(); ReadLen(br); br.ReadByte(); // BIT STRING + unusedif (br.ReadByte()!=0x30) throw new FormatException("Bad RSAPublicKey");ReadLen(br);if (br.ReadByte()!=0x02) throw new FormatException("Bad modulus");int modLen = ReadLen(br); var mod = br.ReadBytes(modLen);if (mod.Length>0 && mod[0]==0x00) { var t=new byte[mod.Length-1]; Buffer.BlockCopy(mod,1,t,0,t.Length); mod=t; }if (br.ReadByte()!=0x02) throw new FormatException("Bad exponent");int expLen = ReadLen(br); var exp = br.ReadBytes(expLen);return new RSAParameters { Modulus = mod, Exponent = exp };
}
  • Android:私钥解析(支持 PKCS#1/PKCS#8),并解密(统一 OAEP)
private fun parsePrivateKey(pemOrB64: String): PrivateKey {val text = pemOrB64.trim()val kf = KeyFactory.getInstance("RSA")return when {text.contains("BEGIN RSA PRIVATE KEY") -> { // PKCS#1val clean = text.replace("-----BEGIN RSA PRIVATE KEY-----","").replace("-----END RSA PRIVATE KEY-----","").replace("\\s".toRegex(), "")val pkcs1 = Base64.decode(clean, Base64.DEFAULT)val der = pkcs1ToPkcs8(pkcs1) // 组装成 PKCS#8kf.generatePrivate(PKCS8EncodedKeySpec(der))}text.contains("BEGIN PRIVATE KEY") -> { // PKCS#8val clean = text.replace("-----BEGIN PRIVATE KEY-----","").replace("-----END PRIVATE KEY-----","").replace("\\s".toRegex(), "")val der = Base64.decode(clean, Base64.DEFAULT)kf.generatePrivate(PKCS8EncodedKeySpec(der))}else -> { // 纯 Base64val raw = Base64.decode(text.replace("\\s".toRegex(), ""), Base64.DEFAULT)try { kf.generatePrivate(PKCS8EncodedKeySpec(raw)) }catch (_: Exception) { kf.generatePrivate(PKCS8EncodedKeySpec(pkcs1ToPkcs8(raw))) }}}
}
  • Verify 调试版(指出失败阶段)
fun verify(tokenB64: String?, expectedMode: String): Pair<Boolean, Payload?> {val plain = decrypt(tokenB64)if (plain == null) { Log.w(TAG, "verify: decrypt failed"); return false to null }val p = parse(plain)if (p == null) { Log.w(TAG, "verify: json parse failed. plain(64)=${plain.take(64)}"); return false to null }val now = System.currentTimeMillis()val okIss = p.iss == "launcher"val okMode = p.mode.equals(expectedMode, true)val okExp = p.exp > nowLog.d(TAG, "verify fields: iss=${p.iss}, mode=${p.mode}, exp=${p.exp}, now=$now, okIss=$okIss, okMode=$okMode, okExp=$okExp")val ok = okIss && okMode && okExpreturn ok to if (ok) p else null
}

传输与编码注意

  • 尽量用 Intent extras 传字符串;若用 URL/命令行,务必做 URL‑encode 或使用 Base64URL(-/_,去掉 =)。
  • Android 端解码前做清洗:token.trim().replace(" ", "+"),并用 Base64.DEFAULT 容忍换行。
  • ADB 传参注意引号与平台差异;实在不稳,改 Base64URL。

安全与架构建议

  • 不要在客户端长期持有私钥(易被逆向)。更推荐:服务端“签名”,客户端“验签”(JWT/JWS 思路)。
  • 若 payload 可能变大,采用“RSA 加密随机 AES 密钥 + AES‑GCM 加密正文”的混合加密。
  • 使用 Android Keystore 存私钥,限制可导出;区分测试/生产,定期轮换。
  • 时间校验建议允许 1–2 分钟偏差(时钟漂移)。

上线前自检清单

  • 两端 OAEP 参数一致(SHA‑1 或 SHA‑256;MGF1 相同;label 默认)。
  • 明文长度未超过上限(SHA‑1≈214B,SHA‑256≈190B,2048 位密钥)。
  • PEM 清洗正确(Base64 仅中间体;无隐藏字符)。
  • Base64 在传输中未被改形(+ 空格、= 丢失);必要时使用 Base64URL。
  • 字段映射正确(i/m/e/g ↔ iss/mode/exp/game),时间单位一致(毫秒)。
  • 日志能区分 Base64/解密/JSON/字段校验失败。
  • 私钥安全存储策略明确(最好不放客户端)。

常见错误对照表

现象/异常常见原因解决
Base64 invalidPEM 带 BEGIN/END/换行去头尾、去空白后再解码
PlatformNotSupportedExceptionUnity 运行时不支持 ImportSubjectPublicKeyInfo用 RSAParameters 解析 SPKI 或使用 BouncyCastle/Android 插件
Specified padding mode…RSACryptoServiceProvider 不支持 OAEP‑SHA256改用 OAEP‑SHA1 或 BouncyCastle/插件实现 SHA‑256
BadPaddingExceptionOAEP 参数不一致、密钥不配、Base64 被改形统一 OAEP 参数;修正传输;核对密钥
verify 逻辑失败字段名/时间单位不一致做字段映射;确认毫秒级时间戳

结语

这次问题的根因不在“密钥/代码对不对”,而是“Unity 与 Android 的加密参数默认值并不一致”。一旦把 OAEP 的细节(消息哈希、MGF1、label)对齐,其它问题(PEM 清洗、平台 API、Base64 传输)就都是工程实现层面的细节。

休闲一刻

祺洛管理系统介绍

祺洛是一个 Rust 企业级快速开发平台,基于(Rust、 Axum、Sea-orm、Jwt、Vue),内置模块如:部门管理、角色用户、菜单及按钮授权、数据权限、系统参数、日志管理等。在线定时任务配置;支持集群,支持多数据源,支持分布式部署。
🌐 官方网站: https://www.qiluo.vip/
让企业级应用开发更简单、更高效、更安全

🌟 如何支持项目?

如果您觉得祺洛Admin的技术方案有价值,或是能解决您在企业级开发中的实际问题,欢迎通过以下方式支持项目发展:

  1. 点亮Star — 访问我们的代码仓库,点击右上角的Star按钮,这是对开源项目最直接的认可,也能帮助更多人发现这个项目:

    • GitCode仓库:https://gitcode.com/will_csdn_go/qiluo_admin.git
    • Gitee仓库:https://gitee.com/chenlunfu/qiluo_admin.git
    • GitHub仓库:https://github.com/chelunfu/qiluo_admin.git
  2. 参与贡献 — 无论是提交Issue反馈问题,还是PR贡献代码,都是对项目成长的重要支持

  3. 分享传播 — 将项目推荐给有需要的同事或朋友,让更多人受益于这个开发框架

您的每一份支持,都是我们持续优化迭代的动力。祺洛Admin团队感谢您的关注与支持!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/95493.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/95493.shtml
英文地址,请注明出处:http://en.pswp.cn/diannao/95493.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Go语言GC机制:高效并发回收解析

Go 语言的垃圾回收&#xff08;Garbage Collection&#xff0c;简称 GC&#xff09;是其自动内存管理的核心机制&#xff0c;旨在自动识别并回收不再被使用的内存&#xff0c;避免内存泄漏&#xff0c;减轻开发者的手动内存管理负担。Go 的 GC 算法经历了多次迭代优化&#xff…

imx6ull-驱动开发篇23——Linux 内核定时器实验

目录 实验程序编写 修改设备树文件 定时器驱动程序 timer.c 测试 timerApp.c Makefile 文件 运行测试 实验程序编写 本讲实验&#xff0c;我们使用正点原子I.MX6U-ALPHA 开发板&#xff0c;通过linux内核定时器周期性的点亮和熄灭开发板上的 LED 灯&#xff0c; LED 灯…

IPTV系统:开启视听与管理的全新篇章

在当今数字化飞速发展的时代&#xff0c;IPTV系统正以前所未有的姿态&#xff0c;重塑着我们的视听体验与管理模式。它不仅仅是一套技术系统&#xff0c;更是连接信息、沟通情感、提升效率的桥梁&#xff0c;为各个领域带来了全新的变革与发展机遇。从电视直播的角度来看&#…

PyTorch笔记9----------Cifar10图像分类

1.图像分类网络模型框架解读 分类网络的基本结构 数据加载模块&#xff1a;对训练数据加载数据重组&#xff1a;组合成网络需要的形式&#xff0c;例如预处理、增强、各种网络处理、loss函数计算优化器 数据加载模块 使用公开数据集&#xff1a;torchvision.datasets使用自定义…

飞凌OK3568开发板QT应用程序编译流程

飞凌OK3568开发板QT应用程序编译流程开发环境&#xff1a;ubuntu20.04&#xff08;主机&#xff09;、飞凌OK3568开发板一般在linux系统下开发用于ARM开发板的QT应用程序时&#xff0c;直接在主机上开发然后进行交叉编译即可&#xff0c;但有时候ARM开发板的厂家提供的SDK中可能…

飞算JavaAI合并项目实战:7天完成3年遗留系统重构

引言 企业数字化进程中&#xff0c;遗留系统改造始终是CIO面临的头号难题。某电商平台的实践数据显示&#xff1a;3年以上的Java项目平均存在47%的冗余代码&#xff0c;63%的架构设计不符合当前业务需求&#xff0c;进行系统性重构需要投入相当于原开发量200%的资源。传统&quo…

卫星速度增量和比冲及推力之间的关系

一、定义1.1.比冲&#xff08;Isp&#xff09;&#xff1a;比冲是衡量发动机性能的重要指标&#xff0c;反映了单位重量推进剂在发动机中产生的冲量&#xff0c;单位为米/秒&#xff08;m/s&#xff09;&#xff0c;代表燃料燃烧时喷流速度。这个单位与速度单位“米/秒”相同&a…

MATLAB绘制各种心形曲线

1.方程(1)心形线的经典隐函数方程为&#xff1a;(2)参数方程&#xff08;更平滑的心形&#xff09;&#xff1a;(3)极坐标心形线(4)参数方程&#xff08;3D心形&#xff09;(5)隐函数3D心形2. MATLAB代码clc;close all;clear all;warning off;%清除变量 rand(seed, 100); randn…

Django REST Framework视图

Django REST Framework (DRF) 视图类详解DRF 提供了丰富的视图类来构建 API&#xff0c;从基础到高级&#xff0c;满足不同复杂度的需求。以下是 DRF 的主要视图类及其使用场景&#xff1a;1. 基础视图类APIView所有 DRF 视图的基类&#xff0c;相当于 Django 的 View 类的增强…

Linux面试题及详细答案 120道(1-15)-- 基础概念

《前后端面试题》专栏集合了前后端各个知识模块的面试题&#xff0c;包括html&#xff0c;javascript&#xff0c;css&#xff0c;vue&#xff0c;react&#xff0c;java&#xff0c;Openlayers&#xff0c;leaflet&#xff0c;cesium&#xff0c;mapboxGL&#xff0c;threejs&…

week1-[分支结构]中位数

week1-[分支结构]中位数 题目描述 给定 444 个正整数 a,b,c,da,b,c,da,b,c,d&#xff0c;输出它们的中位数&#xff0c;答案四舍五入保留 111 位小数。 输入格式 输入共 111 行 444 个正整数 a,b,c,da,b,c,da,b,c,d。 输出格式 输出共 111 行 111 个浮点数表示答案。 样例 #1 样…

[激光原理与应用-259]:理论 - 几何光学 - 平面镜的反射、平面透镜的折射、平面镜的反射成像、平面透镜的成像的规律

一、平面镜的反射规律平面镜的反射遵循镜面反射定律&#xff0c;即光线在光滑表面&#xff08;反射面平整度远大于波长&#xff09;发生反射时&#xff0c;满足以下条件&#xff1a;反射光线、入射光线与法线共面&#xff1a;反射光线、入射光线和法线&#xff08;垂直于反射面…

相机按键功能解析

相机按键功能解析佳能相机按键机身背面机身正面机身顶部机身侧面 佳能相机按键 机身背面取景器目镜&#xff1a;用于拍摄时观察相机形成的图像。实拍显示/视频拍摄按钮&#xff1a;按下即可开始拍摄或录制视频。光圈/曝光补偿键&#xff1a;调整光圈大小和曝光补偿&#xff0c;…

51单片机-驱动LED模块教程

本章思维导图&#xff1a; 51单片机驱动LED灯模块 LED灯元器件简介 LED&#xff08;Light Emitting Diode&#xff0c;发光二极管&#xff09; 是一种固态半导体器件&#xff0c;通过P-N结中电子与空穴复合直接将电能转化为光能。其核心结构由P型半导体&#xff08;空穴主导&a…

Git 完全手册:从入门到团队协作实战(2)

Hello大家好&#xff01;很高兴我们又见面啦&#xff01;给生活添点passion&#xff0c;开始今天的编程之路&#xff01; 我的博客&#xff1a;<但凡. 我的专栏&#xff1a;《编程之路》、《数据结构与算法之美》、《C修炼之路》、《Linux修炼&#xff1a;终端之内 洞悉真理…

c语言中堆和栈的区别

1.栈区(stack):由编译器自动分配释放&#xff0c;栈主要用于存储局部变量、函数参数、函数调用和返回信息等。其操作方式类似于数据结构中的栈。 2.堆区(heap):一般由程序员分配释放&#xff0c;若程序员不释放&#xff0c;则可能会引起内存泄漏。注堆和数据结构中的堆栈不一样…

华为实验WLAN 基础配置随练

业务vlan 20 192.168.20.x管理vlan 100 192.168.100.x步骤① 网络互通Core sw:vlan batch 20 100 dhcp enable int vlanif 20IP add 192.168.20.1 24dhcp select interfaceinterface GigabitEthernet0/0/1/2port link-type trunkport trunk pvid vlan 100port trunk allow-pas…

CMake 如何查找 Python2和Python3

问题 在一个CMakeLists.txt文件里面看到了下面的这句话 find_package(Python2 COMPONENTS Interpreter Development NumPy)这个好有趣啊&#xff0c;Python2也是一个C的库吗&#xff0c;也有Python2Config.cmake或者FindPython2.cmake? 回答 find_package(Python2 COMPONENTS …

心灵笔记:刻意练习

心灵笔记&#xff1a;刻意练习提要 所有人都以为“杰出”源于“天赋”&#xff0c;而“天才”却说&#xff1a;我的成就源于“正确的练习”&#xff01; 定义&#xff1a;刻意练习是一种有目的、有方法、能带来能力持续提升的结构化训练方式&#xff0c;它并非简单的重复劳动&a…

langchain入门笔记03:使用fastapi部署本地大模型后端接口,优化局域网内的问答响应速度

文章目录前言一、fastapi的简单入门1&#xff1a;安装必要的包&#xff08;python3.11&#xff09;&#xff1a;2&#xff1a;快速搭建一个fastapi&#xff1a;二、提升问答的响应速度1. fastapi部署后端接口&#xff0c;在局域网内访问的方法2. 局域网内的测试&#xff1a;“未…