ExtendScriptにおけるDRMの実装
今回は完全にExtendScriptをベースとするデベロッパー向けの内容になります。わたしたちデベロッパーも開発には環境整備から始まりコードを書く時間までそれ相応のコストを負担しています。例えばQR Code Makerでは開発の予備調査からリリースまで20ヶ月近く要し、延べ時間は200時間を余裕で超えるわけです。人月で計算するとわたしが個人で負担したコストは150万円超となります。これは極端な例ですが、ちょっとしたフリースクリプトを配布している方々も大なり小なりコストを負担しています。本来、ある程度の品質を担保する必要がある場合は当然の如く有料化を視野に入れなければならない事なのです。
そこで問題になるのがユーザー認証やコピープロテクション等のDRM関連の技術です。ExtendScriptでは元来こう言った物に対しては考えられてはいません。(jsxbinはコードの隠蔽には有効だがコピーに対しては保護とはならない)しかし、比較的手間のかからない手法で出来ることも幾つか存在します。
今回は基礎的な手法から高度な実装の概要までを紹介します。
スクリプトの実行制限を行う手法としてポピュラーなものには以下のようなものがあります。
- 期間による制限
- ユーザーによる制限
- ネットワーク経由による認証
一番簡単に実現できるのが期間限定です。これはスクリプトを配布する前に有効期限をハードコードしてしまう手法です。
実行期間限定
var tm = new Date();
alert(tm.getTime());
ExtendScriptではこのようにUnix Timeを取得できます。Unix Timeとは1970.1.1午前0時から延々とms単位でカウントされるものです。これを利用して有効期限を設定します。
var tm = new Date();
alert(tm.getTime()+604800000); //60×60×24×7×1000=1週間
このようなコードにてリリース時に有効期限を用意してこれを実行スクリプトにハードコードします。
var expiretime = 1529883574545;
var tm = new Date();var flag = true;
if (tm>expiretime) flag = false;
if (flag) { //execute main routine
$.writeln("test");
}else {
alert("有効期限は終了しました。");
}
この様な構成でスクリプトを作成しバイナリ化して配布します。
使用開始からの期間限定
この場合、使用開始からの期限切れを管理するために何らかの時間関連パラメータを保持する必要があります。AIやPSの場合であればcustom preferenceやcustom ActionDescripterを利用して記録しておくことも可能です。
<strong>Photoshop</strong>
//write
var desc = new ActionDescriptor();
desc.putString(1234567890123, "TESTString");
app.putCustomOptions("storageTest", desc, true);
//Read
var customActionDescriptor = app.getCustomOptions("storageTest");
alert(customActionDescriptor.getString(1234567890123));
<strong>Illustrator</strong>
app.apreferences.setIntegerPrefernce("expireTime","1234567980");
app.apreferences.getIntegerPrefernce("expireTime");
以下はIllustratorでの実装例です。
if (app.preferences.preferenceExists("expireTime")){
var tm = new Date();
app.preferences.setIntegerPreference("expireTime", tm.getTime()+604800000);
}
var t = new Date();
if (app.preferences.getIntegerPreference("expireTime")>t.getTime()){
alert("Run Script...");
}else{
alert("Expired...");
}
この例ではIllustratorの初期設定に有効期限を書き込んでいます。
この様なカスタムデータを置きたくない場合は実行スクリプトファイルを期限付きでユーザー側にて生成する方法が考えられます。以下にごく初歩的な手法を解説します。
基本的にユーザー側で最初にインストーラースクリプトを実行させます。この時に失効時間を実行スクリプトコードと共に実行用ファイルに書き込みます。
var b="@JSXBIN@ES@2.0@MyBbyBnACMAbyBn0AKJBnABjzDjTjUjSBfEXzIjUjPiTjUjSjJjOjHCfVzCjUjTDf"
+"GnfnfOCZCnAFcfACzBheEXzGjMjFjOjHjUjIFfjBfnndNnJDnASzCjTjUGAAnnftaEJEnABQzAHfVGf"
+"AVzBjJIfBEjzGiOjVjNjCjFjSJfRBEXzGjTjVjCjTjUjSKfjBfRCVIfBFdBffffnfAVIfBAFdNByBzB"
+"hcLJFnASzCjOjNMCCzBhLNCNCNCNCNCNCNCNCNCNCNXzBhQOfVGfACzBhKPXzBhRQfVGfAnndDnnXzB"
+"hSRfVGfAnnCPXzBhTSfVGfAnndDnnXzBhUTfVGfAnnCPXzBhVUfVGfAnndDnnXzBhWVfVGfAnnCPXzB"
+"hXWfVGfAnndDnnXzBhYXfVGfAnnCPXzBhZYfVGfAnndDnnXzChRhQZfVGfAnnCPXzChRhRgafVGfAnn"
+"dDnnnftJGnASzCjEjWgbDEXzFjGjMjPjPjSgcfjzEiNjBjUjIgdfRBCzBhPgeVMfCnndKffnftJHnAS"
+"zCjDjLgfECzBhNhAnChAVMfCCPVgbfDnndKnndKnnftOIJInASgfEndAffACzChdhdhBVgffEnndKnJ"
+"JnASzEjUjNjTjUhCFEjzEiEjBjUjFhDfntnftOKJKnAEjzEjNjBjJjOhEfnfAUzChGhGhFChBVgffEX"
+"zChRhShGfVGfAnnCEVDfGEXzHjHjFjUiUjJjNjFhHfVhCfFnfnnnnnAHI4B0AiAgb4D0AiAgf4E0AiA"
+"D40BhAG40BiAhC4F0AiAM4C0AiABGAzFjDjLjEjHjUhIALMMbyBn0ABJNnAEjzFjBjMjFjSjUhJfRBC"
+"NCNXzEjOjBjNjFhKfjzDjBjQjQhLfnneGKhAjVjTjFjSXzLjVjTjFjSiBjEjPjCjFiJjEhMfjhLfnnf"
+"f0DhEAOBJPnAEjhIfRBjDfff0DHByB";
function getExpireTimestamp(n){
var d = new Date();
var tm = String(d.getTime()+Number(n));
var st = [];
for (var i=0; i<12; i++ ) st[i] = Number (tm.substr (i, 1));
var nm = st[0]+st[1]*3+st[2]+st[3]*3+st[4]+st[5]*3+st[6]+st[7]*3+st[8]+st[9]*3+st[10]+st[11]*3;
var dv = Math.floor (nm/10);
st[12] = 10 - (nm-dv*10);
if (st[12]==10) st[12] = 0;
return Number(st.join(""));
}
$.fileName.match(/(.+\/)(.+?\.jsx)/);
var pth = RegExp.$1 + "testCompiledScript.jsx";
var f = File(pth);
f.open("w");
f.writeln("#target indesign-13;");
var ts = getExpireTimestamp(86400000);
f.writeln("var ts=" + ts + ";");
f.writeln("eval('"+b+"')");
f.close();
上のコードを実行して生成された実行ファイルが以下のようになります。(以下は見やすいようにバイナリ化されたコード部分を折り返しています。)
#target indesign-13;
var ts=1529994194667;
eval('@JSXBIN@ES@2.0@MyBbyBnACMAbyBn0AKJBnABjzDjTjUjSBfEXzIjUjPiTjUjSjJjOjHCfVz'
+'CjUjTDfGnfnfOCZCnAFcfACzBheEXzGjMjFjOjHjUjIFfjBfnndNnJDnASzCjTjUGAAnnftaEJEnAB'
+'QzAHfVGfAVzBjJIfBEjzGiOjVjNjCjFjSJfRBEXzGjTjVjCjTjUjSKfjBfRCVIfBFdBffffnfAVIfBAFdNB'
+'yBzBhcLJFnASzCjOjNMCCzBhLNCNCNCNCNCNCNCNCNCNCNXzBhQOfVGfACzBhKPXzBhR'
+'QfVGfAnndDnnXzBhSRfVGfAnnCPXzBhTSfVGfAnndDnnXzBhUTfVGfAnnCPXzBhVUfVGfAn'
+'ndDnnXzBhWVfVGfAnnCPXzBhXWfVGfAnndDnnXzBhYXfVGfAnnCPXzBhZYfVGfAnndDnnX'
+'zChRhQZfVGfAnnCPXzChRhRgafVGfAnndDnnnftJGnASzCjEjWgbDEXzFjGjMjPjPjSgcfjzEiNjB'
+'jUjIgdfRBCzBhPgeVMfCnndKffnftJHnASzCjDjLgfECzBhNhAnChAVMfCCPVgbfDnndKnndKn'
+'nftOIJInASgfEndAffACzChdhdhBVgffEnndKnJJnASzEjUjNjTjUhCFEjzEiEjBjUjFhDfntnftOKJKn'
+'AEjzEjNjBjJjOhEfnfAUzChGhGhFChBVgffEXzChRhShGfVGfAnnCEVDfGEXzHjHjFjUiUjJjNjFhH'
+'fVhCfFnfnnnnnAHI4B0AiAgb4D0AiAgf4E0AiAD40BhAG40BiAhC4F0AiAM4C0AiABGAzFjDj'
+'LjEjHjUhIALMMbyBn0ABJNnAEjzFjBjMjFjSjUhJfRBCNCNXzEjOjBjNjFhKfjzDjBjQjQhLfnneGK'
+'hAjVjTjFjSXzLjVjTjFjSiBjEjPjCjFiJjEhMfjhLfnnff0DhEAOBJPnAEjhIfRBjDfff0DHByB')
getExpireTimestampが生成するタイムスタンプはチェックサムが付加されています。上のコードではtsの値が見えていますが、このリテラルを書き換えるとチェックサムが不正になるためエラーを生じてスクリプトの実行が停止します。
メインルーチンとタイムスタンプ評価関数をバイナリ化した部分の構造は以下のようになっています。
function ckdgt(ts){
str.toString();
if(str.length>13) return false;
var st = [];
for (var i=0; i<13; i++ ) st[i] = Number (str.substr (i, 1));
var nm = st[0]+st[1]*3+st[2]+st[3]*3+st[4]+st[5]*3+st[6]
+st[7]*3+st[8]+st[9]*3+st[10]+st[11]*3;
var dv = Math.floor (nm/10);
var ck = 10 - (nm-dv*10);
if (ck==10) ck = 0;
if (ck==st[12]) main();
}
function main(){
alert(app.name + "\n user"+ app.userAdobeId);
}
ckdgt(ts);
引数として渡されたUnix Timeの長さとチェックサムの2つを確認し、期限内かつ改変が見られない場合にmain関数を実行するようになっています。この例は必要最小限で、実際の運用ではユーザーハッシュの追記やデジットのシャッフル等のアルゴリズムを追加して保護強度を高めます。
以下に示すのはユーザーを一意に認識するために取得可能なプロパティです。
app.userAdobeId//ID
app.userAdobeID//AI
app.userGuid//ID
app.userGUID//AI
Photoshopの場合はsystemInformation内にシリアルが記述されていますのでひと手間必要となります。
app.systemInformation.match(/シリアル番号 : (\d+)/);//PS
$.writeln(RegExp.$1);
ネットワーク認証
ExtendScripの場合、Socketクラスを利用した通信を利用して実行管理を行うことができます。
・
・
・
var st = dgst.hash(str);
var hjklstg="/uis/drm_provider.php";
var dst = File ($.fileName).parent.fsName;
try {var libPath = dst +"/HttpClient.framework";
if (eo==null) var eo = new ExternalObject("lib:"+libPath);
var rt="";
var stgatha = "";
rt=eo.getHTTP("http://"+stgatha+hjklstg+"?hash="+st+"&app_id="+appID);
eo.unload();
if (rt.indexOf("true")>1) return true;
else return false;
} catch (e){
alert(e);
}
}
このコードでは1行目のhash取得関数にてuserGuidをhash化し、それをDRMサーバーへ問い合わせています。
サーバーから返されるのはBooleanでtrueが返された場合のみスクリプトを実行します。このコードで利用されているのは私が用意したHTTPクライアント用ExternalObjectです。これを利用すると本来ソケット接続が不可能なIllustrator等でもネットワークを経由した処理が可能となります。
ハッシュ化関数は以下を利用すると良いでしょう。
https://github.com/ten-A/Extend_Script_experimentals/blob/master/generateHash.jsx
JSXBINファイルのデコードについて
現在のJSXBINはver.2にあたり、解析されてデコードが可能になっています。これについて変数や定数等を更にスクランブル処理を行うJSXBlindというツールがあります。
詳しくはMarc Auretさんの以下のページを参照してください。記事の最後にダウンロードリンクがあります。
http://www.indiscripts.com/post/2015/12/jsxblind-first-jsxbin-obfuscator-for-extendscript
JSXBIN自体は元々コードの隠蔽のために用意されているものです。現在デコードに関する情報が一部で流れていますが、バイナリ化する前のスクリプトをobfuscator等を事前にかけるなどの処理によりデコード時に読みにくいコードにする事は可能です。しかし、ECMA3準拠のExtendScriptでは難読化された際に新しいJavaScriptの構造に置き換えられて正常に動かなくなる場合があります。
こういった場合でもこのJSXBlindは難読化処理が可能です。通常用いられるAscii領域だけではなくマルチバイトキャラクタを利用した処理を行うためにデコードしたとしても非常に読みにくいものになります。
終わりに…
この様な処理を組み込むという事は対価をお支払いいただいている正規ユーザーに対するデベロッパー側の責任でもあります。
また、DRM関連情報及び認証サーバー等の運用に関してはわたしに直接ご相談ください。