🤖

App Store Connect APIをSwiftから叩きたい

2024/06/23に公開

App Store Connect APIはドキュメントに詳細が載っていますので、基本的にはそちらを参照してください。

https://developer.apple.com/documentation/appstoreconnectapi

その中で、引っかかったポイントとしてトークンの作成方法と、gzipデータの中身を読む方法があるので紹介します。

トークンの作成方法

トークンに関するドキュメントはこちらです。

https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests#3878466

App Store Connect APIはJWTを利用します。
最初にApp Store ConnectからAPIキーを作成します。
p8形式のファイルをダウンロードし、Issuer IDとKey IDをメモしておきます。

let header: [String: String] = [
  "alg": "ES256",
  "kid": keyID,
  "typ": "jwt",
]

let payload: [String: Any] = [
  "iss": issuerID,
  "iat": Int(Date().timeIntervalSince1970),
  "exp": Int(Date().timeIntervalSince1970 + (20 * 60)),
  "aud": "appstoreconnect-v1",
]

こんな感じで headerpayload を作成しておきます。(scopeはオプショナルなのでここでは無視しています。)
これらをJSON文字列にしたあとBase64(URLエンコード)形式に変換して . で結合します。

let headerJson = try! JSONSerialization.data(withJSONObject: header)
let payloadJson = try! JSONSerialization.data(withJSONObject: payload)

let input = "\(headerJson.base64urlEncodedString()).\(payloadJson.base64urlEncodedString())"
URLエンコード形式のBase64の変換方法
import Foundation

extension Data {
  func base64urlEncodedString() -> String {
    return base64EncodedString()
      .replacingOccurrences(of: "+", with: "-")
      .replacingOccurrences(of: "/", with: "_")
      .replacingOccurrences(of: "=", with: "")
  }
}

次にダウンロードしたプライベートキーを利用してES256形式で暗号化してシグネチャを作成します。
これには CryptoKit を利用します。

let privateKey = try P256.Signing.PrivateKey(pemRepresentation: privateKey)
let data = input.data(using: .utf8)!
let signature = try privateKey.signature(for: data)

P256.Signing.PrivateKey の初期化にはPEM形式( -----BEGIN PRIVATE KEY----------END PRIVATE KEY----- で囲われたフォーマット形式 )の文字列をそのまま渡せるのが便利です。(これは @available(iOS 14.0, macOS 11.0, watchOS 7.0, tvOS 14.0, *) ということでバージョンには気をつける必要があるかもです)

最後に先ほど作成したヘッダーとペイロードをBase64で変換して . で結合した文字列と、シグネチャの内容をBase64で変換した文字列を . で結合することでAPIトークンが完成します。

let raw = signature.rawRepresentation
let apiToken = "\(input).\(raw.base64urlEncodedString())"

あとはAPIリクエストのヘッダーに Authorization: Bearer \(apiToken) のような形で付与してあげればリクエストすることができます。

コラム

ちなみにGitHub ActionsのSecretsにプライベートキーの内容を保存して利用しようとしたところ、なぜかうまくいかず1時間程度ハマってしまいました。
原因はGitHub Actionsの設定に保存した時に改行コードが変換されてしまっていたためです。
この対処法については昨日記事にしたのでそちらを参照ください。

https://zenn.dev/fromkk/articles/4ee7df3e65c87e

gzip 形式で圧縮されたデータの中身を見る方法

App Store Connect APIにはJSON形式でレスポンスが返るものがほとんどですが、中にはgzip形式で圧縮されたデータが返ってくるものがあります。
このデータを復元する方法としてライブラリの利用が考えられます。

https://github.com/1024jp/GzipSwift

今回はこちらのライブラリは利用せずに、調べていくうちにzlibを利用すれば復元できそうということが分かったので、ChatGPTに聞いてコードを書いてもらいました。

https://chatgpt.com/share/ccc06b02-a7ea-46f9-aced-9f4f496add32

gzip形式のデータを復元するコード
import Foundation
import zlib

func gunzip(data: Data) -> Data? {
  guard data.count > 0 else {
    return nil
  }

  var stream = z_stream()
  var status: Int32

  status = inflateInit2_(&stream, 16 + MAX_WBITS, ZLIB_VERSION, Int32(MemoryLayout<z_stream>.size))
  guard status == Z_OK else {
    return nil
  }

  var decompressedData = Data(capacity: data.count * 2)
  data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in
    stream.next_in = UnsafeMutablePointer<Bytef>(
      mutating: bytes.bindMemory(to: Bytef.self).baseAddress!)
    stream.avail_in = uint(data.count)

    while stream.avail_in > 0 {
      if Int(stream.total_out) >= decompressedData.count {
        decompressedData.count += data.count / 2
      }

      stream.next_out = decompressedData.withUnsafeMutableBytes {
        $0.bindMemory(to: Bytef.self).baseAddress! + Int(stream.total_out)
      }
      stream.avail_out = uint(decompressedData.count) - uint(stream.total_out)

      status = inflate(&stream, Z_SYNC_FLUSH)
      guard status == Z_OK || status == Z_STREAM_END else {
        inflateEnd(&stream)
        return
      }
    }
  }

  inflateEnd(&stream)
  decompressedData.count = Int(stream.total_out)

  return decompressedData
}

ChatGPTが生成したコードをそのまま利用しているので、何か誤りなどあればご指摘ください。
(僕はそのまま利用できたので、そのまま使っています)

まとめ

ここではSwiftでApp Store Connect APIに利用するトークンを作成する方法と、gzip形式のデータを復元する方法について紹介しました。

これらの内容を利用してアプリの日々のインストール数とユーザーレビューをSlackにまとめて投稿するようなCLIツールを作っています。
よかったらみてみてください。

https://github.com/fromkk/AppReporter.git

Discussion