You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用BouncyCastle解密大CSV文件时遇PartialInputStream流提前结束异常

PGP解密大文件时抛出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对这种模式的大文件分段流处理存在兼容性问题,读取到分段末尾时会误判为流提前结束。

修复方案

  1. 修改GnuPG加密命令,禁用AEAD模式
    在加密时添加参数强制使用传统加密模式,生成的文件可被现有BouncyCastle实现正常解密:

    gpg --encrypt -r example@email.com --s2k-cipher-algo AES-256 --disable-aead '.\FILE.csv'
    
  2. 升级BouncyCastle版本
    将BouncyCastle.Cryptography NuGet包升级到v2.4.0及以上版本,新版本已修复AEAD模式下大文件解密的流处理逻辑。

  3. 临时兼容:捕获异常并判断流状态
    若无法升级包或修改加密命令,可手动处理流读取,捕获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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 06:04:54