GAS(Google Apps Script)のライブラリーコードを隠蔽する

以前はGAS(Google Apps Script)のライブラリーは隠蔽されていた記憶がかすかに残っています。少なくとも、パブリックメソッドとプライベートメソッドを書き分けて、関数の公開と非公開を選択出来ていたことは覚えているので、即ち、やっぱりライブラリー全体のコードは隠蔽されていたような気がするのです。
昔のことはこの際どちらでも良いのですが、本記事の目的としてはライブラリーとして公開するライブラリー自体のコードを隠蔽し、ライブラリーの利用者や第三者から解読を不可とすることにあります。
研究記事となりますので、実際は何らかの方法で解読できてしまったという場合にも責任は負いかねますのでご参考までとして下さい。

***※余談※*** 私の人生は、半角括弧「()」を使うべきか、全角括弧「（）」を使うべきかに悩まされ翻弄される人生でした。特にPC黎明期は画面やフォントの関係から半角派でした。今も実はどちらかと言えば半角の方がしっくりくるのですが、ちょっと下にずれていたり、ちょっと潰れ過ぎていたりする印象もあり、Microsoft社が全角推しになったタイミングで、メジャープラットフォームの意向に倣って全角を使うようになりました。ただ、やっぱり半角文字の直後に続く括弧が全角でる事に違和感を拭えない時もあります。そこで、本記事では半角文字直後に続く括弧については半角括弧にしています。これも最良の解とは言えず、全角括弧と半角括弧が混ざっていること自体に今度は違和感を覚える方も居ると思いますがご了承下さい。

# 1. ライブラリーの公開方法（基本の部分なのでご存じの方は飛ばしてください）

### 1-1. コーディング

```Javascript:main.gas
function test() {
    console.log('This is the test code to run!')
}
```
ライブラリーに実行させたいコードを実装します。ここではテストのため簡単な内容ですが、コンソールに文字を出力しています。

### 1-2. デプロイ（ライブラリーとしてビルド）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206965/88874e86-ad29-d5b7-6096-4ac04daf9bfb.png)
デプロイメニューから「***新しいデプロイ***」を選択します。既に一度デプロイしていて更新したコードを公開し直したい場合は「***デプロイを管理***」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206965/501b4e48-d3c4-de01-41fa-a1b539ccbc67.png)
新しいデプロイにて「***ライブラリ***」を選択します（余談ですが、Googleは長音無し派なんですね）

「***説明文***」は必要に応じて入力します（未入力でも問題なし）

「***デプロイ***」ボタンをクリックして完了します。まだこの時点では外部に公開はされていません。

### 1-3. ライブラリーとして外部に公開

ライブラリーを誰でも使えるように外部公開するには、デプロイメニューの右側にある***人型に＋が付いたアイコン***をクリックして外部に共有します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206965/f7c9bb02-ed8f-76d7-6e30-bde5b9a6dd84.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206965/9105ee83-7ea2-930c-7b2f-1479623db1cb.png)

公開範囲を「***リンクを知っている全員***」とすることで、一般に利用可能なライブラリーとして外部に公開されます。

:::note alert
この際、権限を「***閲覧者***」とすることで「***利用できるが（ライブラリーコード自体の）変更は出来ない***」という権限になります。この点が非常に重要なので注意します。間違って共同編集者などにはしないようにして下さい。
:::

# 2. ライブラリーの利用（基本の部分なのでご存じの方は飛ばしてください）

ライブラリー利用者は自分の製造したGASプロジェクトの使用ライブラリーリストに、使用したいライブラリーの***スクリプトID***（ライブラリー識別子）を追加することで利用が可能となります。

### 2-1. ID（ライブラリー識別子）の確認方法（ライブラリー側）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206965/93cde0e7-3ef0-3cf5-ce28-c18f541140cb.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206965/2838e0b9-4995-ec7d-0f15-20c5d7569a4a.png)

### 2-2. ID（ライブラリー識別子）の指定方法（利用側）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206965/ab5afb40-ed30-e523-800a-399895803356.png)
エディターのライブラリー欄で***＋***ボタンをクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206965/7fcb8097-d29d-e695-3fdf-fde11b3b41f7.png)
前項の***ID***（ライブラリー識別子）を***スクリプトID欄***に転記して***検索***ボタンをクリックします。期待通り検索結果が表示されたら、使用したい***ライブラリーバージョン***を選択し、自前コードから当該ライブラリーを利用する際の***クラス名***を任意に指定します（図のように半角スペースなどが入らないようにしましょう）

***追加***ボタンをクリックすれば組み込み完了です。

# 3. ライブラリーのコード参照が可能

ここまでの状態ですと、以下のURLにアクセスすることでライブラリーのコードも全て参照可能な状態となります。
```javascript:ライブラリーのコードが見られるurl
https://script.google.com/d/ライブラリー識別子/edit
```

オープンソース風な用途であれば全く問題ないかと思いますし、寧ろコードが見えていた方が安心して（中で何をやっているのか分かるので）利用できるというメリットもあります。

:::note alert
公開されたGASライブラリーのコードは今日では参照は可能となっている。
:::

# 4. コード自体の暗号化

しかしながら、様々な理由からコードを見せたくない場合もあるでしょう。そんな場合にはコード自体を暗号化して実行時に復号するという方法が考えられます。

### 4-1. 暗号化（復号化も）の準備

これからコードを暗号化していきますが、暗号化や復号化を行うために手っ取り早くここでは[CryptoJS](https://github.com/brix/crypto-js.git)という外部のライブラリーを使います。つまり、ライブラリーからライブラリーを利用するような親亀子亀状態となります。

ID : 1IEkpeS8hsMSVLRdCMprij996zG6ek9UvGwcCJao_hlDMlgbWWvJpONrs 

にてライブラリーを指定します。これで準備は整いました。
人のライブラリー使用では心配（そのアカウントに何かあった場合に利用不可となるため）という方は、自分のリソースとして同じようにライブラリーを用意して利用しても構いませんし、暗号化、復号化コードと同じプロジェクトに入れてしまうという方法もあります。

### 4-2. ライブラリーコードの暗号化の準備

暗号化、復号化にはパスワードが必要です。これは露出させないためにライブラリーのプロパティーに設定しておくことにします。

:::note information
【大前提】ライブラリーのプロパティーに設定されたデータは、例えライブラリーが公開されていたとしても第三者からは見えない。ライブラリーのコードからのみ参照が可能。
:::

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206965/df44b50d-3bb2-4dcc-8e40-673e15c80a75.png)
passwordというキーにパスワードを設定しておきます。こちらは分かりづらく長いものであれば良いので、勝手に作らせる場合には以下のようなコードを実行してもOKです。
```Javascript:パスワードを自動生成して設定する場合
function setPropertyPassword() {
  PropertiesService.getScriptProperties().setProperty('password', generateRandomString(128));
}

function generateRandomString(length) {
  const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@+*}{[]?<>,.!#$%&()=~';
  var result = '';
  const charactersLength = characters.length;

  for (let i = 0; i < length; i++) {
    result += characters.charAt(Math.floor(Math.random() * charactersLength));
  }

  return result;
}
```

### 4-3. ライブラリーコードの暗号化

隠蔽したいコードを暗号化します。コードをJavaScriptの体をなした文字列として暗号化します。

```Javascript:main.gas（元の形（再掲））
function test() {
    console.log('This is the test code to run!')
}
```
こちらを取り敢えずこんな形にします。
```Javascript:main.gas（元の形）
const SOURCE = `
function test() {
    console.log('This is the test code to run!')
}
`
const CODE_KEY = "code";

// プロパティーに入れて暗号化済みのテキストが目に触れることも避ける場合
function setCryptCode() {
  const cryptedCode = crypt(getPropertyPassword(), SOURCE);
  PropertiesService.getScriptProperties().setProperty(CODE_KEY, cryptedCode);
}

function crypt(password, text) {
  var cipher = new CryptGAS.Cipher(password, 'aes');
  var encryptedMessage = cipher.encrypt (text);
  return encryptedMessage;
}

// プロパティーからパスワードを取得する
function getPropertyPassword() {
  console.log(PropertiesService.getScriptProperties().getProperty('password'));
  return PropertiesService.getScriptProperties().getProperty('password');
}
```

```setCryptCode()```を実行することで、暗号化された実行コードがプロパティーのsourceキーに登録されます。

# 5. ランタイム復号と実行

実行する際には復号化してEval()に掛けます。
```Javascript:実行
function execute() {
  // 暗号化された文字列を取得
  const encryptedCode = PropertiesService.getScriptProperties().getProperty(CODE_KEY);
  // 復号化
  const code = decrypt(getPropertyPassword(), encryptedCode);
  // 実行
  eval(code);
}

function getPropertyPassword() {
  console.log(PropertiesService.getScriptProperties().getProperty('password'));
  return PropertiesService.getScriptProperties().getProperty('password');
}
```

```execute()```を実行すると、暗号化文字列を持ってきて、復号化してから実行を掛けるため、この実装においては復号化パスワードも、暗号化されたプログラムも隠蔽されています。オンメモリで復号から実行までされるためこれらの内容を詐取されることもありません。デバッガーなどでブレークポイントを設置して変数の中身を見るなども出来ないと思いますが、念のため変数に代入などせずに直接呼び出す方がより安心でしょう。
```Javascript:変数を介在させない方が少し安心か……
function execute() {
  eval(decrypt(getPropertyPassword(),PropertiesService.getScriptProperties().getProperty(CODE_KEY)));
}
```

:::note alert
但し、この方法は見てお分かりの通り復号化というプロセスが介在するため、実行速度という点では多かれ少なかれ不利である事に間違いありません。
:::

実際に暗号化しないコードと、したコードの実行時間を簡単に計ってみました。大きなコードになるほどコストは大きくなりますのでご注意ください。

***そのまま実行した場合（100回実行時）***
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206965/b0df7efd-b3bb-f69a-79e5-cff4b9768a00.png)

***復号を挟んで実行した場合（100回実行時）***
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206965/46f3c79d-2a50-070a-1bb7-1d629cc93aaf.png)

こんな短い文字列の復号化でもオーバーヘッドがそれなりにあることが分かります。実行する度にブレはありますが、この例では137msと76msで倍近くの差があります。速度が求められるような処理の場合には要注意です。

# 6. ビジネスシーンでの利用想定

コードの隠蔽化や暗号化だけであれば、ここまでの方法で賄えますが、特にビジネスシーンなどにおいては、ライセンス化して特定のライセンスキーを持っているユーザーにだけライブラリーの実行を許可したいこともあります。
簡単に考えればプロパティーに設定している復号化パスワードを、ユーザープログラム側から渡してもらうようにすればこれは実現が可能なのですが、複数のユーザーに同じパスワードを配布する事はセキュリティーや運用面を考えると非現実的ですし、ライセンスキーには有効期限やライセンスタイプ（グレード）みたいな情報も含めたくなるのが人情です。

そこで、コードの暗号化を行いながら、解読時に任意の情報を渡す方法を考えます。

# 7. 公開鍵と非公開鍵の（ような）考え方を導入

そんなに大層な話ではありませんが、ユーザープログラム側から渡す情報を解読用のキーではなくて、暗号化された情報にします。

```javascript:暗号化された利用者用キー
'U2FsdGVkX1/tMKq+OKrC560hUGaOFrSqa4CSw7EaYsXM0sRxMqlnw/I/8FiHk6Yp0iGWbR5iYGYVsVQzcwrSww/s/Df8zbCABk/CoVBFwY06ED9EoerEApZglU0gBeQ0'
```

```javascript:暗号化される前の情報
{
  'license': 'PLATINUM',
  'expire': '2027/11/01',
  'user': 'testuser@test.com'
}
```

サーバー側では「暗号化された利用者用キー」をサーバー側にしか保持していないキーで復号化して利用条件を判定します。この方法を取ると、ライセンスの一覧のようなものを管理しなくても済みます。この例の場合「serverSideSecret」という文字列を暗号化キーにしているので、これをGASのライブラリー側のスクリプトプロパティーに置いておいて復号化するといった方法が考えられます。
ただし、サブスクのようないつ失効してもおかしくないようなものの場合は、サブスク状態を管理しておく必要があります。これは、Stripeのような決済サービスの履歴をそのまま使うといった方法も考えられますが、実は色々と考慮点があったりします。
その話は別の記事に残したいと思います。

[EOF]