PyArmor 〜pythonソースコードを暗号化して納品する楽な方法〜

# はじめに
　受託開発などをしている際、クライアントに対して成果物の納品をしなければならない場面があるかと思います。そんな時例えばSaaSとして機能を提供できれば、クライアント側はAPIを叩いてその機能を利用することになるため、こちら側のソースコードを把握することはもちろんできなくなります。
　だがしかし、それが現実的で無い場面も多いかと思います。そんな時に役立つPyArmorというライブラリが存在します。PyArmorはソースコードを暗号化してくれるライブラリであり、理論上ソースコードの復元ができません。手っ取り早くソースコードの納品を行いたい際に非常に便利なライブラリになります。またただ暗号化するだけでなく、色々なライセンス（使用可能期間、実行デバイスなど）を付与した上で暗号化することも可能です。
　PyArmorについては、ググれば他の記事も出てきますが、その仕様が若干変わっていたため、メモの意味も含め今回は記事としてまとめてみました。
　PyArmorの使用方法については以下の公式ドキュメントを参考にしています。

[PyArmor公式ドキュメント](https://pyarmor.readthedocs.io/en/latest/)

　尚、今回のブログのソースコードについては、　[[GitHub]](https://github.com/spider-man-tm/PyArmor)　に上げています。環境はMacのローカル環境、必要なライブラリはPipfileを参照ください。

# 実際にやってみる

## 暗号化前
　こちらが暗号化前のコードです。[[Single_Module]](https://github.com/spider-man-tm/PyArmor/tree/master/Single_Module)
　画像ファイルを読み込んだ後に訓練済みのVGG16モデルで画像分類＆画像の表示を行っています。

```Single_Module/main.py
import os
import sys
import cv2
import numpy as np
import matplotlib.pyplot as plt
import torch
from torchvision import models, transforms

sys.path.append(os.path.abspath('..'))
import imagenet_class


img = cv2.imread('../baseball.png')
img = cv2.resize(img, (256, 256))
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img_tensor = transforms.ToTensor()(img)
img_tensor = img_tensor.unsqueeze_(0)

model = models.vgg16(pretrained=True)

output = model(img_tensor)
output = np.argmax(output.detach().numpy())

print(imagenet_class.target[str(output)][1])   # ballplayer

plt.imshow(img)
plt.show()
```
![スクリーンショット 2020-08-26 18.31.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323251/c8359371-f016-4653-0b61-16ade24fdb8d.png)

結果も`ballplayer`と正確に分類できていることが分かります。

## 暗号化
　暗号化は以下のコマンドで行います。

```
$ pyarmor obfuscate main.py
```

実行後、直下に`dist`ディレクトリがされます。中身のファイルは以下の通りです。

- pytransform/_pytransform.dylib
    - 実行時に使用されるダイナミックライブラリ（Macだと.dylibファイル、Linuxだと.soファイル）
- dist/main.py
    - 難読化されたファイル

## ライセンスをつけての暗号化
　公式ドキュメントの`Generating License For Obfuscated Scripts`の部分に載っていますが、まず以下のコマンドを実行します。(r001などの名称はご自由に)

```
$ pyarmor licenses --expired 2022-01-01 r001
```

　すると直下に`licenses`ディレクトリ、及び配下に各種ファイルが生成されます。次に先ほどと同様、`pyarmor obfuscate`コマンドでmainファイルを暗号化するのですが、以下の様にオプションをつけて実行します。

```
$ pyarmor obfuscate --with-license licenses/r001/license.lic main.py
```

そうすると使用期限付ライセンスありといった形で暗号化することが可能です。[[Single_Module_Obfuscating_pre]](https://github.com/spider-man-tm/PyArmor/tree/master/Single_Module_Obfuscating_pre)

- 尚、試しに二つ前のコマンドで`--expired 2019-01-01`とすると、既に期限切れのため、暗号化されたmainファイルを実行することができなくなります。
- 上記と同じ様な形でデバイスを指定したライセンスを発行することも可能です。詳細は公式ドキュメントを参照してください。

## 暗号化されたファイルの実行
　暗号化されたmainファイルは通常のpython scriptと同じ様に実行できます。上記までに暗号されたmainファイルは`dist`ディレクトリ直下に存在するため、オリジナルと相対パスが若干違っています。そこでパスを揃えるため　[[Single_Module_Obfuscating]](https://github.com/spider-man-tm/PyArmor/tree/master/Single_Module_Obfuscating)　の様な構成にしておきました。ファイルの置き場を変えた以外は何もいじっていません。試しに以下のコマンドを実行してみます。

```
$ python main.py
```

【結果】

[out] ballplayer

![スクリーンショット 2020-08-26 18.31.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323251/d9ec9b79-09f9-a017-37d3-2bebfac21e5d.png)

暗号化前と、全く同じ結果であることが確認できます。また試しに肝心のmainファイルがどの様になっているのか見てみます。

```
$ cat main.py

from pytransform import pyarmor_runtime
pyarmor_runtime()
__pyarmor__(__name__, __file__, b'\x50\x59\x41\x52 ...（以下省略）

```

　この様な形で正常に暗号化されていることが分かります。尚、先ほど箇条書きで書きましたが、このメインファイルが実行されるには同時に生成されるダイナミックライブラリが必ず必要になってきます。

## 異なるディレクトリ配下の.pyファイルを含めて難読化する場合
　先ほどまでの方法の場合、デフォルトで直下にある`.py`ファイル全てを暗号化します。もし、サブディレクトリまで含めた`.py`ファイル全てを再帰的に暗号化したい場合以下のオプションをつけて実行します。

```
$ pyarmor obfuscate --recursive main.py
```

　試しにやってみます。
　[[Whole_Module]](https://github.com/spider-man-tm/PyArmor/tree/master/Whole_Module)　が暗号化前のソースコードですが、先ほどとやっていることは同じです。（ただ、ファイルを分けただけ）
　直下の`main.py`はサブディレクト下の`model/model.py`を読み込んでいます。

```Whole_Module/model/model.py
from torch import nn
from torchvision import models


class SampleModel(nn.Module):
    def __init__(self):
        super(SampleModel, self).__init__()
        self.backborn = models.vgg16(pretrained=True)

    def forward(self, x):
        x = self.backborn(x)
        return x

```

```Whole_Module/main.py
import os
import sys
import cv2
import numpy as np
import matplotlib.pyplot as plt
import torch
from torchvision import transforms

from model.model import SampleModel
sys.path.append(os.path.abspath('..'))
import imagenet_class


img = cv2.imread('../baseball.png')
img = cv2.resize(img, (256, 256))
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img_tensor = transforms.ToTensor()(img)
img_tensor = img_tensor.unsqueeze_(0)

model = SampleModel()

output = model(img_tensor)
output = np.argmax(output.detach().numpy())

print(imagenet_class.target[str(output)][1])

plt.imshow(img)
plt.show()

```

　ここで、先ほどのコマンドを実行すると`dist`ディレクトリが新たに生成されると共に、暗号化された`main.py`、及び`model.py`、またダイナミックライブラリが生成されます。　[[Whole_Module_Obfuscating_pre]](https://github.com/spider-man-tm/PyArmor/tree/master/Whole_Module_Obfuscating_pre)。　ただし、このままだと先ほどと同じ理由（画像ファイルの相対パスが異なる）で実行できないため、生成されたファイルを移動しておきました。こちらも先ほど同様問題なく実行することが可能です。　[[Whole_Module_Obfuscating]](https://github.com/spider-man-tm/PyArmor/tree/master/Whole_Module_Obfuscating)