Tauriデスクトップアプリのライセンス認証はCloudflare Workerでプロキシする
デスクトップアプリのバイナリに、決済サービスやライセンスサービスのAPIキーを安全に隠すことはできません。
TauriやElectron、ネイティブアプリにsecretを埋め込むなら、それはユーザーの手元に配布されます。難読化しても、Rustでコンパイルしても、信頼境界は変わりません。
ライセンス認証では、Cloudflare Workerのような小さなバックエンドでプロキシするほうが安全です。
問題: バイナリにAPIキーを入れない
危険な構成はこれです。
desktop app -> Lemon Squeezy API
この形では、アプリがprovider APIを直接呼ぶためのcredentialを持ちます。credentialがライセンスのactivateやvalidateに使えるなら、全ユーザーに配布すべきではありません。
安全な構成: appからWorkerへ送る
構成をこうします。
desktop app -> Cloudflare Worker -> license provider API
デスクトップアプリは、ユーザー入力とデバイス情報だけを送ります。
{
"license_key": "XXXX-XXXX-XXXX-XXXX",
"instance_name": "device-or-installation-id"
}
Workerがserver-sideでAPIキーを足し、provider APIを呼びます。アプリには必要最小限の結果だけ返します。
{
"ok": true,
"status": "active",
"instance_id": "instance_123"
}
providerのraw responseをそのまま返さないほうが、UIもテストも安定します。
secretはWranglerで保存する
Cloudflare Workerではsecretを使います。
npx wrangler secret put LEMON_SQUEEZY_API_KEY
Worker側では env から読みます。
export interface Env {
LEMON_SQUEEZY_API_KEY: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const body = await request.json();
const providerResp = await fetch("https://api.lemonsqueezy.com/v1/licenses/activate", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${env.LEMON_SQUEEZY_API_KEY}`,
},
body: JSON.stringify({
license_key: body.license_key,
instance_name: body.instance_name,
}),
});
const data = await providerResp.json();
return Response.json({
ok: Boolean(data.activated),
status: data.license_key?.status ?? "unknown",
instance_id: data.instance?.id ?? null,
});
},
};
ポイントは、デスクトップアプリがprovider API keyを一度も受け取らないことです。
API quotaを無駄にしない
Workerの入口で、明らかに不正な入力を弾きます。
function isPossibleLicenseKey(value: unknown): value is string {
return typeof value === "string" && value.length > 0 && value.length <= 256;
}
失敗理由も分けます。
if (!isPossibleLicenseKey(body.license_key)) {
return Response.json({ ok: false, reason: "invalid_format" }, { status: 400 });
}
false だけ返すと、無効キー、activation limit、ネットワーク障害、Worker障害の区別がつきません。
WebViewのOriginを意識する
TauriなどのWebViewは、通常のWebサイトと同じOriginにならない場合があります。cookieを使わず、browser credentialに依存しないなら、CORSは単純にできます。
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
};
これはライセンス検証の代わりではありません。WebViewからWorkerへ到達できるようにするだけです。
出荷前チェック
- production Workerにsecretが入っている
- desktop repoやconfigにAPIキーがない
- invalid inputでprovider APIを呼ばない
- activation limitをUIに明確に出す
- logにlicense key全文を出さない
- Worker停止時にrecoverableなnetwork errorとして扱う