概要
GooglePlayでは課金処理の実行時に署名を発行してくれるため、そのレシートが正規のレシートかどうかを検証する事が可能です。
今回はGoでその検証ロジックを実装してみます。
環境
- golang 1.6
必要な情報
以下の3つが必要になります。
1. アプリの公開鍵
以下の様なbase64文字列です。
Vm1wS01GWXlSWGhYV0d4WFlteEtWMWxVU2xOVlZsbDNXa1pPYW1KR2NIaFZWelZyWWtkS1NHVkdhRmhoTVZVeFYxWmtTMVpzV25GV2JHUnBWMFZLV0ZaVldrWlBWa0pWVFVRd1BWWnFTakJXTWtWNFYxaHNWMkpzU2xkWlZFcFRWVlpaZDFwR1RtcGlSbkI0VlZjMWEySkhTa2hsUm1oWVlURlZNVmRXWkV0V2JGcHhWbXhrYVZkRlNsaFdWVnBHVDFaQ1ZVMUVNRDFXYWtvd1ZqSkZlRmRZYkZkaWJFcFhXVlJLVTFWV1dYZGFSazVxWWtad2VGVlhOV3RpUjBwSVpVWm9XR0V4VlRGWFZtUkxWbXhhY1Zac1pHbFhSVXBZVmxWYVJrOVdRbFZOUkRBOVBRPT0=
※値はダミーです
2. 購入時のreceipt
以下の様なJSONです。クライアントから送ってもらいます。
{ "orderId": "GPA.xxxx-xxxx-xxxx-xxxxx", "packageName": "com.example", "productId": "premium", "purchaseTime": 1459390336508, "purchaseState": 0, "developerPayload": "your_payload", "purchaseToken": "xxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxx", "autoRenewing": true }
※各値はダミーです
3. 購入時のsignature
以下の様なbase64文字列です。クライアントから送ってもらいます。
Vm0xd1MwMUdXWGxTV0doWVYwZDRXRmx0ZUV0V01XeFZVMnhPVmxac2JETlhhMXBQWVcxS1IyTklhRlpXZWxaeVdXdGtTMU5IVmtkaFJtaG9UVlpWZUZZeFdtdFRNVnB6VjI1R1YySkhVbkJXTUZaTFYwWmFWbGRyV2xCV2EwcFdWRlZSZDFCV1duRlRha0pYVFd0V05GWXhhSE5XTWtwelUyeGtXbFpGY0ZSV1ZscGFaREZ3UjFSdGNHbFNia0kwVmxaak1XRXlTa2hUYTJoc1VtMW9XVmxVUmxaTlZtUlhXa1YwVjJKR2NIaFdiWGhyWVZaa1JsTnNhRmRXVm5CSFZERmFRMVpWTVVWTlJERlhZV3R2ZDFacVNrWmxSbVJaWWtaa2FXSkZjRmhYVmxKTFZURldWMWRZWkdGU2F6VnhXV3RhZDJWR1ZsaE9WM1JwVWpCd1NWcFZXbTlYUjBWNFZsUkdXRlp0VWt4V2JYaGhZMVphYzFwSGJGaFNWWEJaVm14V1lWSnJPVmRSYkZaT1VrUkJPVkJSUFQwPQ==
※値はダミーです
公開鍵、signatureは元々base64で扱われています。receipt情報は普通のJSONですが、扱いやすさのためにbase64エンコードしたものとして扱います。
公開鍵の取得
GooglePlayDeveloperConsoleにログインし、左メニューのすべてのアプリ
から自分のアプリをクリックし、以下のページを開きます。
検証ロジック
頑張って実装すると以下のようにします。
package main import ( "crypto" "crypto/rsa" "crypto/sha1" "crypto/x509" "encoding/base64" "fmt" "log" ) const ( // you can get this public key from GooglePlayDeveloperConsole. base64EncodedPublicKey = "--- your app's public key ---" ) func main() { receipt := "--- receipt data encoded by base64 ---" signature := "--- signature ---" isValid, err := verify(receipt, signature) if err != nil { log.Println(err) } log.Println("valid: ", isValid) } func verify(receipt string, signature string) (bool, error) { // prepare public key decodedPublicKey, err := base64.StdEncoding.DecodeString(base64EncodedPublicKey) if err != nil { return false, fmt.Errorf("failed to decode public key") } publicKeyInterface, err := x509.ParsePKIXPublicKey(decodedPublicKey) if err != nil { return false, fmt.Errorf("failed to parse public key") } publicKey, _ := publicKeyInterface.(*rsa.PublicKey) // decode signature decodedSignature, err := base64.StdEncoding.DecodeString(signature) if err != nil { return false, fmt.Errorf("failed to decode signature") } // generate hash value from receipt decodedReceipt, err := base64.StdEncoding.DecodeString(receipt) if err != nil { return false, fmt.Errorf("failed to decode receipt") } hasher := sha1.New() hasher.Write(decodedReceipt) hashedReceipt := hasher.Sum(nil) // verify err = rsa.VerifyPKCS1v15(publicKey, crypto.SHA1, hashedReceipt, decodedSignature) if err != nil { return false, nil } return true, nil }
動作確認
レシートと署名が正しいと以下のようにtrue
が返ります。
$ go run verify.go 2016/04/01 09:02:01 valid: true
ライブラリでもっと楽に検証
go-iapというライブラリがあるのでそれを使うともっとコードが短くなります。
import ( "encoding/base64" "log" "github.com/dogenzaka/go-iap/playstore" ) const ( base64EncodedPublicKey = "--- your app's public key ---" ) func main() { receipt := "--- receipt data encoded by base64 ---" signature := "--- signature ---" d, _ := base64.StdEncoding.DecodeString(receipt) isValid, err := playstore.VerifySignature(base64EncodedPublicKey, d, signature) if err != nil { log.Println("Failed to verify signature") } log.Println("valid: ", isValid) }
その他注意
今回receiptはbase64文字列で受け取って処理しています。
もしレシートをstructで定義して受け取る場合はフィールドの順番に気をつけましょう。フィールド順が元のレシートと異なるとハッシュ化で違う値になるためです。
以下が正しい順番です。ただしGoogle側がフィールドを変更する可能性もあるので、Goでは定義せずまるっとJSONごと受け取って検証するほうが安全です。
type Receipt struct { OrderID string `json:"orderId"` PackageName string `json:"packageName"` ProductID string `json:"productId"` PurchaseTime int64 `json:"purchaseTime"` PurchaseState int `json:"purchaseState"` DeveloperPayload string `json:"developerPayload"` PurchaseToken string `json:"purchaseToken"` AutoRenewing bool `json:"autoRenewing"` }