abpframework/abp · error · AbpException

The encrypted BLOB is corrupted or has an invalid format: in

Error message

The encrypted BLOB is corrupted or has an invalid format: invalid terminal record!

What it means

In ChunkedDecryptingReadStream.ProduceNext, a zero-length prefix signals the terminal record. The codec then reads the terminal GCM tag; if ReadExactly returns null (stream ended before the tag) or extra bytes follow the tag, the terminal record is malformed and the BLOB cannot be authenticated as complete, so it throws.

Source

Thrown at framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/ChunkedDecryptingReadStream.cs:63

    }

    public ValueTask EnsureReadToAuthenticatedEndAsync(CancellationToken cancellationToken = default)
    {
        return EnsureReadToAuthenticatedEndCoreAsync(cancellationToken);
    }

    protected override byte[]? ProduceNext()
    {
        var cipherChunkSize = BlobEncryptionCodec.GetCipherChunkSize(
            BlobEncryptionCodec.ReadUpTo(_cipherStream, BlobEncryptionCodec.ChunkLengthPrefixSize),
            _chunkSize
        );
        if (cipherChunkSize == 0)
        {
            var terminalTag = BlobEncryptionCodec.ReadExactly(_cipherStream, BlobEncryptionCodec.GcmTagSize);
            if (terminalTag == null || BlobEncryptionCodec.ReadUpTo(_cipherStream, 1).Length != 0)
            {
                throw new AbpException("The encrypted BLOB is corrupted or has an invalid format: invalid terminal record!");
            }

            SetChunkIndex(_chunkIndex);
            BlobEncryptionCodec.VerifyTerminalRecordCore(_chunkCipher, _associatedData, _nonce, terminalTag);
            return null;
        }

        return DecryptPayload(
            BlobEncryptionCodec.ReadExactly(_cipherStream, cipherChunkSize),
            BlobEncryptionCodec.ReadExactly(_cipherStream, BlobEncryptionCodec.GcmTagSize)
        );
    }

    protected override async Task<byte[]?> ProduceNextAsync(CancellationToken cancellationToken)
    {
        var cipherChunkSize = BlobEncryptionCodec.GetCipherChunkSize(
            await BlobEncryptionCodec.ReadUpToAsync(_cipherStream, BlobEncryptionCodec.ChunkLengthPrefixSize, cancellationToken),
            _chunkSize

View on GitHub (pinned to 7ed43b1931)

Solutions

  1. Re-upload the BLOB from a known-good source.
  2. Verify the stored object length matches an expected ciphertext length.
  3. Check the storage backend for partial writes, replication lag, or appended junk.
  4. Ensure nothing else writes to the same BLOB key (no append/concat).

Example fix

// before — saving then appending to the same key
await blob.SaveAsync(name, enc1);
await AppendAsync(name, enc2); // trailing garbage after terminal record

// after — one writer per key, no append
await blob.SaveAsync(name, enc1);
await blob.SaveAsync(name + ".2", enc2);
Defensive patterns

Strategy: try-catch

Validate before calling

// Verify the stored object length is consistent with the format before decrypting.
var info = await provider.GetOrNullAsync(name);
if (info == null) throw new FileNotFoundException(name);
// An encrypted BLOB is: magic + version + header + chunks + terminal record.
// Reject implausibly short or unexpectedly long objects.
if (info.ContentLength < 32)
    throw new InvalidOperationException($"BLOB '{name}' too short to contain a valid terminal record.");

Type guard

public sealed record VerifiedEncryptedBlob(string Name, long ContentLength)
{
    public static VerifiedEncryptedBlob Check(string name, long len)
    {
        if (len < 32) throw new InvalidOperationException("blob too short / no terminal record");
        return new VerifiedEncryptedBlob(name, len);
    }
}

Try / catch

try
{
    return await blob.GetAllBytesAsync(name);
}
catch (AbpException ex) when (ex.Message.Contains("invalid terminal record"))
{
    logger.LogError(ex, "BLOB '{Name}' has a malformed terminal record; re-upload.", name);
    throw;
}

Prevention

When it happens

Trigger: Decrypting a BLOB whose cipher stream ends before the terminal tag is fully present, or which has trailing garbage after the terminal tag (synchronous read path).

Common situations: Truncation that cuts off the terminal tag; concatenation of two BLOBs into one object; tampering that appended bytes; storage corruption.

Related errors


AI-assisted analysis of abpframework/abp@7ed43b1931 (2026-08-13). Data as JSON: /api/errors/6350ad6d958f05f2. Report an issue: GitHub.