Java 第三方RSA公钥加密 异常(not a sequence)

本文探讨了一种特定情况下使用RSA公钥加密时遇到的异常问题,并提供了解决方案。通过对公钥格式进行预处理,成功实现了加密过程,避免了因公钥格式问题导致的异常。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第三方公钥文件.pem 
公钥:
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBALfuBySGu7R41NpDSfJIB91RFnX4F5o1QVplszJKUeD9DXkLfdmsR9pM
66MhfnAaV51RJ2LHDgn8PiTtkCyXYRcRZOWqo+faTD73/LLb+UDpqkrhKAmrm0bd
zhbvLhIMWgd+qcnzblnW6BUyXpweoguMGmriOFL+gbczNiPDWPE/AgMBAAE=
-----END RSA PUBLIC KEY-----

pubkeyBuf:第三方公钥转码,Base64.decode(pubkey);  

private byte[] getEncSymmKey(byte[] pubkeyBuf, byte[] randomKey) throws Exception {
        X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(pubkeyBuf);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        Key publicKey = keyFactory.generatePublic(pubKeySpec);
        
        // 对数据加密  
        Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);

        return cipher.doFinal(randomKey);
    } 

始终出现异常:java.security.InvalidKeyException: IOException: algid parse error, not a sequence
此公钥字节数和有的公钥字节数不同:此公钥为192字节,Base64转码后,byte 140字节

不出现异常公钥
-----BEGIN RSA PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQChDzcjw/rWgFwnxunbKp7/4e8w
/UmXx2jk6qEEn69t6N2R1i/LmcyDT1xr/T2AHGOiXNQ5V8W4iCaaeNawi7aJaRht
Vx1uOH/2U378fscEESEG8XDqll0GCfB1/TjKI2aitVSzXOtRs8kYgGU78f7VmDNg
XIlk3gdhnzh+uoEQywIDAQAB
-----END RSA PUBLIC KEY-----
不出异常的公钥为222字节,Base64转码后,byte 162字节就无异常

不止如何用最上面的第三方公钥进行RSA加密才不出现异常?
<think>我们正在使用.NET Framework 4.7.2,它没有内置的PEM导入功能。我们需要将PEM公钥转换为XML格式,因为RSACryptoServiceProvider只支持XML格式的密钥。 步骤: 1. 从PEM字符串中提取Base64部分。 2. 将Base64字符串解码为字节数组。 3. 解析ASN.1结构以获取模数(Modulus)和指数(Exponent)。 4. 构建XML字符串。 5. 使用FromXmlString方法加载公钥。 注意:PEM格式的RSA公钥通常是X.509 SubjectPublicKeyInfo结构(以-----BEGIN PUBLIC KEY-----开头)。 我们将编写一个辅助方法将PEM公钥转换为XML字符串。 由于.NET Framework 4.7.2没有内置的PEM解析器,我们将手动解析(注意:这仅适用于RSA公钥,且假设密钥是标准的)。 解析过程: 1. 去除PEM的页眉页脚和换行符,得到Base64字符串。 2. 将Base64字符串转换为字节数组。 3. 这个字节数组是ASN.1 DER编码。我们期望的结构是: SEQUENCE (2 elements) SEQUENCE (2 elements) // 算法标识 OBJECT IDENTIFIER // 1.2.840.113549.1.1.1 for RSA NULL BIT STRING // 包含另一个SEQUENCE(模数和指数) 4. 我们跳过前面的字节(通常是固定长度)直到模数和指数序列。具体来说: - 前几个字节是ASN.1头,我们可以按以下方式解析: 0x30 [总长度] ... 然后是一个嵌套的序列(算法标识),然后是一个位串(bit string)。 5. 在位串中,第一个字节是0(表示没有未使用的位),然后是一个序列(0x30)包含两个整数(模数和指数)。 但是,为了简化,我们可以使用固定的偏移量(因为RSA公钥的结构是标准的)。然而,这不是最健壮的方法,但对于已知的密钥(如2048位)是可行的。 另一种方法是使用System.Security.Cryptography.X509Certificates中的PublicKey类,但PEM不是证书,而是公钥。我们可以尝试创建一个X509Certificate2,但PEM公钥不是证书,所以不行。 因此,我们手动解析。注意:以下代码假设密钥是RSA,并且结构是标准的。如果密钥长度不同,偏移量可能会变。 我们采用更通用的方法:解析ASN.1结构,找到BIT STRING,然后从BIT STRING中解析出两个整数(模数和指数)。 但是,为了简单,我们假设一个典型的RSA公钥(例如2048位)的偏移量是固定的。实际上,指数通常是一个小整数(如65537),所以我们可以用固定偏移量。 然而,更安全的方法是使用一个ASN.1解析器。由于我们不想引入第三方库,我们将编写一个简单的解析器来解析这个特定的结构。 步骤: 1. 解码Base64得到字节数组。 2. 使用一个BinaryReader读取这个数组,按照ASN.1规则。 我们定义一个小型解析器: - ASN.1类型标记:第一个字节是类型(如0x30表示SEQUENCE),然后是一个长度(可能是一个或多个字节)。 - 我们期望整个结构是一个SEQUENCE,包含两个元素:一个SEQUENCE(算法)和一个BIT STRING。 然后,我们进入BIT STRING,它本身包含一个SEQUENCE(两个整数:模数和指数)。 解析过程: 1. 读取一个字节,应该是0x30(SEQUENCE)。 2. 读取长度(可能是多个字节,但这里我们假设长度小于128,所以只有一个字节)。 3. 然后读取算法标识序列(0x30,然后长度,然后OID和NULL)。 4. 然后读取位字符串(0x03,然后长度,然后一个字节(未使用位,通常为0),然后位字符串的内容(又是一个序列))。 5. 然后解析这个内容序列:0x30,长度,然后两个整数(每个整数以0x02开头)。 我们将编写一个辅助函数来解析ASN.1整数(以0x02开头)。 注意:由于我们只处理公钥,结构相对固定。 代码步骤: 但是,为了简化,我们可以使用以下偏移量(对于2048位密钥): - 跳过前24个字节(典型情况),然后读取模数(128字节)和指数(3字节,如65537)。 然而,这种方法不通用。我们将采用解析的方式。 由于时间关系,我们采用一个已经验证过的解析方法(注意:这里只支持RSA公钥,并且指数长度是3字节或更少的情况)。 参考:https://stackoverflow.com/questions/18024985/c-sharp-rsa-encrypt-with-public-key-pem/32150572 我们将编写一个方法:ConvertPemToXml 具体步骤: 1. 去除PEM的页眉页脚和空白。 2. Base64解码。 3. 使用BinaryReader读取字节数组,按照ASN.1结构。 注意:我们假设长度都是单字节(小于128),这样简化处理。 但实际长度可能大于127,此时长度字节的第一个比特为1,然后后续字节表示长度。为了简单,我们假设公钥不大,长度用单字节表示。 然而,对于2048位的密钥,模数是256字节(但实际存储时可能会去掉前导0,所以可能是257或256?)。我们需要处理。 因此,我们写一个解析DER整数的方法,它处理0x02标记。 我们将分步解析: 步骤1:解析外部序列 读取0x30,然后读取长度L1(假设为单字节,如果L1>127则使用多字节,但这里我们先按单字节处理,如果遇到0x82则后面两个字节是长度,0x81则后面一个字节是长度) 步骤2:解析内部序列(算法标识) 读取0x30,然后长度L2,然后跳过L2字节(因为算法标识是固定的,我们不需要它,但需要验证OID是否为RSA)。 步骤3:读取位字符串(0x03) 然后读取长度L3(同样可能多字节,但这里假设单字节),然后读取一个字节(未使用位数,应为0),然后读取L3-1个字节(因为有一个未使用位说明字节),这L3-1个字节实际上是一个序列(0x30开头),然后解析这个序列。 步骤4:解析序列(包含模数和指数) 读取0x30,长度L4(可能多字节,但这里假设单字节),然后读取两个整数(每个整数以0x02开头)。 步骤5:解析模数(第一个整数) 读取0x02,然后长度L5(单字节),然后读取L5字节作为模数(注意:模数的第一个字节可能是0,因为模数是一个大整数,且要求是正数,所以如果最高位为1,则可能前面加0,但实际存储时,DER编码要求整数是正数且用最小字节数,所以第一个字节可能为0,此时我们保留这个0,因为XML中需要完整的模数) 步骤6:解析指数(第二个整数) 读取0x02,长度L6,然后读取L6字节作为指数。 最后,将模数和指数转换为Base64字符串,并构建XML。 注意:XML格式: <RSAKeyValue> <Modulus>...</Modulus> <Exponent>...</Exponent> </RSAKeyValue> 但是,模数和指数都是Base64编码。 我们编写一个方法,将PEM公钥字符串转换为XML字符串。 注意:由于PEM公钥可能包含换行符,我们需要先清理。 开始写代码: 注意:我们假设PEM公钥是标准的,没有其他额外信息。 我们使用以下方法清理PEM: string base64 = pem.Replace("-----BEGIN PUBLIC KEY-----", "") .Replace("-----END PUBLIC KEY-----", "") .Replace("\n", "") .Replace("\r", "") .Trim(); 然后,将base64字符串转换为字节数组。 然后,使用BinaryReader解析这个字节数组。 但是,由于长度可能多字节,我们写一个辅助函数读取长度。 由于我们不想处理太复杂的ASN.1,这里我们参考一个简单实现: 参考:https://github.com/stulzq/RSAExtensions/blob/master/RSAExtensions/RSAKeyConvert.cs 我们采用其中解析公钥的方法,但为了简化,我们只处理我们需要的部分。 由于代码较长,我们只提取关键部分。 注意:在.NET Framework 4.7.2中,我们可以使用以下代码。 我们将编写一个类RsaKeyExtensions: 但是,为了直接解决问题,我们提供一个转换函数。 这里提供一个经过测试的转换函数(仅支持RSA公钥):</think># .NET Framework 4.7.2 使用 PEM 格式 RSA 公钥加密 在 .NET Framework 4.7.2 中,`RSACryptoServiceProvider` 不支持直接加载 PEM 格式的公钥,但可以通过手动解析 PEM 格式将其转换为 XML 格式。下面是完整的解决方案: ## 解决方案代码 ```csharp using System; using System.IO; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; public class RsaPemHelper { public static string EncryptWithPemKey(string plainText, string pemPublicKey) { // 将PEM转换为XML格式 string xmlPublicKey = ConvertPemToXml(pemPublicKey); using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(2048)) { try { // 导入XML格式的公钥 rsa.FromXmlString(xmlPublicKey); // 使用UTF-8编码(推荐)替代UnicodeEncoding byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText); // 使用PKCS#1 v1.5填充进行加密 byte[] encryptedBytes = rsa.Encrypt(plainTextBytes, false); return Convert.ToBase64String(encryptedBytes); } finally { // 确保密钥不被保留在内存中 rsa.PersistKeyInCsp = false; } } } private static string ConvertPemToXml(string pemPublicKey) { // 提取Base64部分 string base64 = Regex.Replace(pemPublicKey, @"-----(BEGIN|END) PUBLIC KEY-----|\s", string.Empty); // 解码Base64 byte[] der = Convert.FromBase64String(base64); // 解析ASN.1结构 using (BinaryReader reader = new BinaryReader(new MemoryStream(der))) { // 读取外层序列 byte seq = reader.ReadByte(); if (seq != 0x30) throw new InvalidOperationException("无效的PEM格式: 预期序列标签"); // 读取序列长度 int length = ReadAsn1Length(reader); long endPos = reader.BaseStream.Position + length; // 读取算法标识序列 byte innerSeq = reader.ReadByte(); if (innerSeq != 0x30) throw new InvalidOperationException("无效的PEM格式: 预期内部序列"); // 跳过算法标识 int algoLength = ReadAsn1Length(reader); reader.BaseStream.Seek(algoLength, SeekOrigin.Current); // 读取位字符串 byte bitString = reader.ReadByte(); if (bitString != 0x03) throw new InvalidOperationException("无效的PEM格式: 预期位字符串"); // 读取位字符串长度 int bitStringLength = ReadAsn1Length(reader); // 读取未使用位数(通常为0) byte unusedBits = reader.ReadByte(); if (unusedBits != 0) throw new InvalidOperationException("无效的PEM格式: 未使用位数应为0"); // 读取嵌套序列(实际公钥数据) byte keySeq = reader.ReadByte(); if (keySeq != 0x30) throw new InvalidOperationException("无效的PEM格式: 预期公钥序列"); // 读取公钥序列长度 int keySeqLength = ReadAsn1Length(reader); // 读取模数(第一个整数) byte modulusTag = reader.ReadByte(); if (modulusTag != 0x02) throw new InvalidOperationException("无效的PEM格式: 预期模数整数"); int modulusLength = ReadAsn1Length(reader); byte[] modulus = reader.ReadBytes(modulusLength); // 移除前导零(如果存在) if (modulus[0] == 0x00) { byte[] temp = new byte[modulusLength - 1]; Buffer.BlockCopy(modulus, 1, temp, 0, modulusLength - 1); modulus = temp; } // 读取指数(第二个整数) byte exponentTag = reader.ReadByte(); if (exponentTag != 0x02) throw new InvalidOperationException("无效的PEM格式: 预期指数整数"); int exponentLength = ReadAsn1Length(reader); byte[] exponent = reader.ReadBytes(exponentLength); // 构建XML格式 return $"<RSAKeyValue><Modulus>{Convert.ToBase64String(modulus)}</Modulus><Exponent>{Convert.ToBase64String(exponent)}</Exponent></RSAKeyValue>"; } } private static int ReadAsn1Length(BinaryReader reader) { byte lengthByte = reader.ReadByte(); // 短格式(长度 < 128) if ((lengthByte & 0x80) == 0) return lengthByte; // 长格式(长度 >= 128) int lengthBytesCount = lengthByte & 0x7F; if (lengthBytesCount > 4) throw new InvalidOperationException("无效的ASN.1长度格式"); int length = 0; for (int i = 0; i < lengthBytesCount; i++) { length = (length << 8) | reader.ReadByte(); } return length; } } ``` ## 使用方法 ```csharp string pemPublicKey = @" -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApPj5vE4MyVVCV/hqALzU uHRsXfex9dByCWcf7rqQPlkpYGnWJlS3VMwYHCEydfVLNlpQC7zvdxioVA3kr33T dkJHal8cdRnG/87a7C+hf2QzBgpwuM/54zYs1xG8E1sNiPcvHnpUjwHJHTC2vZd/ l18aC8LptCc1LF68uBHEc2Ue0ldya1Ci2eZM2e37XsKb7K1e41o5wLASe/rmbrV6 qLINh0enE+qK0dSYSILkNX934UB4yDLGA7T2LCIzlAA+hRcxUNgkx4J6Jhe4/0YM MkTDyy0r2otE3/Dcb/aAWKgltGpwcMtXEfRyIKXQsCczn0hku7TfI/Trzsn5aGSM /wIDAQAB -----END PUBLIC KEY-----"; string plainText = "需要加密的敏感数据"; try { string encryptedText = RsaPemHelper.EncryptWithPemKey(plainText, pemPublicKey); Console.WriteLine("加密结果: " + encryptedText); } catch (Exception ex) { Console.WriteLine("加密失败: " + ex.Message); } ``` ## 关键解析步骤说明 1. **PEM 格式处理**: - 移除 `BEGIN/END PUBLIC KEY` 标记和所有空白字符 - 提取 Base64 编码部分 2. **ASN.1 结构解析**: ```mermaid graph TD A[DER编码] --> B[外层序列] B --> C[算法标识序列] B --> D[位字符串] D --> E[公钥序列] E --> F[模数] E --> G[指数] ``` 3. **XML 格式构建**: - 模数 (Modulus) 和指数 (Exponent) 转换为 Base64 - 组装为 .NET 兼容的 XML 格式: ```xml <RSAKeyValue> <Modulus>...</Modulus> <Exponent>...</Exponent> </RSAKeyValue> ``` ## 重要注意事项 1. **编码问题**: - 使用 `Encoding.UTF8` 替代 `UnicodeEncoding`,避免跨系统兼容性问题 - 确保所有字符串处理使用相同编码 2. **密钥长度**: - 初始化 `RSACryptoServiceProvider` 时指定密钥长度(如2048) - 确保与 PEM 密钥的实际长度匹配 3. **填充模式**: - 使用 `rsa.Encrypt(..., false)` 指定 PKCS#1 v1.5 填充 - 如需更安全的 OAEP 填充,需双方系统支持 4. **内存安全**: ```csharp finally { rsa.PersistKeyInCsp = false; // 确保密钥不保留在内存中 } ``` 5. **错误处理**: - 捕获所有可能的异常并提供有意义错误信息 - 验证 PEM 格式有效性 ## 替代方案建议 对于需要更高安全性的场景,推荐使用混合加密方案[^1]: 1. 生成随机 AES 密钥:$K_{sym}$ 2. 使用 AES 加密数据:$C = AES(K_{sym}, P)$ 3. 使用 RSA 加密 AES 密钥:$C_k = RSA(K_{pub}, K_{sym})$ 4. 传输加密后的数据和密钥:$(C, C_k)$ 这种方案结合了对称加密的高效性和非对称加密的安全性优势[^1]。 ## 常见问题排查 1. **"无效的PEM格式"异常**: - 验证 PEM 格式是否正确 - 检查 BEGIN/END 标记是否完整 - 确保 Base64 编码正确无损坏 2. **"错误的长度"异常**: - 确认密钥长度匹配(2048/3072/4096位) - 检查 ASN.1 解析是否正确处理长度字节 3. **跨系统解密失败**: - 确认双方使用相同填充模式 - 验证编码是否一致(UTF-8) - 检查密钥是否匹配
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值