Files
go-iap/hms/validator.go
2020-09-16 14:03:08 +08:00

258 lines
10 KiB
Go

package hms
import (
"bytes"
"context"
"crypto"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
)
// VerifySignature validate inapp order or subscription data signature. Returns nil if pass.
//
// Document: https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides-V5/verifying-signature-returned-result-0000001050033088-V5
// Source code originated from https://github.com/HMS-Core/hms-iap-serverdemo/blob/92241f97fed1b68ddeb7cb37ea4ca6e6d33d2a87/demo/demo.go#L60
func VerifySignature(base64EncodedPublicKey string, data string, signature string) (err error) {
publicKeyByte, err := base64.StdEncoding.DecodeString(base64EncodedPublicKey)
if err != nil {
return err
}
pub, err := x509.ParsePKIXPublicKey(publicKeyByte)
if err != nil {
return err
}
hashed := sha256.Sum256([]byte(data))
signatureByte, err := base64.StdEncoding.DecodeString(signature)
if err != nil {
return err
}
return rsa.VerifyPKCS1v15(pub.(*rsa.PublicKey), crypto.SHA256, hashed[:], signatureByte)
}
// SubscriptionVerifyResponse JSON response after requested {rootUrl}/sub/applications/v2/purchases/get
type SubscriptionVerifyResponse struct {
ResponseCode string `json:"responseCode"` // Response code, if = "0" means succeed, for others see https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-error-code-0000001050166248-V5
ResponseMessage string `json:"responseMessage,omitempty"` // Response descriptions, especially when error
InappPurchaseData string `json:"inappPurchaseData,omitempty"` // InappPurchaseData JSON string
}
// VerifySubscription gets subscriptions info with subscriptionId and purchaseToken.
//
// Document: https://developer.huawei.com/consumer/en/doc/development/HMSCore-References-V5/api-subscription-verify-purchase-token-0000001050706080-V5
// Source code originated from https://github.com/HMS-Core/hms-iap-serverdemo/blob/92241f97fed1b68ddeb7cb37ea4ca6e6d33d2a87/demo/subscription.go#L40
func (c *Client) VerifySubscription(ctx context.Context, purchaseToken, subscriptionID string, accountFlag int64) (InAppPurchaseData, error) {
var iap InAppPurchaseData
dataString, err := c.GetSubscriptionDataString(ctx, purchaseToken, subscriptionID, accountFlag)
if err != nil {
return iap, err
}
if err := json.Unmarshal([]byte(dataString), &iap); err != nil {
return iap, err
}
return iap, nil
}
// GetSubscriptionDataString gets subscriptions response data string.
//
// Document: https://developer.huawei.com/consumer/en/doc/development/HMSCore-References-V5/api-subscription-verify-purchase-token-0000001050706080-V5
// Source code originated from https://github.com/HMS-Core/hms-iap-serverdemo/blob/92241f97fed1b68ddeb7cb37ea4ca6e6d33d2a87/demo/subscription.go#L40
func (c *Client) GetSubscriptionDataString(ctx context.Context, purchaseToken, subscriptionID string, accountFlag int64) (string, error) {
bodyMap := map[string]string{
"subscriptionId": subscriptionID,
"purchaseToken": purchaseToken,
}
url := c.getRootSubscriptionURLByFlag(accountFlag) + "/sub/applications/v2/purchases/get"
bodyBytes, err := c.sendJSONRequest(ctx, url, bodyMap)
if err != nil {
// log.Printf("GetSubscriptionDataString(): Encounter error: %s", err)
return "", err
}
var resp SubscriptionVerifyResponse
if err := json.Unmarshal(bodyBytes, &resp); err != nil {
return "", err
}
if err := c.getResponseErrorByCode(resp.ResponseCode); err != nil {
return "", err
}
return resp.InappPurchaseData, nil
}
// OrderVerifyResponse JSON response from {rootUrl}/applications/purchases/tokens/verify
type OrderVerifyResponse struct {
ResponseCode string `json:"responseCode"` // Response code, if = "0" means succeed, for others see https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-error-code-0000001050166248-V5
ResponseMessage string `json:"responseMessage,omitempty"` // Response descriptions, especially when error
PurchaseTokenData string `json:"purchaseTokenData,omitempty"` // InappPurchaseData JSON string
DataSignature string `json:"dataSignature,omitempty"` // Signature to verify PurchaseTokenData string
}
// VerifyOrder gets order (single item purchase) info with productId and purchaseToken.
//
// Note that this method does not verify the DataSignature, thus security is relied on HTTPS solely.
//
// Document: https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/api-order-verify-purchase-token-0000001050746113-V5
// Source code originated from https://github.com/HMS-Core/hms-iap-serverdemo/blob/92241f97fed1b68ddeb7cb37ea4ca6e6d33d2a87/demo/order.go#L41
func (c *Client) VerifyOrder(ctx context.Context, purchaseToken, productID string, accountFlag int64) (InAppPurchaseData, error) {
var iap InAppPurchaseData
dataString, _, err := c.GetOrderDataString(ctx, purchaseToken, productID, accountFlag)
if err != nil {
return iap, err
}
if err := json.Unmarshal([]byte(dataString), &iap); err != nil {
return iap, err
}
return iap, nil
}
// GetOrderDataString gets order (single item purchase) response data as json string and dataSignature
//
// Document: https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/api-order-verify-purchase-token-0000001050746113-V5
// Source code originated from https://github.com/HMS-Core/hms-iap-serverdemo/blob/92241f97fed1b68ddeb7cb37ea4ca6e6d33d2a87/demo/order.go#L41
func (c *Client) GetOrderDataString(ctx context.Context, purchaseToken, productID string, accountFlag int64) (purchaseTokenData, dataSignature string, err error) {
bodyMap := map[string]string{
"purchaseToken": purchaseToken,
"productId": productID,
}
url := c.getRootOrderURLByFlag(accountFlag) + "/applications/purchases/tokens/verify"
bodyBytes, err := c.sendJSONRequest(ctx, url, bodyMap)
if err != nil {
// log.Printf("GetOrderDataString(): Encounter error: %s", err)
return "", "", err
}
var resp OrderVerifyResponse
if err := json.Unmarshal(bodyBytes, &resp); err != nil {
return "", "", err
}
if err := c.getResponseErrorByCode(resp.ResponseCode); err != nil {
return "", "", err
}
return resp.PurchaseTokenData, resp.DataSignature, nil
}
// Helper function to send http json request and get response bodyBytes.
//
// Source code originated from https://github.com/HMS-Core/hms-iap-serverdemo/blob/92241f97fed1b68ddeb7cb37ea4ca6e6d33d2a87/demo/demo.go#L33
func (c *Client) sendJSONRequest(ctx context.Context, url string, bodyMap map[string]string) (bodyBytes []byte, err error) {
bodyString, err := json.Marshal(bodyMap)
if err != nil {
return
}
req, err := http.NewRequest("POST", url, bytes.NewReader(bodyString))
if err != nil {
return
}
req = req.WithContext(ctx)
req.Header.Set("Content-Type", "application/json; charset=UTF-8")
atHeader, err := c.GetApplicationAccessTokenHeader()
if err == nil {
req.Header.Set("Authorization", atHeader)
} else {
return
}
resp, err := c.httpCli.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
bodyBytes, err = ioutil.ReadAll(resp.Body)
if err != nil {
return
}
return
}
// GetCanceledOrRefundedPurchases gets all revoked purchases in CanceledPurchaseList{}.
// This method allow fetch over 1000 results regardles the cap implied by HMS API. Though you should still limit maxRows to a certain number to increate preformance.
//
// In case of an error, this method might return some fetch results if maxRows greater than 1000 or equals 0.
//
// Source code originated from https://github.com/HMS-Core/hms-iap-serverdemo/blob/92241f97fed1b68ddeb7cb37ea4ca6e6d33d2a87/demo/order.go#L52
// Document: https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/api-cancel-or-refund-record-0000001050746117-V5
func (c *Client) GetCanceledOrRefundedPurchases(
// context of request
ctx context.Context,
// start time timestamp in milliseconds, if =0, will default to 1 month ago.
startAt int64,
// end time timestamp in milliseconds, if =0, will default to now.
endAt int64,
// rows to return. default to 1000 if maxRows>1000 or equals to 0.
maxRows int,
// Token returned in the last query to query the data on the next page.
continuationToken string,
// Query type. Ignore this parameter when continuationToken is passed. The options are as follows:
// 0: Queries purchase information about consumables and non-consumables. This is the default value.
// 1: Queries all purchase information about consumables, non-consumables, and subscriptions.
productType int64,
// Account flag to determine which API URL to use.
accountFlag int64,
) (canceledPurchases []CanceledPurchase, newContinuationToken string, responseCode string, responseMessage string, err error) {
// default values
if maxRows > 1000 || maxRows < 1 {
maxRows = 1000
}
switch endAt {
case 0:
endAt = time.Now().UnixNano() / 1000000
case startAt:
endAt++
}
bodyMap := map[string]string{
"startAt": fmt.Sprintf("%v", startAt),
"endAt": fmt.Sprintf("%v", endAt),
"maxRows": fmt.Sprintf("%v", maxRows),
"continuationToken": continuationToken,
"type": fmt.Sprintf("%v", productType),
}
url := c.getRootOrderURLByFlag(accountFlag) + "/applications/v2/purchases/cancelledList"
bodyBytes, err := c.sendJSONRequest(ctx, url, bodyMap)
if err != nil {
// log.Printf("GetCanceledOrRefundedPurchases(): Encounter error: %s", err)
}
var cpl CanceledPurchaseList // temporary variable to store api query result
err = json.Unmarshal(bodyBytes, &cpl)
if err != nil {
return canceledPurchases, continuationToken, cpl.ResponseCode, cpl.ResponseMessage, err
}
if cpl.ResponseCode != "0" {
return canceledPurchases, continuationToken, cpl.ResponseCode, cpl.ResponseMessage, c.getResponseErrorByCode(cpl.ResponseCode)
}
err = json.Unmarshal([]byte(cpl.CancelledPurchaseList), &canceledPurchases)
if err != nil {
return canceledPurchases, continuationToken, cpl.ResponseCode, cpl.ResponseMessage, err
}
return canceledPurchases, cpl.ContinuationToken, cpl.ResponseCode, cpl.ResponseMessage, nil
}