Eine Datei mit einem öffentlichen Schlüssel/Zertifikat verschlüsseln und mit einem privaten Schlüssel/Zertifikat entschlüsseln

Mit Public-Private-Keys/-Zertifikaten können Daten verschlüsselt und entschlüsselt werden. Der öffentliche Schlüssel wird verwendet zum Verschlüsseln, und später wird der geheime private Schlüssel genutzt, um verschlüsselte Daten zu entschlüsseln.

Nach sehr viel ausprobieren und recherchieren habe ich nachfolgende Funktion zum Verschlüsseln und entschlüsseln gefunden/entwickelt.

Verschlüsseln

private static void doEncryptFile(
    string sourceFilePath, 
    string destinationFilePath)
{
    var collection = new X509Certificate2Collection();

    collection.Import(
        encryptPublicKeyFilePath, 
        null, 
        X509KeyStorageFlags.PersistKeySet);

    var certificate = collection[0];

    var data = File.ReadAllBytes(sourceFilePath);

    var contentInfo = new ContentInfo(data);
    var envelopedCms = new EnvelopedCms(contentInfo);
    envelopedCms.Encrypt(new CmsRecipient(certificate));

    var encryptedData = envelopedCms.Encode();

    File.WriteAllBytes(destinationFilePath, encryptedData);
}

Quelle

encryptPublicKeyFilePath ist hier der Dateipfad zu einem öffentlichen Schlüssel (als *.cer-Datei)

Entschlüsseln

private static void doDecryptFile(
    string sourceFilePath, 
    string destinationFilePath)
{
    var collection = new X509Certificate2Collection();
    collection.Import(
        encryptPrivateKeyFilePath, 
        encryptPassword, 
        X509KeyStorageFlags.PersistKeySet);

    var encryptedData = File.ReadAllBytes(sourceFilePath);

    var envelopedCms = new EnvelopedCms();
    envelopedCms.Decode(encryptedData);
    envelopedCms.Decrypt(collection);
    var data = envelopedCms.ContentInfo.Content;

    File.WriteAllBytes(destinationFilePath, data);
}

Quelle

encryptPrivateKeyFilePath ist hier der Dateipfad zu einem privaten Schlüssel (als *.pfx-Datei), der mit encryptPassword geschützt ist.

Große Dateien

Ich habe festgestellt, dass obiger Code jeweils nur bei Dateien bis ca. 100 MB funktioniert. Das sei wohl eine Einschränkung von der .NET-Implementierung bei Microsoft.

Die zugrundeliegende Win32-Crypto-Bibliothek hat einen so genannten „Streaming mode“, der auch größere Dateien verarbeiten kann. Hier gibt es eine 10 Jahre alte Implementation, die ich leider beim Entschlüsseln nicht zum Laufen gebracht habe.

Ebenso habe ich es nach viel Ausprobieren, Googeln und Fluchen nicht geschafft, mit BouncyCastle C# die Funktionalität umzusetzen.

Ich bin deshalb einen anderen Weg gegangen:

Eine große zu verschlüsselnde Datei behandle ich wie folgt:

  1. Datei in Blöcke von maximal 70 MB aufsplitten
  2. Jeden Block-Datei einzeln verschlüsseln
  3. Alle verschlüsselten Block-Dateien in einer ZIP-Datei zusammen fassen

Umgekehrt mache ich es beim Entschlüsseln:

  1. ZIP-Datei entpacken
  2. Die einzelnen Block-Dateien entschlüsseln
  3. Alle entschlüsselten Block-Dateien in der richtigen Reihenfolge zu einer Datei hintereinander anhängen

Code fürs Verschlüsseln:

private static void encryptFile(
    string sourceFilePath, 
    string destinationFilePath)
{
    const int splitAtMB = 70;
    const int splitAtBytes = splitAtMB * 1024 * 1024;

    var tempSplittedFolder = 
        Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(@"N"));
    var tempEncryptedFolder = 
        Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(@"N"));
    Directory.CreateDirectory(tempSplittedFolder);
    Directory.CreateDirectory(tempEncryptedFolder);
    try
    {
        splitFile(sourceFilePath, splitAtBytes, tempSplittedFolder);

        foreach (var sfp in Directory.GetFiles(tempSplittedFolder))
        {
            var dfp = Path.Combine(tempEncryptedFolder, Path.GetFileName(sfp));
            doEncryptFile(sfp, dfp);
        }

        // ZIP bauen.
        if (File.Exists(destinationFilePath)) File.Delete(destinationFilePath);
        ZipFile.CreateFromDirectory(
            tempEncryptedFolder,
            destinationFilePath,
            CompressionLevel.NoCompression,
            false);
    }
    finally
    {
        Directory.Delete(tempSplittedFolder, true);
        Directory.Delete(tempEncryptedFolder, true);
    }
}

Code zum Entschlüsseln:

private static void decryptFile(
    string sourceFilePath, 
    string destinationFilePath)
{
    var tempSplittedFolder = 
        Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(@"N"));
    var tempEncryptedFolder = 
        Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(@"N"));
    Directory.CreateDirectory(tempSplittedFolder);
    Directory.CreateDirectory(tempEncryptedFolder);
    try
    {
        ZipFile.ExtractToDirectory(
            sourceFilePath,
            tempEncryptedFolder);

        foreach (var sfp in Directory.GetFiles(tempEncryptedFolder))
        {
            var dfp = Path.Combine(tempSplittedFolder, Path.GetFileName(sfp));
            doDecryptFile(sfp, dfp);
        }

        // Zusammenbauen.
        if (File.Exists(destinationFilePath)) File.Delete(destinationFilePath);
        joinFiles(
            Directory.GetFiles(tempSplittedFolder).OrderBy(f => f).ToArray(), 
            destinationFilePath);
    }
    finally
    {
        Directory.Delete(tempSplittedFolder, true);
        Directory.Delete(tempEncryptedFolder, true);
    }
}

Schlüssel erzeugen

Das Erzeugen der privaten und öffentlichen Schlüssel habe ich auch per Code machen wollen:

private static void makeCert(
    string publicKeyFilePath,
    string privateKeyFilePath,
    string password,
    string cn)
{
    var ecdsa = RSA.Create(); // generate asymmetric key pair
    var req = new CertificateRequest($@"cn={cn}", ecdsa, 
        HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
    var cert = req.CreateSelfSigned(
        DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1));

    // Create PFX (PKCS #12) with private key
    File.WriteAllBytes(privateKeyFilePath, 
        cert.Export(X509ContentType.Pkcs12, password));

    // Create CER (public key only)
    File.WriteAllBytes(publicKeyFilePath, 
        cert.Export(X509ContentType.Cert));
}

Quelle

Wer statt Dateien nur Zeichenfolgen im Speicher verschlüsseln/entschlüsseln will, kann die oben genannten Methoden natürlich ganz einfach adaptieren.

Verschlüsseln

private static string doEncryptString(
    string data)
{
    var collection = new X509Certificate2Collection();

    collection.Import(
        encryptPublicKeyFilePath, 
        null, 
        X509KeyStorageFlags.PersistKeySet);

    var certificate = collection[0];

    var contentInfo = new ContentInfo(data);
    var envelopedCms = new EnvelopedCms(contentInfo);
    envelopedCms.Encrypt(new CmsRecipient(certificate));

    var encryptedData = envelopedCms.Encode();

    return encryptedData;
}

Entschlüsseln

private static string doDecryptString(
    string encryptedData)
{
    var collection = new X509Certificate2Collection();
    collection.Import(
        encryptPrivateKeyFilePath, 
        encryptPassword, 
        X509KeyStorageFlags.PersistKeySet);

    var envelopedCms = new EnvelopedCms();
    envelopedCms.Decode(encryptedData);
    envelopedCms.Decrypt(collection);
    var data = envelopedCms.ContentInfo.Content;

    return data;
}