はじめに
受託開発などをしている際、クライアントに対して成果物の納品をしなければならない場面があるかと思います。そんな時例えばSaaSとして機能を提供できれば、クライアント側はAPIを叩いてその機能を利用することになるため、こちら側のソースコードを把握することはもちろんできなくなります。
だがしかし、それが現実的で無い場面も多いかと思います。そんな時に役立つPyArmorというライブラリが存在します。PyArmorはソースコードを暗号化してくれるライブラリであり、理論上ソースコードの復元ができません。手っ取り早くソースコードの納品を行いたい際に非常に便利なライブラリになります。またただ暗号化するだけでなく、色々なライセンス(使用可能期間、実行デバイスなど)を付与した上で暗号化することも可能です。
PyArmorについては、ググれば他の記事も出てきますが、その仕様が若干変わっていたため、メモの意味も含め今回は記事としてまとめてみました。
PyArmorの使用方法については以下の公式ドキュメントを参考にしています。
尚、今回のブログのソースコードについては、 [GitHub] に上げています。環境はMacのローカル環境、必要なライブラリはPipfileを参照ください。
実際にやってみる
暗号化前
こちらが暗号化前のコードです。[Single_Module]
画像ファイルを読み込んだ後に訓練済みのVGG16モデルで画像分類&画像の表示を行っています。
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()
結果も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]
- 尚、試しに二つ前のコマンドで
--expired 2019-01-01
とすると、既に期限切れのため、暗号化されたmainファイルを実行することができなくなります。 - 上記と同じ様な形でデバイスを指定したライセンスを発行することも可能です。詳細は公式ドキュメントを参照してください。
暗号化されたファイルの実行
暗号化されたmainファイルは通常のpython scriptと同じ様に実行できます。上記までに暗号されたmainファイルはdist
ディレクトリ直下に存在するため、オリジナルと相対パスが若干違っています。そこでパスを揃えるため [Single_Module_Obfuscating] の様な構成にしておきました。ファイルの置き場を変えた以外は何もいじっていません。試しに以下のコマンドを実行してみます。
$ python main.py
【結果】
[out] ballplayer
暗号化前と、全く同じ結果であることが確認できます。また試しに肝心の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] が暗号化前のソースコードですが、先ほどとやっていることは同じです。(ただ、ファイルを分けただけ)
直下のmain.py
はサブディレクト下の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
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]。 ただし、このままだと先ほどと同じ理由(画像ファイルの相対パスが異なる)で実行できないため、生成されたファイルを移動しておきました。こちらも先ほど同様問題なく実行することが可能です。 [Whole_Module_Obfuscating]