標準Windowsで追加インストールなしの暗号化ソリューションを作ってみた

# はじめに

いつの頃からか、Windowsからパスワード付きZip作成の機能がなくなってしまっています。
そのため、安全にファイルを取引先などに渡すために、追加のソリューションの導入が必要です。

ところが、相手先によっては会社で許可されているソフトウェア以外は勝手にインストールできない場合があります。
あるいは、ITリテラシーの低い取引先または担当者の場合、こちらの推奨するアーカイブソフトを導入してもらうのも、またひと苦労かかります。

つまり、Windowsにファイル受け渡しのための暗号化（自分のストレージを守るディスク暗号化はありますが）ツールが標準で用意されていない、というところに問題があるわけです。

そこで、タイトルにあるように（実際は「ソリューション」という程大げさなものではないのですが）作ってみました。

# AES暗号化をWindowsの標準機能だけで実現するツールを作った

このツールは、以下の要件を満たします。

- AES暗号化によるパスワード付きZip以上の安全性
- 十分な桁数の堅牢なパスワードをツールが自動生成
- Windows10なら追加インストールなしで動作（相手にbatファイルを渡す必要はある）
- MacやLinuxでもOpenSSLコマンドで本AES暗号とは互換性あり（つまりMacやLinuxともやり取り可能）

ツールの実体としては、ファイルの暗号化に使うenc.batと復号に使うdec.batという、２つのbatファイル（テキストファイル）になります。

```enc.bat
@echo off
set Args=%*
set Args=%Args:"=\"%
@powershell -NoProfile -ExecutionPolicy Unrestricted "&([scriptblock]::create((gc \"%~f0\"|?{$_.readcount -gt 7})-join\"`n\"))" '%Args%'
goto:eof

#-----------------------------------------------------------------

function aes_encrypt($password, $filepath) {
  $plainText = [Convert]::ToBase64String([System.IO.File]::ReadAllBytes($filepath))
  $salt = 0..7 | % { Get-Random -Maximum 255 }
  $md5 = [Security.Cryptography.MD5]::Create()
  $seed = [Text.Encoding]::UTF8.GetBytes($password) + $salt
  $kiv = $md5.ComputeHash($seed)
  $kiv += $md5.ComputeHash($kiv + $seed)
  $md5.Dispose()
  $aes = New-Object Security.Cryptography.AesManaged
  $aes.Key = $kiv[0..15]
  $aes.IV = $kiv[16..31]
  $aes.BlockSize = 128
  $aes.Mode = [Security.Cryptography.CipherMode]::CBC
  $aes.Padding = [Security.Cryptography.PaddingMode]::PKCS7
  $encryptor = $aes.CreateEncryptor()
  $plainBytes = [Text.Encoding]::UTF8.GetBytes($plainText)
  $outFilepath = $filepath + ".aes"
  [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("Salted__") + $salt + $encryptor.TransformFinalBlock($plainBytes, 0, $plainBytes.Length)) | Out-File $outFilepath -Encoding utf8
  $encryptor.Dispose()
  $aes.Dispose()
}

[void]([System.Reflection.Assembly]::LoadWithPartialName("Microsoft.VisualBasic"))
$path = [string]$args;

Add-type -AssemblyName System.Web
Add-Type -AssemblyName Microsoft.VisualBasic
$defaultPassword = [System.Web.Security.Membership]::GeneratePassword(20,3)
$password = [Microsoft.VisualBasic.Interaction]::InputBox('Enter password', 'Password', $defaultPassword)

$parser = [Microsoft.VisualBasic.FileIO.TextFieldParser]::new([System.IO.MemoryStream]::new([System.Text.Encoding]::Default.GetBytes($path)),[System.Text.Encoding]::Default);
$parser.TextFieldType=[Microsoft.VisualBasic.FileIO.FieldType]::Delimited;
$parser.SetDelimiters(" ");
$parser.ReadFields() | % {
        $path = $_;
        aes_encrypt $password $path
        $inputPath = $path + ".aes"
        $outputPath = $path + ".zip"
        Compress-Archive -Path $inputPath -DestinationPath $outputPath -Force
        Remove-Item $inputPath
}
```

```dec.bat
@echo off
set Args=%*
set Args=%Args:"=\"%
@powershell -NoProfile -ExecutionPolicy Unrestricted "&([scriptblock]::create((gc \"%~f0\"|?{$_.readcount -gt 7})-join\"`n\"))" '%Args%'
goto:eof

#-----------------------------------------------------------------

function aes_decrypt($password, $filepath) {
  $txt = Get-Content $filepath
  $encryptedText = [Convert]::FromBase64String($txt)
  $salt = $encryptedText[8..15]
  $md5 = [Security.Cryptography.MD5]::Create()
  $seed = [Text.Encoding]::UTF8.GetBytes($password) + $salt
  $kiv = $md5.ComputeHash($seed)
  $kiv += $md5.ComputeHash($kiv + $seed)
  $md5.Dispose()
  $aes = New-Object Security.Cryptography.AesManaged
  $aes.Key = $kiv[0..15]
  $aes.IV = $kiv[16..31]
  $aes.BlockSize = 128
  $aes.Mode = [Security.Cryptography.CipherMode]::CBC
  $aes.Padding = [Security.Cryptography.PaddingMode]::PKCS7
  $decryptor = $aes.CreateDecryptor()
  $result = [Text.Encoding]::UTF8.GetString($decryptor.TransformFinalBlock($encryptedText[16..($encryptedText.Length - 1)], 0, $encryptedText.Length - 16))
  $originalFilepath = ($filepath -split ".aes")[0]
  $splitted = $originalFilepath.Split(".")
  $ext = $splitted[-1]
  $path_wo_ext = $splitted[0]
  $outFilepath = $path_wo_ext + "_decrypted." + $ext
  [System.IO.File]::WriteAllBytes($outFilepath, [Convert]::FromBase64String($result))
  [Console]::ReadKey() | Out-Null
  $decryptor.Dispose()
  $aes.Dispose()
}


[void]([System.Reflection.Assembly]::LoadWithPartialName("Microsoft.VisualBasic"))
$path = [string]$args;

Add-Type -AssemblyName Microsoft.VisualBasic
$password = [Microsoft.VisualBasic.Interaction]::InputBox('Enter password', 'Password', "")

$parser = [Microsoft.VisualBasic.FileIO.TextFieldParser]::new([System.IO.MemoryStream]::new([System.Text.Encoding]::Default.GetBytes($path)),[System.Text.Encoding]::Default);
$parser.TextFieldType=[Microsoft.VisualBasic.FileIO.FieldType]::Delimited;
$parser.SetDelimiters(" ");
$parser.ReadFields() | % {
        $path = $_;
 	$outputPath = $path.Split(".")[0] + "." + $path.Split(".")[1] + ".aes"
	Expand-Archive -Path $path -DestinationPath ".\" -Force
        aes_decrypt $password $outputPath
}
```

# 使い方

1. enc.batに暗号化したいファイルをドラッグ＆ドロップします。パスワード入力欄が出るので、任意のパスワード文字列を入力するか、 デフォルトで複雑なパスワードが最初から入っているので、それをメモしておきます。 OKをクリックすると、ファイルが暗号化されます。 （暗号化する際、データサイズ削減のためZip圧縮もかけるため、暗号化後の拡張子は.zipになります）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12753/d0403240-705a-f76b-bce4-50627e736af2.png)

2. 暗号化されたファイル(Zipファイル)とdec.batを相手に送ります。 また、パスワードを安全な方法で相手に伝達します（別経路で送るか、公開鍵暗号を使う）

3. 暗号化されたファイルを受け取った側は、dec.batに暗号化されたZipファイルをドラッグ＆ドロップします。 パスワード入力欄に、伝達されたパスワードを入力します。すると、復号されたファイルが生成されます。


# Mac & Linuxでの互換コマンド  
このAES暗号化ツールと互換性のあるUNIXコマンドを以下に示します。

```bash
暗号化するコマンド(Linux&Mac共通)
$ bash -c "base64 movie.mp4 | openssl enc -e -aes-128-cbc -md md5 -a -pass pass:'password' > movie.mp4.aes && zip movie.mp4.zip movie.mp4.aes"

復号化するコマンド
(Linuxの場合)
$ bash -c "unzip ./movie.mp4.zip && sed 's/^[^a-zA-z0-9+/=]//g' ./movie.mp4.aes | base64 -d | openssl enc -d -aes-128-cbc -md md5 -pass pass:'password' | base64 -d > decrypted.mp4"

(Macの場合)
$ bash -c "unzip ./movie.mp4.zip && sed 's/^[^a-zA-z0-9+/=]//g' ./movie.mp4.aes | base64 -D | openssl enc -d -aes-128-cbc -md md5 -pass pass:'password' | base64 -D > decrypted.mp4"
(base64の-dオプションが、Macの場合-Dと大文字になっています)
```

（"movie.mp4"の文字列部分は実際のファイル名に置き換えること）
（'password'の文字列部分がパスワードになるのでコマンド実行時には複雑なパスワードに置き換えること）

# パスワードをいかに安全に送るか

AESのような共通鍵暗号の悩みどころは、鍵（パスワード）をいかに安全に相手に渡すかという「鍵配送問題」です。
公開鍵暗号はこの問題に対するソリューションです。

@KEINOS さんが、Githubの公開鍵をうまく活用した便利なソリューションを提供してくださっています。こちらを利用してAESパスワードを暗号化して送るとなお良いでしょう。

[GitHub の公開鍵でファイルを暗号化する（またはそのスクリプト）](https://qiita.com/KEINOS/items/2abce1e5b15d799ac6d7)

もっとも、こうしたツールを使えるリテラシーを持っている方は非エンジニアでは少ないと思いますので、そういう人たち向けには別経路で平文でパスワードを送ることになるんでしょうか。心配ですがしょうがないですね。ちなみにメールを複数回に分けて、は当然別経路とは言いません。

# 謝辞  
本ツールの作成にあたり、多くの知見やコードを以下の記事より拝借しております。
私はこれらの知見やサンプルコードを接着して作っただけなので、本当にすごいのは私ではなく、ここに挙げた記事を書かれた方々です。

- [OpenSSL で暗号化したデータを PowerShell で復号する](https://qiita.com/c-yan/items/3f0d5079ff6db154b998)

- [Powershellでバッチファイル１ファイル＆複数ファイル処理](http://www.gstudio.me/blog/2018/04/powershell-1.html)

- [Create Input Box PowerShell](https://itfordummies.net/2016/05/09/create-input-box-powershell/)

- [Windowsでpowershellを使用しランダムなパスワードを生成する](https://monaski.hatenablog.com/entry/2015/09/29/202421)