砂漠の旅人(たびと)

UNIX / MS-DOS 時代から電脳砂漠を旅しています

【暗号/複合】C#でOpenSSL AES-256-CBC を作る (PBKDF2 & SHA256, MD5)

こんにちは、たびとです。

とある REST API のパラメータに、openssl enc コマンドで作った暗号文を何度も渡す処理が、 非効率なので、C# で暗号化して REST API を実行すれば楽になりそう、が出発点です。

openssl enc コマンドを使ってコンソール上で暗号化・複合化を行い、 それを C# のプログラムからも同じように処理できることを確認します。

今回は下記 2パターンの C# プログラムを作ります。

  • SHA256(-md) + PBKDF2 の暗号化・複合化
  • MD5(-md) の暗号化・複合化 (openssl 1.1.0 未満のデフォルト)

この記事の対象者

  • openssl enc コマンドを使った暗号化・複合化に興味がある
  • openssl enc コマンドを使った暗号化・複合化を C# で実装してみたい

openssl enc による暗号化/複合化

Ubuntu 上で確認する openssl の版数は v3.0.13 です。

$ openssl version
OpenSSL 3.0.13 30 Jan 2024 (Library: OpenSSL 3.0.13 30 Jan 2024)

openssl enc コマンドはオプションが多いので、ヘルプを確認します。

$ openssl enc -help
Usage: enc [options]

General options:
 -help               Display this summary
 -list               List ciphers
 -ciphers            Alias for -list
 -e                  Encrypt
 -d                  Decrypt
 -p                  Print the iv/key
 -P                  Print the iv/key and exit
 -engine val         Use engine, possibly a hardware device

Input options:
 -in infile          Input file
 -k val              Passphrase
 -kfile infile       Read passphrase from file

Output options:
 -out outfile        Output file
 -pass val           Passphrase source
 -v                  Verbose output
 -a                  Base64 encode/decode, depending on encryption flag
 -base64             Same as option -a
 -A                  Used with -[base64|a] to specify base64 buffer as a single line

Encryption options:
 -nopad              Disable standard block padding
 -salt               Use salt in the KDF (default)
 -nosalt             Do not use salt in the KDF
 -debug              Print debug info
 -bufsize val        Buffer size
 -K val              Raw key, in hex
 -S val              Salt, in hex
 -iv val             IV in hex
 -md val             Use specified digest to create a key from the passphrase
 -iter +int          Specify the iteration count and force the use of PBKDF2
                     Default: 10000
 -pbkdf2             Use password-based key derivation function 2 (PBKDF2)
                     Use -iter to change the iteration count from 10000
 -none               Don't encrypt
 -*                  Any supported cipher

Random state options:
 -rand val           Load the given file(s) into the random number generator
 -writerand outfile  Write random data to the specified file

Provider options:
 -provider-path val  Provider load path (must be before 'provider' argument if required)
 -provider val       Provider to load (can be specified multiple times)
 -propquery val      Property query used when fetching algorithms

今回利用するオプションは下記の通り。

enc オプション 説明
-e 暗号化
-d 複合化
-aes-256-cbc AES 256bit CBC モード (C# AES デフォルト)
-md val sha256(デフォルト) or md5
-base64 Base64 エンコード・デコード (-a と同じ)
-A -base64 とセット
-pass val パスフレーズ
-pbkdf2 パスフレーズのハッシュ化方式
-iter +int pbkdf2 のストレッチング回数(デフォルト 10,000)

SHA256(-md) + PBKDF2 の暗号化・複合化

openssl enc コマンドを使って、文字列を暗号化します。

$ echo -n 'Ubuntu:SHA256 + PBKDF2' | openssl enc -e -aes-256-cbc -md sha256 -base64 -A -pass pass:password -pbkdf2
U2FsdGVkX180uM6YCr78gph0+q9msr+1uQ+ypYaJ3OlN8Dvx4dm/LPKfEK/N0ukM

openssl enc コマンドを使って、暗号化した文字列を複合化できることを確認します。

$ echo -n 'U2FsdGVkX180uM6YCr78gph0+q9msr+1uQ+ypYaJ3OlN8Dvx4dm/LPKfEK/N0ukM' | openssl enc -d -aes-256-cbc -md sha256 -base64 -A -pass pass:password -pbkdf2
Ubuntu:SHA256 + PBKDF2

MD5(-md) の暗号化・複合化

openssl enc コマンドを使って、文字列を暗号化します。 警告が表示されますが、無視します。 このコマンドは openssl 1.1.0 未満で実装したプログラムとの互換性維持に利用していると思います。

$ echo -n 'Ubuntu:MD5' | openssl enc -e -aes-256-cbc -md md5 -base64 -A -pass pass:password
*** WARNING : deprecated key derivation used.
Using -iter or -pbkdf2 would be better.
U2FsdGVkX1+q3wHjghZvztfesMppgfFP917KweDAtYA=

openssl enc コマンドを使って、暗号化した文字列を複合化できることを確認します。 こちらも同様に警告が表示されますが、無視します。

$ echo -n 'U2FsdGVkX1+q3wHjghZvztfesMppgfFP917KweDAtYA=' | openssl enc -d -aes-256-cbc -md md5 -base64 -A -pass pass:password
*** WARNING : deprecated key derivation used.
Using -iter or -pbkdf2 would be better.
Ubuntu:MD5

C# による実装

.NET 8.0 を使ったコンソールアプリです。できるだけ簡潔に記載しました。

下記の暗号化・複合化をプログラムの最後に直接記載しています。

  1. C# 版 SHA256 + PBKDF2 の暗号化・複合化
  2. C#MD5 の暗号化・複合化
  3. openssl enc コマンド SHA256 + PBKDF2 暗号文を複合化 (上記結果を利用)
  4. openssl enc コマンド MD5 暗号文を複合化 (上記結果を利用)
using System.Security.Cryptography;
using System.Text;

var isPbkdf2 = true;        // -pbkdf2
var pbkdf2Count = 10000;    // -iter +int

byte[] GenerateRandomBytes(int length)
{
    using var rng = RandomNumberGenerator.Create();
    var randomBytes = new byte[length];
    rng.GetBytes(randomBytes);
    return randomBytes;
}

(byte[], byte[]) DeriveKeyAndIV(string passphrase, byte[] salt)
{
    byte[] key, iv;
    var pass = Encoding.UTF8.GetBytes(passphrase);
    if (isPbkdf2)
    {
        // PBKDF2(-pbkdf2, -iter +int, -md sha256)
        using var derivedBytes = new Rfc2898DeriveBytes(pass, salt, pbkdf2Count, HashAlgorithmName.SHA256);
        key = derivedBytes.GetBytes(32);
        iv = derivedBytes.GetBytes(16);
    }
    else
    {
        // -md md5
        var hash1 = MD5.HashData([.. pass, .. salt]);           // Hash1 = MD5(Password + Salt)
        var hash2 = MD5.HashData([.. hash1, .. pass, .. salt]); // Hash2 = MD5(Hash1 + Password + Salt)
        key = (byte[])[.. hash1, .. hash2];                     // Key   = Hash1 + Hash2
        iv = MD5.HashData([.. hash2, .. pass, .. salt]);        // IV    = MD5(Hash2 + Password + Salt)
    }
    return (key, iv);
}

string Encrypt(string passphrase, string plainText)
{
    var salt = GenerateRandomBytes(8);
    (var key, var iv) = DeriveKeyAndIV(passphrase, salt);
    var createEncryptor = Aes.Create().CreateEncryptor(key, iv);

    var head = (byte[])[.. Encoding.ASCII.GetBytes("Salted__"), .. salt];
    using MemoryStream mem = new();
    mem.Write(head, 0, head.Length);
    using CryptoStream crypt = new(mem, createEncryptor, CryptoStreamMode.Write);
    using (StreamWriter writer = new(crypt))
        writer.Write(plainText);
    return Convert.ToBase64String(mem.ToArray());
}

string Decrypt(string passphrase, string cipherText)
{
    var body = Convert.FromBase64String(cipherText);
    var salt = body[8..16];
    (var key, var iv) = DeriveKeyAndIV(passphrase, salt);
    var createDecryptor = Aes.Create().CreateDecryptor(key, iv);

    using MemoryStream mem = new(body[16..]);
    using CryptoStream crypt = new(mem, createDecryptor, CryptoStreamMode.Read);
    using StreamReader reader = new(crypt);
    return reader.ReadToEnd();
}

string passphrase = "password";
string result;

// 暗号化:SHA256 + PBKDF2
var text = "C#: SHA256 + PBKDF2";
result = Encrypt(passphrase, text);
Console.WriteLine($"Encrypt-1: {result}");

// 複合化:SHA256 + PBKDF2
result = Decrypt(passphrase, result);
Console.WriteLine($"Decrypt-1: {result}");
Console.WriteLine();

// 暗号化:MD5
isPbkdf2 = false;       // MD5
text = "C#: MD5";
result = Encrypt(passphrase, text);
Console.WriteLine($"Encrypt-2: {result}");

// 複合化:MD5
result = Decrypt(passphrase, result);
Console.WriteLine($"Decrypt-2: {result}");
Console.WriteLine();

// openssl enc コマンド結果の複合化(SHA256 + PBKDF2)
isPbkdf2 = true;        // PBKDF2
text = "U2FsdGVkX180uM6YCr78gph0+q9msr+1uQ+ypYaJ3OlN8Dvx4dm/LPKfEK/N0ukM";
result = Decrypt(passphrase, text);
Console.WriteLine($"Decrypt-3: {text}");
Console.WriteLine($"Decrypt-3: {result}");
Console.WriteLine();

// openssl enc コマンド結果の複合化(MD5)
isPbkdf2 = false;       // MD5
text = "U2FsdGVkX1+q3wHjghZvztfesMppgfFP917KweDAtYA=";
result = Decrypt(passphrase, text);
Console.WriteLine($"Decrypt-4: {text}");
Console.WriteLine($"Decrypt-4: {result}");
Console.WriteLine();

GenerateRandomBytes

OpenSSL の暗号化は、"Salted__xxxxxxxx" の 16 バイトを先頭に付加します。 x の箇所はランダムに生成し、salt として利用します。 同じ文字列でも、毎回暗号文が異なるのは、これが理由です。

DeriveKeyAndIV

SHA256 + PBKDF2 と MD5 による鍵と初期ベクタを生成する箇所です。 MD5 は、stack overflow に下記が記載されていましたが、 まだ正しいコードは投稿されていないようです。

Hash0 = ''
Hash1 = MD5(Hash0 + Password + Salt)
Hash2 = MD5(Hash1 + Password + Salt)
Hash3 = MD5(Hash2 + Password + Salt)

Key = Hash1 + Hash2
IV = Hash3

Encrypt

ランダムに生成した 8文字が salt になっているのと、先頭 16 バイト付加に注意します。

また、Aes.Create() の初期値が下記であるため、Aes.Create().CreateEncryptor() を使って簡潔に記述しています。

aesProvider.Padding = PaddingMode.PKCS7;
aesProvider.Mode = CipherMode.CBC;
aesProvider.BlockSize = 128;
aesProvider.KeySize = 256;  

Decrypt

ランダムに生成した 8 文字を取り出して salt にするのと、先頭 16 バイトをスキップして複合化します。

実行結果

Encrypt-1: U2FsdGVkX1/IMEcoUYTS3hIIoOLOTUA4JPX8ynfjdxYl/7dKvmPWfADXcux6gWpt
Decrypt-1: C#: SHA256 + PBKDF2

Encrypt-2: U2FsdGVkX18Hcilj4MDr3lbWmhiDq8yYDNA/clavnPM=
Decrypt-2: C#: MD5

Decrypt-3: U2FsdGVkX180uM6YCr78gph0+q9msr+1uQ+ypYaJ3OlN8Dvx4dm/LPKfEK/N0ukM
Decrypt-3: Ubuntu:SHA256 + PBKDF2

Decrypt-4: U2FsdGVkX1+q3wHjghZvztfesMppgfFP917KweDAtYA=
Decrypt-4: Ubuntu:MD5

C# SHA256 + PBKDF2 の暗号文を openssl enc コマンドで複合化

上記の C# SHA256 + PBKDF2 で作った暗号文が、openssl enc コマンドで正しく複合化されることを確認します。

$ echo -n 'U2FsdGVkX1/IMEcoUYTS3hIIoOLOTUA4JPX8ynfjdxYl/7dKvmPWfADXcux6gWpt' | openssl enc -d -aes-256-cbc -md sha256 -base64 -A -pass pass:password -pbkdf2
C#: SHA256 + PBKDF2

C# MD5 の暗号文を openssl enc コマンドで複合化

上記の C# MD5 で作った暗号文が、openssl enc コマンドで正しく複合化されることを確認します。

$ echo -n 'U2FsdGVkX18Hcilj4MDr3lbWmhiDq8yYDNA/clavnPM=' | openssl enc -d -aes-256-cbc -md md5 -base64 -A -pass pass:password
*** WARNING : deprecated key derivation used.
Using -iter or -pbkdf2 would be better.
C#: MD5

まとめ

今回の openssl enc コマンドによる暗号化・複合化の C# 化は、情報が局所的で完成形を作り上げるのに苦労しました。 GitHub にある OpenSSLソースコードも見ましたが、難しくて途中で投げ出しました。 時間と興味のある方は、apps/enc.c から追いかけてみてください。(C のポインタと自己参照構造体が理解できていることが前提です)

結局、困ったときの stack overflow 頼みで C#ソースコードを作り上げました。 これで、今回のソースコードをベースにクラス化と例外処理を付けて、冒頭の REST API ツールを C# で実装できそうです。

では、皆さん、良い旅を。

参考サイト

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy