使用BouncyCastle解密大CSV文件时遇PartialInputStream流提前结束异常
Premature end of stream异常的问题与解决方案 问题场景
在C# .NET 7.0环境下,使用BouncyCastle.Cryptography v2.3.0实现PGP解密服务,小文件解密正常,但处理超过1364KB的CSV文件时,抛出如下异常:
Premature end of stream in PartialInputStream
异常抛出在BcpgInputStream.cs第338行的Read方法中,核心代码片段:
338: public override int Read(Span<byte> buffer) { do { if (dataLength != 0) { int count = buffer.Length; int readLen = (dataLength > count || dataLength < 0) ? count : dataLength; int len = m_in.Read(buffer[..readLen]); if (len < 1) throw new EndOfStreamException("Premature end of stream in PartialInputStream"); dataLength -= len; return len; } } while (partial && ReadPartialDataLength() >= 0); return 0; }
堆栈跟踪信息:
at Org.BouncyCastle.Bcpg.BcpgInputStream.PartialInputStream.Read(Span`1 buffer) in /_/crypto/src/bcpg/BcpgInputStream.cs:line 350 at System.IO.BufferedStream.Read(Span`1 destination) in /_/src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs:line 562 at Org.BouncyCastle.Bcpg.BcpgInputStream.Read(Span`1 buffer) in /_/crypto/src/bcpg/BcpgInputStream.cs:line 67 at Org.BouncyCastle.Utilities.IO.Streams.CopyTo(Stream source, Stream destination, Int32 bufferSize) in /_/crypto/src/util/io/Streams.cs:line 31 at Org.BouncyCastle.Utilities.IO.BaseInputStream.CopyTo(Stream destination, Int32 bufferSize) in /_/crypto/src/util/io/BaseInputStream.cs:line 21 at [redacted]ExternalPGPCryptographicService.Decrypt(Byte[] abPrivateKey, Byte[] abPassword, Byte[] abEncryptedData) in [redacted]ExternalPGPCryptographicService.cs:line 188 at [redacted]TestHarness.FormTestHarness._btnPGPCryptoDecrypt_Click(Object sender, EventArgs e) in [redacted]TestHarness.cs:line 86
异常在调用streamDecrypted.CopyTo(memoryStream)时触发,无论使用Streams.PipeAll还是直接调用streamDecrypted.Read都会报错。
加密环境与密钥情况
待解密文件是在Windows 11上用GnuPG v2.4.4执行以下命令加密的:
gpg --encrypt -r example@email.com '.\FILE.csv'
该加密文件可通过gpg CLI正常解密。
密钥对测试情况:
- 自研实现生成的RSA 3072密钥对(带ZIP压缩)
- gpg CLI加
--openpgp标志生成的密钥对
两种密钥对均能复现该问题。
触发阈值:只要源文件超过1364KB,无论内容如何必现;移除1个字符重新加密后,解密即可成功。
解密实现代码
public byte[] Decrypt(byte[] abPrivateKey, byte[] abPassword, byte[] abEncryptedData) { PgpObjectFactory pgpObjectFactory; PgpEncryptedDataList pgpEncryptedDataList; PgpObject pgpObject; PgpPrivateKey pgpPrivateKey; PgpPublicKeyEncryptedData pgpPublicKeyEncryptedData; PgpSecretKeyRingBundle pgpSecretKeyRingBundle; PgpLiteralData pgpLiteralData; // invalid provate key so don't bother if (abPrivateKey.Length == 0) { throw new Exception("Invalid private key"); } // no encrypted data so don't bother if (abEncryptedData.Length == 0) { return new byte[0]; } // load the private key and the encrypted data into memory streams using (Stream streamPrivateKey = new MemoryStream(abPrivateKey)) using (Stream streamEncryptedData = new MemoryStream(abEncryptedData)) { // create a new pgp object factory using the encrypted data pgpObjectFactory = new PgpObjectFactory(streamEncryptedData); pgpObject = pgpObjectFactory.NextPgpObject(); // try find the encrypted pgp data if (pgpObject is PgpEncryptedDataList) { pgpEncryptedDataList = (PgpEncryptedDataList)pgpObject; } else { pgpEncryptedDataList = (PgpEncryptedDataList)pgpObjectFactory.NextPgpObject(); } pgpPrivateKey = null; pgpPublicKeyEncryptedData = null; Stream streamPrivateKeyClear; PgpObjectFactory pgpObjectFactoryPrivateKey; PgpObject pgpMessage; // create a keyring bundle from the private key stream pgpSecretKeyRingBundle = new PgpSecretKeyRingBundle(PgpUtilities.GetDecoderStream(streamPrivateKey)); // try find the private key foreach (PgpPublicKeyEncryptedData pgpPublicKeyEncrypted in pgpEncryptedDataList.GetEncryptedDataObjects()) { // this is bad, it converts to string first pgpPrivateKey = FindSecretKey(pgpSecretKeyRingBundle, pgpPublicKeyEncrypted.KeyId, abPassword); if (pgpPrivateKey != null) { pgpPublicKeyEncryptedData = pgpPublicKeyEncrypted; break; } } // we must have a private key at this point if (pgpPrivateKey == null) { throw new Exception("Secret key not found"); } // find the public key from the private key bundle streamPrivateKeyClear = pgpPublicKeyEncryptedData.GetDataStream(pgpPrivateKey); // create private key pgpObjectFactoryPrivateKey = new PgpObjectFactory(streamPrivateKeyClear); // find the encrypted data in the pgp object pgpMessage = pgpObjectFactoryPrivateKey.NextPgpObject(); // find out if the message is compressed if (pgpMessage is PgpCompressedData) { PgpCompressedData pgpCompressedData; PgpObjectFactory pgpObjectFactoryCompressedData; // decompress the message pgpCompressedData = (PgpCompressedData)pgpMessage; pgpObjectFactoryCompressedData = new PgpObjectFactory(pgpCompressedData.GetDataStream()); pgpMessage = pgpObjectFactoryCompressedData.NextPgpObject(); } if (pgpMessage is PgpOnePassSignatureList) { throw new Exception("The PGP message has been signed and must be verified."); } // at this point it must be literal data. i.e. the actual message if (pgpMessage is PgpLiteralData) { // verify the integrity of the message if it is enabled if (pgpPublicKeyEncryptedData.IsIntegrityProtected()) { if (pgpPublicKeyEncryptedData.Verify() == false) { throw new Exception("Failed integrity check"); } } // now get the data from the message pgpLiteralData = (PgpLiteralData)pgpMessage; // pull out the decrypted data from the input stream using (MemoryStream memoryStream = new MemoryStream()) using (Stream streamDecrypted = pgpLiteralData.GetInputStream()) { streamDecrypted.CopyTo(memoryStream); return memoryStream.ToArray(); } } throw new Exception("The PGP message is in an unexpected state."); } }
注:受架构限制,加密文件在传入Decrypt函数前会被转为Base64字符串,之后通过Convert.FromBase64String转回字节数组。
解决方案
核心原因
GnuPG v2.4+默认使用AEAD加密模式(如OCB),而BouncyCastle.Cryptography v2.3.0对这种模式的大文件分段流处理存在兼容性问题,读取到分段末尾时会误判为流提前结束。
修复方案
修改GnuPG加密命令,禁用AEAD模式
在加密时添加参数强制使用传统加密模式,生成的文件可被现有BouncyCastle实现正常解密:gpg --encrypt -r example@email.com --s2k-cipher-algo AES-256 --disable-aead '.\FILE.csv'升级BouncyCastle版本
将BouncyCastle.Cryptography NuGet包升级到v2.4.0及以上版本,新版本已修复AEAD模式下大文件解密的流处理逻辑。临时兼容:捕获异常并判断流状态
若无法升级包或修改加密命令,可手动处理流读取,捕获EndOfStreamException并判断是否为正常结束:using (MemoryStream memoryStream = new MemoryStream()) using (Stream streamDecrypted = pgpLiteralData.GetInputStream()) { byte[] buffer = new byte[8192]; int bytesRead; try { while ((bytesRead = streamDecrypted.Read(buffer, 0, buffer.Length)) > 0) { memoryStream.Write(buffer, 0, bytesRead); } } catch (EndOfStreamException ex) { // 仅当未读取到任何数据时才抛出异常 if (memoryStream.Length == 0) { throw; } } return memoryStream.ToArray(); }
内容的提问来源于stack exchange,提问作者Dan

