App Store Connect APIをSwiftから叩きたい
App Store Connect APIはドキュメントに詳細が載っていますので、基本的にはそちらを参照してください。
その中で、引っかかったポイントとしてトークンの作成方法と、gzipデータの中身を読む方法があるので紹介します。
トークンの作成方法
トークンに関するドキュメントはこちらです。
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",
]
こんな感じで header
と payload
を作成しておきます。(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の設定に保存した時に改行コードが変換されてしまっていたためです。
この対処法については昨日記事にしたのでそちらを参照ください。
gzip 形式で圧縮されたデータの中身を見る方法
App Store Connect APIにはJSON形式でレスポンスが返るものがほとんどですが、中にはgzip形式で圧縮されたデータが返ってくるものがあります。
このデータを復元する方法としてライブラリの利用が考えられます。
今回はこちらのライブラリは利用せずに、調べていくうちにzlibを利用すれば復元できそうということが分かったので、ChatGPTに聞いてコードを書いてもらいました。
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ツールを作っています。
よかったらみてみてください。
Discussion