OpenSSL互換のAES-XXX-CBC暗号化・復号化をC#で試した時のメモ

# はじめに

パスワードを暗号化してYAMLファイルに持たせて、javaやrubyおよびC#で作ったツールで利用したいということでぞれぞれの言語で実装方法を調べた時のメモの中から、今回はC#版について記載します。

# ソースコード

DLLを作成する場合は、「クラスライブラリ」としてプロジェクトを作成します。

また、VBScriptで使用する場合は、プロジェクトのプロパティを開き、アセンブリ情報の箇所で「アセンブリを COM参照可能にする(M)」にチェックしておく必要があります。
※プロジェクトのプロパティを開くとビルドの箇所に「COM相互運用機能の登録(C)」といったチェックボックスが存在しますがここはチェックせずに手動で「regasm.exe」を使ってアセンブリ登録する手順とします。

```csharp:Projects\CryptAesLib\CryptAesLib\CryptAesLib.cs
using System;
using System.Text;
using System.IO;
using System.Security.Cryptography;
using System.Runtime.InteropServices;

namespace CryptAesLib
{
  [ComVisible(true)]
  [ClassInterface(ClassInterfaceType.AutoDual)]
  public class ClsCrypt
  {
    private String _strErrMsg = "";
    private String _strErrDmp = "";
    private String _strResult = "";
    private int _intKeySize = 128;
    private int _intBlockSize = 128;
    private Boolean _blnIsVerbose = false;
    private byte[] _baKey;
    private byte[] _baIv;
    private static Random _objRndm = new Random();

    public ClsCrypt()
    {
    }

    [ComVisible(true)]
    public String strDefKey { get { return "default_secret_key#!!"; } }
    [ComVisible(true)]
    public String strErrMsg { get { return _strErrMsg; } }
    [ComVisible(true)]
    public String strErrDmp { get { return _strErrDmp; } }
    [ComVisible(true)]
    public String strResult { get { return _strResult; } }
    [ComVisible(true)]
    public int intKeySize { get { return _intKeySize; } set { _intKeySize = value; } }
    [ComVisible(true)]
    public int intBlockSize { get { return _intBlockSize; } set { _intBlockSize = value; } }
    [ComVisible(true)]
    public Boolean blnIsVerbose { get { return _blnIsVerbose; } set { _blnIsVerbose = value; } }

    [ComVisible(true)]
    public Boolean encrypt(String strKey, String strPlain)
    {
      Boolean blnIsOk = true;
      byte[] baEnc;
      String strSalt = null;
      try
      {
        strSalt = this.getRandomString(8);
        byte[] baKey = Encoding.UTF8.GetBytes(strKey);
        byte[] baPlain = Encoding.UTF8.GetBytes(strPlain);
        byte[] baPrefix = Encoding.ASCII.GetBytes("Salted__" + strSalt);
        byte[] baSalt = Encoding.ASCII.GetBytes(strSalt);
        if (!getOpenSSLKey(baKey, baSalt)) return false;
        Rijndael objAes = Rijndael.Create();
        objAes.Mode = CipherMode.CBC;
        objAes.Padding = PaddingMode.PKCS7;
        objAes.KeySize = _intKeySize;
        objAes.BlockSize = _intBlockSize;
        objAes.Key = _baKey;
        objAes.IV = _baIv;
        using (MemoryStream objMemStrm = new MemoryStream())
        using (CryptoStream objCptStrm = new CryptoStream(objMemStrm, objAes.CreateEncryptor(), CryptoStreamMode.Write))
        {
          objCptStrm.Write(baPlain, 0, baPlain.Length);
          objCptStrm.FlushFinalBlock();
          baEnc = objMemStrm.ToArray();
        }
        byte[] baEncWithSalt = new byte[baPrefix.Length + baEnc.Length];
        Buffer.BlockCopy(baPrefix, 0, baEncWithSalt, 0, baPrefix.Length);
        Buffer.BlockCopy(baEnc, 0, baEncWithSalt, baPrefix.Length, baEnc.Length);
        _strResult = Convert.ToBase64String(baEncWithSalt);
      }
      catch (Exception objExcptn)
      {
        blnIsOk = false;
        _strErrMsg = objExcptn.Message;
        _strErrDmp = objExcptn.StackTrace;
      }
      return blnIsOk;
    }

    [ComVisible(true)]
    public Boolean decrypt(String strKey, String strBase64Enc)
    {
      Boolean blnIsOk = true;
      byte[] baSalt = new byte[8];
      byte[] baPlain;
      try
      {
        byte[] baKey = Encoding.UTF8.GetBytes(strKey);
        byte[] baEncWithSalt = Convert.FromBase64String(strBase64Enc);
        for (int i = 0; i < 8; i++)
        {
          baSalt[i] = baEncWithSalt[(8) + i];
        }
        byte[] baEnc = new byte[baEncWithSalt.Length - 16];
        for (int i = 0; i < baEncWithSalt.Length - 16; i++)
        {
          baEnc[i] = baEncWithSalt[16 + i];
        }
        if (!getOpenSSLKey(baKey, baSalt)) return false;
        Rijndael objAes = Rijndael.Create();
        objAes.Mode = CipherMode.CBC;
        objAes.Padding = PaddingMode.PKCS7;
        objAes.KeySize = _intKeySize;
        objAes.BlockSize = _intBlockSize;
        objAes.Key = _baKey;
        objAes.IV = _baIv;
        using (MemoryStream objMemStrm = new MemoryStream())
        using (CryptoStream objCptStrm = new CryptoStream(objMemStrm, objAes.CreateDecryptor(), CryptoStreamMode.Write))
        {
          objCptStrm.Write(baEnc, 0, baEnc.Length);
          objCptStrm.FlushFinalBlock();
          baPlain = objMemStrm.ToArray();
        }
        _strResult = Encoding.UTF8.GetString(baPlain);
      }
      catch (Exception objExcptn)
      {
        blnIsOk = false;
        _strErrMsg = objExcptn.Message;
        _strErrDmp = objExcptn.StackTrace;
      }
      return blnIsOk;
    }

    private Boolean getOpenSSLKey(byte[] baKey, byte[] baSalt)
    {
      Boolean blnIsOk = true;
      MD5 objMd5 = MD5.Create();
      byte[] baHash1 = new byte[16];
      byte[] baHash2 = new byte[16];
      byte[] baPreKey = new byte[baKey.Length + baSalt.Length];
      byte[] baPreIV = new byte[16 + baPreKey.Length];
      byte[] baPreHash2 = new byte[16 + baPreKey.Length];
      // 128
      //   Key   = MD5(暗号キー + SALT)
      //   IV    = MD5(Key + 暗号キー + SALT)
      // 192,256
      //   Hash0 = ''
      //   Hash1 = MD5(Hash0 + 暗号キー + SALT)
      //   Hash2 = MD5(Hash1 + 暗号キー + SALT)
      //   Hash3 = MD5(Hash2 + 暗号キー + SALT)
      //   Key   = Hash1 + Hash2
      //   IV    = Hash3
      try
      {
        Buffer.BlockCopy(baKey, 0, baPreKey, 0, baKey.Length);
        Buffer.BlockCopy(baSalt, 0, baPreKey, baKey.Length, baSalt.Length);
        if (128 == intKeySize)
        {
          _baKey = objMd5.ComputeHash(baPreKey);
        }
        else
        {
          baHash1 = objMd5.ComputeHash(baPreKey);
          Buffer.BlockCopy(baHash1, 0, baPreHash2, 0, baHash1.Length);
          Buffer.BlockCopy(baPreKey, 0, baPreHash2, baHash1.Length, baPreKey.Length);
          baHash2 = objMd5.ComputeHash(baPreHash2);
          _baKey = new byte[32];
          Buffer.BlockCopy(baHash1, 0, _baKey, 0, baHash1.Length);
          Buffer.BlockCopy(baHash2, 0, _baKey, baHash1.Length, baHash2.Length);
        }
        if (128 == intKeySize)
        {
          Buffer.BlockCopy(_baKey, 0, baPreIV, 0, _baKey.Length);
          Buffer.BlockCopy(baPreKey, 0, baPreIV, _baKey.Length, baPreKey.Length);
        }
        else
        {
          Buffer.BlockCopy(baHash2, 0, baPreIV, 0, baHash2.Length);
          Buffer.BlockCopy(baPreKey, 0, baPreIV, baHash2.Length, baPreKey.Length);
        }
        _baIv = objMd5.ComputeHash(baPreIV);
      }
      catch (Exception objExcptn)
      {
        blnIsOk = false;
        _strErrMsg = objExcptn.Message;
        _strErrDmp = objExcptn.StackTrace;
      }
      finally
      {
        objMd5.Clear();
        objMd5 = null;
      }
      return blnIsOk;
    }

    private string getRandomString(int intLength)
    {
      byte[] baCharList = new byte[] {
        0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a,
        0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74,
        0x75, 0x76, 0x77, 0x78, 0x79, 0x7a
      };
      byte[] baRandom = new byte[intLength];
      for (int i = 0; i < baRandom.Length; i++)
      {
        baRandom[i] = baCharList[_objRndm.Next(26)];
      }
      return Encoding.UTF8.GetString(baRandom);
    }
  }
}
```

単体テストプロジェクト

鍵長が128/192/256ビットそれぞれで問題なく暗号化⇒復号化できることを確認しておきます。

```csharp:Projects\CryptAesLib\UnitTestProject1\UnitTest_CryptAesLib.cs
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using CryptAesLib;

namespace UnitTestProject1
{
  [TestClass]
  public class UnitTest_CryptAesLib
  {
    private ClsCrypt _ClsCrypt = null;

    [TestInitialize]
    public void TestInitialize()
    {
      _ClsCrypt = new ClsCrypt();
    }

    [TestMethod]
    public void encrypt_decrypt_128bitで暗号化および復号化が正常実行されること()
    {
      String strKey = "secret_key_is_here!!!";
      String strExpect = "パスワードをここに記載";
      _ClsCrypt.encrypt(strKey, strExpect);
      _ClsCrypt.decrypt(strKey, _ClsCrypt.strResult);
      Assert.AreEqual(_ClsCrypt.strResult, strExpect);
    }

    [TestMethod]
    public void encrypt_decrypt_192itで暗号化および復号化が正常実行されること()
    {
      String strKey = "secret_key_is_here!!!";
      String strExpect = "パスワードをここに記載";
      _ClsCrypt.intKeySize = 192;
      _ClsCrypt.encrypt(strKey, strExpect);
      _ClsCrypt.decrypt(strKey, _ClsCrypt.strResult);
      Assert.AreEqual(_ClsCrypt.strResult, strExpect);
    }

    [TestMethod]
    public void encrypt_decrypt_256bitで暗号化および復号化が正常実行されること()
    {
      String strKey = "secret_key_is_here!!!";
      String strExpect = "パスワードをここに記載";
      _ClsCrypt.intKeySize = 256;
      _ClsCrypt.encrypt(strKey, strExpect);
      _ClsCrypt.decrypt(strKey, _ClsCrypt.strResult);
      Assert.AreEqual(_ClsCrypt.strResult, strExpect);
    }
  }
}
```

# VBScriptの場合

VBScriptで使用するには前もって「regasm.exe」を使ってアセンブリ登録が必要です。
このとき64bit OSであればcscript.exeが64bitと32bitで別々に存在するようにregasm.exeも64bitと32bit用のものがそれぞれ存在します。

#### ■64bit cscript.exeを使用する場合
　C:\Windows\Microsoft.NET\Framework64\v4.0.30319\regasm.exe
#### ■32bit cscript.exeを使用する場合
　C:\Windows\Microsoft.NET\Framework\v4.0.30319\regasm.exe

### アセンブリ登録（管理者として実行）

```bat
C:\> C:\Windows\Microsoft.NET\Framework64\v4.0.30319\regasm.exe /codebase C:\Tool\Infra\dll\CryptAesLib.dll
Microsoft .NET Framework Assembly Registration Utility 4.6.1038.0
for Microsoft .NET Framework Version 4.6.1038.0
Copyright (C) Microsoft Corporation.  All rights reserved.

RegAsm : warning RA0000 : 署名されていないアセンブリを /codebase を使用して登録すると、同じコンピューターにインストールされるその他のアプリケーションとの競合が生じる可能性があります。/codeb
ase スイッチは署名されたアセンブリのみに使用できます。アセンブリに厳密な名前を付けて、再登録してください。
型は正常に登録されました。
C:\>
```

### アセンブリ登録解除（管理者として実行）

```bat
C:\> C:\Windows\Microsoft.NET\Framework64\v4.0.30319\regasm.exe /u C:\Tool\Infra\dll\CryptAesLib.dll
Microsoft .NET Framework Assembly Registration Utility 4.6.1038.0
for Microsoft .NET Framework Version 4.6.1038.0
Copyright (C) Microsoft Corporation.  All rights reserved.

型は正常に登録が解除されました。
C:\>
```

### VBScriptファイルの作成

暗号化⇒復号化の例です。

```
Dim myObj
WScript.StdOut.WriteLine "01.オブジェクト生成します"
Set myObj = CreateObject("CryptAesLib.ClsCrypt")

strSecretKey = "secret_key_123###"
strPassword = "健康診断でメタボ予備軍に判定されダイエット中"

WScript.StdOut.WriteLine "鍵 = " & strSecretKey
WScript.StdOut.WriteLine "パスワード = " & strPassword

WScript.StdOut.WriteLine "10.暗号化"
If myObj.encrypt(strSecretKey, strPassword) Then
  WScript.StdOut.WriteLine "11.OK"
  WScript.StdOut.WriteLine myObj.strResult
Else
  WScript.StdOut.WriteLine "12.NG"
  WScript.StdOut.WriteLine myObj.strErrMsg
  WScript.Quit 20
End If

WScript.StdOut.WriteLine "20.復号化"
If myObj.decrypt(strSecretKey, myObj.strResult) Then
  WScript.StdOut.WriteLine "21.OK"
  WScript.StdOut.WriteLine myObj.strResult
Else
  WScript.StdOut.WriteLine "22.NG"
  WScript.StdOut.WriteLine myObj.strErrMsg
  WScript.Quit 20
End If
WScript.Quit 0
```

### 実行確認

実行してみます。

```bat
C:\> cscript  C:\sample\test.vbs
Microsoft (R) Windows Script Host Version 5.812
Copyright (C) Microsoft Corporation. All rights reserved.

01.オブジェクト生成します
鍵 = secret_key_123###
パスワード = 健康診断でメタボ予備軍に判定されダイエット中
10.暗号化
11.OK
U2FsdGVkX19vcXJuYnh4dOMFDf3ZINHqymTGBdBjAb9dUekWwxraFdCZIhVr7nkOBtftlhSLPKeefKKvsmfy5VNp2YzfImeGwHJaCUQmg/iZ/PQ8FIbR5ubDW5g4Atns
20.復号化
21.OK
健康診断でメタボ予備軍に判定されダイエット中
C:\>
```

問題なく、暗号化⇒復号化ができました。

# Powershellの場合

ここではコマンドラインから実行してみます。

```bat
PS C:\> $strPathFDll = "C:\Tool\Infra\dll\CryptAesLib.dll"
PS C:\> [Reflection.Assembly]::LoadFile($strPathFDll) | out-null
PS C:\> $objCrypt= New-Object CryptAesLib.ClsCrypt
PS C:\> $strSecretKey = "secret_key_123###"
PS C:\> $strPassword = "健康診断でメタボ予備軍に判定されダイエット中"
```

まずは128bitの鍵長で暗号化を実行します。

```bat
PS C:\> $objCrypt.encrypt($strSecretKey, $strPassword)
True
PS C:\> Write-Output $objCrypt.strResult
U2FsdGVkX19rdWxsZGNoaee4GUf/W8a5p2JmP6LfI6lKgMRD6HzRp1P7DumcCVyTGl5rZqIQtW6TM5XNZ/sK7P2Reo377q7E73yEm6ScPjr15smIs9u3WsiKBreh5ham
PS C:\>
```

復号化を実行します。

```bat
PS C:\> $objCrypt.decrypt($strSecretKey, $objCrypt.strResult)
True
PS C:\> Write-Output $objCrypt.strResult
健康診断でメタボ予備軍に判定されダイエット中
PS C:\>
```

192bitでの暗号化と復号化

```bat
PS C:\> $objCrypt.intKeySize = 192
PS C:\> $objCrypt.encrypt($strSecretKey, $strPassword)
True
PS C:\> Write-Output $objCrypt.strResult
U2FsdGVkX19waW9nbmxndMEalkVlBITJTQkXlttHfAjFcDqf06qPZxAsLA4a2bZSV1ULKoZ3niExGIby+PDEI6EAhZ105fcHtaOtN9E4DZ6tH4/Vcssn4IQyx5JL5zbZ
PS C:\> $objCrypt.decrypt($strSecretKey, $objCrypt.strResult)
True
PS C:\> Write-Output $objCrypt.strResult
健康診断でメタボ予備軍に判定されダイエット中
PS C:\>
```

256bitでの暗号化と復号化

```bat
PS C:\> $objCrypt.intKeySize = 256
PS C:\> $objCrypt.encrypt($strSecretKey, $strPassword)
True
PS C:\> Write-Output $objCrypt.strResult
U2FsdGVkX190eHJoZm15Z1S93tOEPJXYXxAM5jbcbZ7Bpvve/qPgodyzGmbloz7yxflZRD+OV5exXPhioBJ43RlpNYStTlcAQg/cN9a7lzzEK3hrXdaltFxtopAzIKlk
PS C:\> $objCrypt.decrypt($strSecretKey, $objCrypt.strResult)
True
PS C:\> Write-Output $objCrypt.strResult
健康診断でメタボ予備軍に判定されダイエット中
PS C:\>
```

# OpenSSLコマンドと互換性の確認

OpenSSLで暗号化した文字列をPowershell(C#クラスライブラリ)で復号化してみます。

```
[hostname ~]$ echo "健康診断でメタボ予備軍に判定されダイエット中" | openssl enc -aes-256-cbc -e -base64 -pass pass:secret_key_123### | tr -d '\n'
U2FsdGVkX193KTPnMxTdIWRd7YPX03jS+iysqTDabM5GL5I0iZY8rwLdct2wKcgcH5rMZPf3gyRATgPqoa2ExSituxr3TpGZNKqLMOggeba/qBdSIQl5DmpC1BusjE0h[hostname ~]$
```

```bat
PS C:\> $strPathFDll = "C:\Tool\Infra\dll\CryptAesLib.dll"
PS C:\> [Reflection.Assembly]::LoadFile($strPathFDll) | out-null
PS C:\> $objCrypt= New-Object CryptAesLib.ClsCrypt
PS C:\> $strSecretKey = "secret_key_123###"
PS C:\> $objCrypt.intKeySize = 256
PS C:\> $strPassword = "U2FsdGVkX193KTPnMxTdIWRd7YPX03jS+iysqTDabM5GL5I0iZY8rwLdct2wKcgcH5rMZPf3gyRATgPqoa2ExSituxr3TpGZNKqLMOggeba/qBdSIQl5DmpC1BusjE0h"
PS C:\> $objCrypt.decrypt($strSecretKey, $strPassword)
True
PS C:\> Write-Output $objCrypt.strResult
健康診断でメタボ予備軍に判定されダイエット中

PS C:\>
```

無事に復号化できました。

次に、上でPowershell(C#クラスライブラリ)で暗号化した文字列をOpenSSLで復号化してみます。
最初に64bit毎に改行を付与します。

```
[hostname ~]$ cat enc.txt
U2FsdGVkX190eHJoZm15Z1S93tOEPJXYXxAM5jbcbZ7Bpvve/qPgodyzGmbloz7y
xflZRD+OV5exXPhioBJ43RlpNYStTlcAQg/cN9a7lzzEK3hrXdaltFxtopAzIKlk
[hostname ~]$
```

復号化してみます。

```
[hostname ~]$ cat enc.txt | openssl enc -aes-256-cbc -d -base64 -pass pass:secret_key_123###
健康診断でメタボ予備軍に判定されダイエット中[hostname ~]$
```

無事に復号化できました。