# Encrypting data using AES-256 CBC and encoding for DPP

This tutorial provides a reference implementation for encrypting message content using AES-256 in CBC mode before uploading it to the Digital Post Platform (DPP).

For a description of the encoding format, see [CBC encryption and encoding](/documentation/delivery#encryption-during-delivery).

GCM mode preferred
CBC mode does not include data integrity checks. If encrypted data is corrupted or an incorrect key is used, the DPP will not be able to detect this.
It is strongly recommended to use AES-256 GCM mode where possible.
If using CBC, always specify a cleartext content hash in the envelope so the DPP can verify integrity.

## Encoding format

The encoded output has the following structure:

| Bytes | Description |
|  --- | --- |
| 1 byte | Encryption indicator — must be `1` for AES-256 CBC |
| 16 bytes | Initialization Vector (IV) — unique per encryption |
| Remaining bytes | AES-256 CBC encrypted content |


The total encrypted output (IV included) must not exceed 20 MB.

## Prerequisites

- .NET 6 or later
- C#


## Reference implementation

The following C# implementation writes an AES-256 CBC encrypted payload to a `PipeWriter`.
It streams the plaintext in chunks, avoiding large in-memory allocations.

```csharp
public static async Task WriteEncryptedPayloadAsync(
    PipeWriter writer,
  string plaintext,
    byte[] keyBytes,
    CancellationToken cancellationToken
)
{
    // Ensure this producer continues asynchronously so callers can start the upload immediately.
    await Task.Yield();
    try
    {
        using var aes = Aes.Create();
        aes.Key = keyBytes;
   aes.GenerateIV();

// Write header: 1-byte encryption indicator (1 = CBC) + 16-byte IV
 var header = new byte[1 + aes.IV.Length];
        header[0] = 1;
    Buffer.BlockCopy(aes.IV, 0, header, 1, aes.IV.Length);
        await writer.WriteAsync(header, cancellationToken).ConfigureAwait(false);

  using var encryptor = aes.CreateEncryptor();
    var encoder = Utf8NoBom.GetEncoder();

        const int blockSizeBytes = 16;
        const int charsPerChunk = 32 * 1024;
        int maxPlainBytesPerChunk = Utf8NoBom.GetMaxByteCount(charsPerChunk);

        byte[] plainBuffer  = ArrayPool<byte>.Shared.Rent(maxPlainBytesPerChunk);
        byte[] cipherBuffer = ArrayPool<byte>.Shared.Rent(maxPlainBytesPerChunk + blockSizeBytes);
        byte[] finalBlockBuffer = new byte[blockSizeBytes];
        int finalBlockCount = 0;
    int charIndex = 0;

    try
{
        while (charIndex < plaintext.Length)
          {
      int charsToProcess = Math.Min(charsPerChunk, plaintext.Length - charIndex);
        bool flush = (charIndex + charsToProcess) >= plaintext.Length;

  encoder.Convert(
       plaintext.AsSpan(charIndex, charsToProcess),
        plainBuffer,
     flush,
              out int charsUsed,
            out int bytesUsed,
        out _
      );
        charIndex += charsUsed;

    int offset = 0;
         while (offset < bytesUsed)
         {
         cancellationToken.ThrowIfCancellationRequested();

   // If there is a partial block buffered, fill it first
           if (finalBlockCount > 0)
                    {
  int needed = blockSizeBytes - finalBlockCount;
          int take = Math.Min(needed, bytesUsed - offset);
   Buffer.BlockCopy(plainBuffer, offset, finalBlockBuffer, finalBlockCount, take);
                    finalBlockCount += take;
    offset += take;

  if (finalBlockCount < blockSizeBytes)
       continue;

          int written = encryptor.TransformBlock(finalBlockBuffer, 0, blockSizeBytes, cipherBuffer, 0);
        await writer.WriteAsync(cipherBuffer.AsMemory(0, written), cancellationToken).ConfigureAwait(false);
      finalBlockCount = 0;
         continue;
         }

      int remaining = bytesUsed - offset;
              int fullBytes = (remaining / blockSizeBytes) * blockSizeBytes;

   if (fullBytes > 0)
      {
             // Encrypt all complete blocks in one call
       int written = encryptor.TransformBlock(plainBuffer, offset, fullBytes, cipherBuffer, 0);
 await writer.WriteAsync(cipherBuffer.AsMemory(0, written), cancellationToken).ConfigureAwait(false);
     offset += fullBytes;
  }
  else
          {
        // Buffer the remaining partial block bytes
    int take = Math.Min(blockSizeBytes, remaining);
  Buffer.BlockCopy(plainBuffer, offset, finalBlockBuffer, 0, take);
     finalBlockCount = take;
             offset += take;
     }
       }
            }

            // Finalize encryption — handles PKCS7 padding of the last block
            byte[] final = encryptor.TransformFinalBlock(finalBlockBuffer, 0, finalBlockCount);
   if (final.Length > 0)
                await writer.WriteAsync(final, cancellationToken).ConfigureAwait(false);

            await writer.CompleteAsync().ConfigureAwait(false);
        }
        finally
     {
  ArrayPool<byte>.Shared.Return(plainBuffer);
       ArrayPool<byte>.Shared.Return(cipherBuffer);
        }
    }
    catch (Exception ex)
    {
    await writer.CompleteAsync(ex).ConfigureAwait(false);
   throw;
    }
}
```

## How it works

1. **Header** — A 1-byte indicator (`1`) is written, followed by a randomly generated 16-byte IV.
2. **Chunked encoding** — The plaintext is converted to UTF-8 and processed in 32 KB character chunks to keep memory usage low.
3. **Block alignment** — Partial CBC blocks are buffered across chunk boundaries using `finalBlockBuffer`, ensuring full 16-byte blocks are always passed to `TransformBlock`.
4. **Finalization** — `TransformFinalBlock` applies PKCS7 padding and writes the last encrypted block.
5. **Streaming** — The implementation writes directly to a `PipeWriter`, allowing the upload to begin before encryption is complete.


## Usage notes

- `keyBytes` must be exactly 32 bytes (256 bits).
- `Utf8NoBom` refers to `new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)`.
- The IV is generated fresh per call using `aes.GenerateIV()`. Never reuse an IV with the same key.
- Because CBC provides no integrity guarantee, always include the SHA-256 hash of the plaintext content in the envelope `hash` field so the DPP can detect corruption.