こんにちは、たびとです。
とある 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 を使ったコンソールアプリです。できるだけ簡潔に記載しました。
下記の暗号化・複合化をプログラムの最後に直接記載しています。
- C# 版 SHA256 + PBKDF2 の暗号化・複合化
- C# 版 MD5 の暗号化・複合化
- openssl enc コマンド SHA256 + PBKDF2 暗号文を複合化 (上記結果を利用)
- 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# で実装できそうです。
では、皆さん、良い旅を。