アプリに組み込む

リクエスト署名

#必要になる場面

独自のバックエンドを持たないアプリはキーをアプリに含めて配布するしかなく、通信をキャプチャすれば誰でもキーを読み取れます。そのキーでリクエスト署名を有効にしてください(コンソール → API キー → 編集 → リクエスト署名)。すると、すべてのリクエストに別の署名シークレットで作成した署名も必要になるため、sk- キー単体では役に立たず、キャプチャしたリクエストもリプレイできません。

#3 つのヘッダー

Authorization はこれまでどおり送信し、ヘッダーを 3 つ追加します。署名対象のテキストは、タイムスタンプ、nonce、HTTP メソッド、パス(クエリ文字列なし)を改行で連結したものです。HMAC のキーには、コンソールに表示される sig_… シークレットをそのまま使います。

headers
X-Timestamp: 1759312345        # unix seconds, within 5 minutes of server time
X-Nonce:     9f2c4e7a1b3d5f60  # 16-128 chars of A-Z a-z 0-9 - _, new for every request
X-Signature: hex(HMAC-SHA256(secret, "1759312345\n9f2c4e7a1b3d5f60\nPOST\n/v1/images/tasks"))

#リクエストに署名する

sign.dart
import 'dart:convert';
import 'dart:math';
import 'package:crypto/crypto.dart';

const apiKey = 'sk-YOUR_KEY';
const signingSecret = 'sig_YOUR_SIGNING_SECRET';
int clockOffset = 0; // server time - device time, from X-Server-Timestamp

Map<String, String> signedHeaders(String method, String path) {
  final ts = (DateTime.now().millisecondsSinceEpoch ~/ 1000 + clockOffset).toString();
  final random = Random.secure();
  final nonce = List.generate(16, (_) => random.nextInt(256).toRadixString(16).padLeft(2, '0')).join();
  final signature = Hmac(sha256, utf8.encode(signingSecret))
      .convert(utf8.encode('$ts\n$nonce\n$method\n$path'))
      .toString();
  return {
    'Authorization': 'Bearer $apiKey',
    'X-Timestamp': ts,
    'X-Nonce': nonce,
    'X-Signature': signature,
  };
}

// headers: signedHeaders('POST', '/v1/images/tasks')
// GET /v1/images/tasks/{id} is signed with its own path, e.g. '/v1/images/tasks/task_123'
sign.mjs
import crypto from "node:crypto";

const ts = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(16).toString("hex");
const signature = crypto
  .createHmac("sha256", process.env.SIGNING_SECRET)
  .update(`${ts}\n${nonce}\nPOST\n/v1/images/tasks`)
  .digest("hex");

await fetch("https://www.aisplice.store/v1/images/tasks", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk-YOUR_KEY",
    "Content-Type": "application/json",
    "X-Timestamp": ts,
    "X-Nonce": nonce,
    "X-Signature": signature,
  },
  body: JSON.stringify(payload),
});

#エラーとルール

  • 拒否されたリクエストは 401 を返し、error.code は signature_required、signature_expired、signature_invalid、signature_replayed のいずれかです。
  • signature_expired の場合は X-Server-Timestamp も返されます。時計がずれている端末は差分を保存してすべてのタイムスタンプに加算し、新しい nonce で一度だけ再試行してください。
  • 再試行には新しい nonce と新しい署名が必要です。nonce は 10 分間記憶されます。
  • コンソールでシークレットを置き換えると、古いシークレットは 30 秒以内に使えなくなります。署名をオフにしてから再びオンにしても、シークレットは変わりません。
  • 署名はキャプチャされた通信からは守りますが、アプリを解析する人からは守れません。ビルドを難読化し(Flutter:--obfuscate)、キーに利用枠の上限を設定してください。

まだ解決しませんか?

リクエスト ID かエラーメッセージを送っていただければ、こちらで調査します。