[OutSystems]パスワード付きZipを作成する

以前書いた[OutSystemsでzipファイルを扱う](https://qiita.com/jyunji_watanabe/items/65568643433d76a1cfae)では、OutSystemsにプリインストールされているZip Extensionを使ってZipファイルを作成する方法を確認した。

ただ、この方法だと作成するZipファイルにパスワードをつけることができないように見える。
よって、ちょっと手を入れて、パスワードを付けるI/Fを追加してみる。

# 環境
Personal Environment(Version 11.15.0 (Build 34858))
Service Studio(Version 11.14.16)
Zip Extension(For version 11.15.0)

# Zip Extension
## 基本と使い方
このExtensionはSystem Componentの1つで、OutSystemsをインストールした環境にはデフォルトで入っているはず。
使い方は、以前書いた[OutSystemsでzipファイルを扱う](https://qiita.com/jyunji_watanabe/items/65568643433d76a1cfae)を参照。

## Zipファイルの操作を実現している方法
私のPEに入っているZip Extensionは、内部でMITライセンスの[SharpZipLib](https://github.com/icsharpcode/SharpZipLib)を使っている。バージョンはファイルのプロパティを見る限り、1.1.0.145。

[SharpZipLibのドキュメント](https://icsharpcode.github.io/SharpZipLib/help/api/index.html)を見ると、暗号化周りの情報があるので、これを流用すれば、暗号化してパスワードを付与したzipを作れそう。

# 修正方針の検討
## パスワードを付与する
Zip Extensionは、Zipファイルを作成する際に、SharpZipLibのZipOutputStreamクラスを使っている。
このクラスの[Passwordプロパティ](https://icsharpcode.github.io/SharpZipLib/help/api/ICSharpCode.SharpZipLib.Zip.ZipOutputStream.html#ICSharpCode_SharpZipLib_Zip_ZipOutputStream_Password)を設定すれば、作成したZipファイルにパスワードをかけられそう。

> When set to null or if the password is empty no encryption is performed

とのことなので、このプロパティを設定しないと、暗号化が行われない（そしてもちろんパスワードもかからない？）ようだ。
ZipOutputStreamの初期化時に、同時にPasswordも指定するように改修する。

## 暗号化アルゴリズム
Zipの場合、初期に使用していた暗号化アルゴリズムに、現代では問題が見つかっているらしい。
そこで、この方法で作成したZipファイルに使われる暗号化アルゴリズムを確認しておきたい。

Stack Overflowに[Using SharpZipLib How do I set the encryption algorithm?](https://stackoverflow.com/questions/53164862/using-sharpziplib-how-do-i-set-the-encryption-algorithm)という質問があり、その回答が参考になる。

> As I understand from reading the code and forum posts, EncryptionAlgorithm exists to document the values available in the Zip standard and not as an option for the end user.

各種暗号化アルゴリズムを列挙したenum EncryptionAlgorithmがあるが、これはZipの標準で定められたアルゴリズムを列挙するためのもので、このライブラリで使えるアルゴリズムを示したものではない。

> The encryption algorithms that are actually available to you are AES128 and AES256. You apply the algorithm on each entry by assigning the AESKeySize property.

実際には、AES128かAES256のみが選択可能で、使用するビット数は[ZipEntry.AESKeySize](https://icsharpcode.github.io/SharpZipLib/help/api/ICSharpCode.SharpZipLib.Zip.ZipEntry.html#ICSharpCode_SharpZipLib_Zip_ZipEntry_AESKeySize)で指定する。
わざわざ低いビット数を選ぶ理由もないので、ZipEntryをインスタンス化した後にAESKeySizeに256を設定しておくとする。

# 修正
## Extensionを用意
まずは、Integration StudioでZip Extensionを開く。
開いたら、Fileメニュー　→　Cloneを選択、今回は動作確認するだけなので、名前はデフォルトの「CloneOfZip」としておく。
私のPEで試したところ、Publish時にエラーが発生した。

- 「現在ターゲットされているフレームワークで解決できなかったフレームワーク アセンブリ "System.ValueTuple, Version=4.0.2.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51" に間接的に依存するため、解決できませんでした。".NETFramework,Version=v4.6.1"。」のようなメッセージが出る：Visual Studioでプロジェクトを選択　→　プロパティ　→　開いたダイアログで、Target Framework 4.6.1だったのを4.7.2にしてみる
- 「型または名前空間名 'ICSharpCode' が見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足しています。 」のようなメッセージが出る：nugetから復元したSharpZipLibの参照解決ができていない。Copy Local->nugetできる場所がプロパティに見当たらなかったので、nuget参照を消してbinフォルダのdllを参照させるように変更

## CreateZip Actionのコピーを作る
Input Parameter 「Password」を追加したCreateZip2 Actionを、CreateZip Actionをコピーして作成。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/b60e744e-f572-c448-775c-0a9673f12761.png)

Actionの実装。

``` cs
public void MssCreateZip2(out object ssZipHandle, int ssCompressionLevel, string ssPassword) {
    ssZipHandle = new ZipHandle(ssCompressionLevel, ssPassword);
} // MssCreateZip2
```

ZipHandleクラスのコンストラクタに第2引数Passwordを追加したものを作成する。なお、ZipHandleクラスはExtension内で、SharpZipLibを操作するための処理をまとめたクラス。メンバ変数のpasswordに渡した値を保存しておく。

``` cs
public ZipHandle(int level, string password = "") {
    this.level = level;
    this.password = password;
}
```

以下は、作成するZipファイルに個別のファイルを登録していくAddFile Actionから呼び出されるWriteStream。このメソッドは保存しておいたZipOutputStreamオブジェクトを返す。最初の呼び出し時にZipOutputStreamをインスタンス化しているので、このとき同時にPasswordを設定する処理を追加しておく（outputStream.Password = this.password;とその前のif文）。

``` cs
public ZipOutputStream WriteStream {
    get {
        if (null == outputStream) {
            if (outputMemoryStream != null) {
                outputMemoryStream.Dispose();
            }
            outputMemoryStream = new MemoryStream();
            outputStream = new ZipOutputStream(outputMemoryStream);
            outputStream.UseZip64 = UseZip64.Off;
            if (!string.IsNullOrEmpty(this.password))
                outputStream.Password = this.password;
            outputStream.SetLevel(this.level);
        }
        return outputStream;
    }
}
```

次に、AESのビット指定について。
AddFile Actionの実体にentry.AESKeySize = 256;を追加する。これで、パスワードを指定したとき、AES256で暗号化してくれるようになるはず。

``` cs
public void MssAddFile(object ssZipHandle, string ssFileName, DateTime ssDateTime, byte[] ssFileContent) {
    Crc32 crc = new Crc32();
    ZipOutputStream stream = ((ZipHandle)ssZipHandle).WriteStream;
    ZipEntry entry = new ZipEntry(ssFileName);

    crc.Reset();
    crc.Update(ssFileContent);

    entry.Crc = crc.Value;
    entry.DateTime = ssDateTime;
    entry.IsUnicodeText = true;
    entry.AESKeySize = 256;     // AES暗号化に使うビット数

    stream.PutNextEntry(entry);
    stream.Write(ssFileContent, 0, ssFileContent.Length);
}
```

# 動作確認
上記修正を入れたCloneOfZipをPublishし、Zipの代わりに暗号化処理を行ってみる。
以下の通り処理の流れは、以前の記事で説明したZip Extensionの場合と同じで、違いは今回追加したCreateZip2を使っている点だけ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/79c4acb7-c4b1-3696-4ed0-0a6cbfc334a5.png)


## 7-Zipで暗号化が適用されていることを確認
7-ZipでZipファイルを開くと、各ファイルに適用されている暗号化アルゴリズムを確認できる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/85d363a3-c2f4-341c-1cbd-0012e53d524f.png)

ちなみに、AESKeySizeを指定せずにPasswordだけを指定した場合のZipファイルは以下の通り、ZipCryptoで圧縮されているらしい。この方法は危ないかもしれない。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/ad231f74-dc05-4a0f-bb23-cfc70fe4c036.png)

# ライセンスの検討
パスワード付きZipを作成する方法の調査として色々書いてきたが、System Component（Zip Extension）のソースコード流用がOKかどうかは不明。
OutSystemsに確認するか、もしくは内部で使っているSharpZipLibがMITライセンスなので、同等のものを自分で作るかかな。