{"record":{"id":"6350ad6d958f05f2","repo":"abpframework/abp","slug":"the-encrypted-blob-is-corrupted-or-has-an-invalid-6350ad","errorCode":null,"errorMessage":"The encrypted BLOB is corrupted or has an invalid format: invalid terminal record!","messagePattern":"The encrypted BLOB is corrupted or has an invalid format: invalid terminal record!","errorType":"exception","errorClass":"AbpException","httpStatus":null,"severity":"error","filePath":"framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/ChunkedDecryptingReadStream.cs","lineNumber":63,"sourceCode":"    }\n\n    public ValueTask EnsureReadToAuthenticatedEndAsync(CancellationToken cancellationToken = default)\n    {\n        return EnsureReadToAuthenticatedEndCoreAsync(cancellationToken);\n    }\n\n    protected override byte[]? ProduceNext()\n    {\n        var cipherChunkSize = BlobEncryptionCodec.GetCipherChunkSize(\n            BlobEncryptionCodec.ReadUpTo(_cipherStream, BlobEncryptionCodec.ChunkLengthPrefixSize),\n            _chunkSize\n        );\n        if (cipherChunkSize == 0)\n        {\n            var terminalTag = BlobEncryptionCodec.ReadExactly(_cipherStream, BlobEncryptionCodec.GcmTagSize);\n            if (terminalTag == null || BlobEncryptionCodec.ReadUpTo(_cipherStream, 1).Length != 0)\n            {\n                throw new AbpException(\"The encrypted BLOB is corrupted or has an invalid format: invalid terminal record!\");\n            }\n\n            SetChunkIndex(_chunkIndex);\n            BlobEncryptionCodec.VerifyTerminalRecordCore(_chunkCipher, _associatedData, _nonce, terminalTag);\n            return null;\n        }\n\n        return DecryptPayload(\n            BlobEncryptionCodec.ReadExactly(_cipherStream, cipherChunkSize),\n            BlobEncryptionCodec.ReadExactly(_cipherStream, BlobEncryptionCodec.GcmTagSize)\n        );\n    }\n\n    protected override async Task<byte[]?> ProduceNextAsync(CancellationToken cancellationToken)\n    {\n        var cipherChunkSize = BlobEncryptionCodec.GetCipherChunkSize(\n            await BlobEncryptionCodec.ReadUpToAsync(_cipherStream, BlobEncryptionCodec.ChunkLengthPrefixSize, cancellationToken),\n            _chunkSize","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/abpframework/abp/blob/7ed43b1931b9df46a50c0c59148a18645641d0df/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/ChunkedDecryptingReadStream.cs#L45-L81","documentation":"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.","triggerScenarios":"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).","commonSituations":"Truncation that cuts off the terminal tag; concatenation of two BLOBs into one object; tampering that appended bytes; storage corruption.","solutions":["Re-upload the BLOB from a known-good source.","Verify the stored object length matches an expected ciphertext length.","Check the storage backend for partial writes, replication lag, or appended junk.","Ensure nothing else writes to the same BLOB key (no append/concat)."],"exampleFix":"// before — saving then appending to the same key\nawait blob.SaveAsync(name, enc1);\nawait AppendAsync(name, enc2); // trailing garbage after terminal record\n\n// after — one writer per key, no append\nawait blob.SaveAsync(name, enc1);\nawait blob.SaveAsync(name + \".2\", enc2);","handlingStrategy":"try-catch","validationCode":"// Verify the stored object length is consistent with the format before decrypting.\nvar info = await provider.GetOrNullAsync(name);\nif (info == null) throw new FileNotFoundException(name);\n// An encrypted BLOB is: magic + version + header + chunks + terminal record.\n// Reject implausibly short or unexpectedly long objects.\nif (info.ContentLength < 32)\n    throw new InvalidOperationException($\"BLOB '{name}' too short to contain a valid terminal record.\");","typeGuard":"public sealed record VerifiedEncryptedBlob(string Name, long ContentLength)\n{\n    public static VerifiedEncryptedBlob Check(string name, long len)\n    {\n        if (len < 32) throw new InvalidOperationException(\"blob too short / no terminal record\");\n        return new VerifiedEncryptedBlob(name, len);\n    }\n}","tryCatchPattern":"try\n{\n    return await blob.GetAllBytesAsync(name);\n}\ncatch (AbpException ex) when (ex.Message.Contains(\"invalid terminal record\"))\n{\n    logger.LogError(ex, \"BLOB '{Name}' has a malformed terminal record; re-upload.\", name);\n    throw;\n}","preventionTips":["Write each BLOB key exactly once; never append/concat.\n        ","Verify upload completion (ETag/status) before reads.","Store checksums and validate on read.","Investigate trailing-bytes corruption in storage."],"tags":["crypto","integrity","corruption","terminal-record"],"backgroundTag":null,"analyzedSha":"7ed43b1931b9df46a50c0c59148a18645641d0df","analyzedAt":"2026-08-13T16:26:11.351Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}