Skip to content

Instantly share code, notes, and snippets.

@tonyjoanes
Created July 14, 2025 09:01
Show Gist options
  • Save tonyjoanes/918629efdd7cd949dda4cd25f3a75597 to your computer and use it in GitHub Desktop.
Save tonyjoanes/918629efdd7cd949dda4cd25f3a75597 to your computer and use it in GitHub Desktop.
using System.Text;
using Azure.Storage.Blobs;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using Org.BouncyCastle.Bcpg;
using Org.BouncyCastle.Bcpg.OpenPgp;
using Org.BouncyCastle.Security;
namespace EncryptBlobFunction;
public class EncryptBlobFunction
{
private readonly ILogger<EncryptBlobFunction> _logger;
private const int BufferSize = 1024 * 1024; // 1MB buffer for streaming
private const string OutputContainerName = "output-container";
public EncryptBlobFunction(ILogger<EncryptBlobFunction> logger)
{
_logger = logger;
}
[Function("EncryptBlobFunction")]
public async Task Run(
[BlobTrigger("input-container/{name}", Connection = "AzureWebJobsStorage")]
Stream inputBlob,
string name,
FunctionContext context
)
{
_logger.LogInformation(
"Started processing blob: {BlobName} with size: {Size} bytes",
name,
inputBlob.Length
);
try
{
// Get connection string and create blob service client
var connectionString = Environment.GetEnvironmentVariable("AzureWebJobsStorage");
if (string.IsNullOrEmpty(connectionString))
{
throw new InvalidOperationException(
"AzureWebJobsStorage connection string is not configured"
);
}
var blobServiceClient = new BlobServiceClient(connectionString);
var outputContainerClient = blobServiceClient.GetBlobContainerClient(
OutputContainerName
);
// Ensure output container exists
await outputContainerClient.CreateIfNotExistsAsync();
// Generate output blob name with .pgp extension
var outputBlobName = Path.GetFileNameWithoutExtension(name) + ".pgp";
var outputBlobClient = outputContainerClient.GetBlobClient(outputBlobName);
// Load PGP public key
var publicKey = LoadPgpPublicKey();
if (publicKey is null)
{
throw new InvalidOperationException(
"Failed to load PGP public key from environment variable"
);
}
_logger.LogInformation("Successfully loaded PGP public key");
// Stream encrypt and upload
await using var outputStream = await outputBlobClient.OpenWriteAsync(overwrite: true);
await EncryptStreamWithPgp(inputBlob, outputStream, publicKey);
_logger.LogInformation(
"Successfully encrypted and uploaded blob: {OutputBlobName}",
outputBlobName
);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing blob: {BlobName}", name);
throw;
}
}
private PgpPublicKey? LoadPgpPublicKey()
{
try
{
var publicKeyString = Environment.GetEnvironmentVariable("PgpPublicKey");
if (string.IsNullOrEmpty(publicKeyString))
{
_logger.LogError("PgpPublicKey environment variable is not set");
return null;
}
// Convert ASCII-armored key to stream
using var keyStream = new MemoryStream(Encoding.UTF8.GetBytes(publicKeyString));
using var decoderStream = PgpUtilities.GetDecoderStream(keyStream);
var pgpPub = new PgpPublicKeyRingBundle(decoderStream);
// Get the first public key suitable for encryption
foreach (PgpPublicKeyRing keyRing in pgpPub.GetKeyRings())
{
foreach (PgpPublicKey key in keyRing.GetPublicKeys())
{
if (key.IsEncryptionKey)
{
_logger.LogInformation(
"Found encryption key with ID: {KeyId}",
key.KeyId.ToString("X")
);
return key;
}
}
}
_logger.LogError("No encryption key found in the public key ring");
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error loading PGP public key");
return null;
}
}
private async Task EncryptStreamWithPgp(
Stream inputStream,
Stream outputStream,
PgpPublicKey publicKey
)
{
try
{
// Create PGP encryption objects
var encryptedDataGenerator = new PgpEncryptedDataGenerator(
SymmetricKeyAlgorithmTag.Aes256,
withIntegrityPacket: true,
new SecureRandom()
);
encryptedDataGenerator.AddMethod(publicKey);
// Create compressed data generator for optional compression
var compressedDataGenerator = new PgpCompressedDataGenerator(
CompressionAlgorithmTag.Zip
);
// Create the encrypted stream
await using var encryptedStream = encryptedDataGenerator.Open(
outputStream,
new byte[BufferSize]
);
await using var compressedStream = compressedDataGenerator.Open(encryptedStream);
// Create literal data generator
var literalDataGenerator = new PgpLiteralDataGenerator();
var fileName = "encrypted_file.csv"; // Generic filename for security
await using var literalStream = literalDataGenerator.Open(
compressedStream,
PgpLiteralData.Binary,
fileName,
inputStream.Length,
DateTime.UtcNow
);
// Stream the data in chunks to keep memory usage low
var buffer = new byte[BufferSize];
int bytesRead;
long totalBytesProcessed = 0;
inputStream.Position = 0; // Ensure we start from the beginning
while ((bytesRead = await inputStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await literalStream.WriteAsync(buffer, 0, bytesRead);
totalBytesProcessed += bytesRead;
// Log progress for large files
if (totalBytesProcessed % (BufferSize * 10) == 0) // Every 10MB
{
_logger.LogInformation(
"Processed {ProcessedBytes} bytes of {TotalBytes} bytes",
totalBytesProcessed,
inputStream.Length
);
}
}
_logger.LogInformation(
"Encryption completed. Total bytes processed: {TotalBytes}",
totalBytesProcessed
);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during PGP encryption");
throw;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment