购买Android应用程式内:签名验证失败


71

我已经尝试使用SDK随附的Dungeons演示代码来解决此问题好几天了。我曾尝试向Google寻求答案,但找不到答案。

  • 在Dungeons演示中,我从开发控制台传递了我的公钥。
  • 签署了apk并没有发布就上传到控制台。
  • 测试android.test.purchased在控制台上创建的产品列表和发布的用于订阅的产品列表(我想要为我的应用程序提供的主要功能)。

但是仍然出现错误,Signature verification failed然后签名与数据不匹配。我该如何解决?

public static ArrayList<VerifiedPurchase> verifyPurchase(String signedData, String signature)
{
    if (signedData == null) {
        Log.e(TAG, "data is null");
        return null;
    }
    if (Consts.DEBUG) {
        Log.i(TAG, "signedData: " + signedData);
    }
    boolean verified = false;
    if (!TextUtils.isEmpty(signature)) {

        String base64EncodedPublicKey = "MIIBIjA....AQAB";
        PublicKey key = Security.generatePublicKey(base64EncodedPublicKey);
        verified = Security.verify(key, signedData, signature);
        if (!verified) {
            Log.w(TAG, "signature does not match data.");
            return null;
        }
    }
}

public static boolean verify(PublicKey publicKey, String signedData, String signature)
{
    if (Consts.DEBUG) {
        Log.i(TAG, "signature: " + signature);
    }
    Signature sig;
    try {
        sig = Signature.getInstance(SIGNATURE_ALGORITHM);
        sig.initVerify(publicKey);
        sig.update(signedData.getBytes());
        if (!sig.verify(Base64.decode(signature))) {
            Log.e(TAG, "Signature verification failed.");
            return false;
        }
        return true;
    } catch (NoSuchAlgorithmException e) {
        Log.e(TAG, "NoSuchAlgorithmException.");
    } catch (InvalidKeyException e) {
        Log.e(TAG, "Invalid key specification.");
    } catch (SignatureException e) {
        Log.e(TAG, "Signature exception.");
    } catch (Base64DecoderException e) {
        Log.e(TAG, "Base64 decoding failed.");
    }
    return false;
}

Answers:


150

当前的Google结算版本中仍然存在此问题。基本上android.test.purchased坏了;你买android.test.purchased后verifyPurchase在功能Security.java总是会失败,并且QueryInventoryFinishedListener将在该行停止,如果(result.isFailure()) ; 这是因为android.test.purchased项目始终未通过Security.java中的TextUtils.isEmpty(signature)检查,因为它不是真实项目,并且服务器未返回任何签名。

我的建议(由于缺少其他解决方案)是不要使用“ android.test.purchased”。网上有各种各样的代码调整,但是它们都不是100%有效的。

如果您使用了android.test.purchased,那么摆脱该错误的一种方法是执行以下操作:

  1. 编辑Security.java,并将verifyPurchase中的“ return false”行更改为“ return true”-这是临时的,我们将在几分钟后将其放回去。
  2. 在QueryInventoryFinishedListener中,在“ if(result.isFailure()){...}”行之后,添加以下内容以消耗并摆脱您永无止境的android.test.purchased项目:

    if (inventory.hasPurchase(SKU_ANDROID_TEST_PURCHASE_GOOD)) {  
       mHelper.consumeAsync(inventory.getPurchase(SKU_ANDROID_TEST_PURCHASE_GOOD),null);
       }
    
  3. 运行您的应用程序,以便consunmeAsync发生,这摆脱了服务器上的“ android.test.purchased”项。

  4. 删除consumerAsync代码(或将其注释掉)。
  5. 返回到Security.java,将“ return true”更改回“ return false”。

您的QueryInventoryFinishedListener将不再在验证时出错,一切都恢复为“正常”(如果可以调用它的话)。记住-不要再使用android.test.purchased了,因为它只会再次导致此错误...它坏了!测试购买的唯一真实方法是上载APK,等待其出现,然后在启用日志记录的情况下在您的设备上对其进行测试(相同的APK)。


5
我遇到了同样的问题,但是在此站点上找到了一个使用android.test.purchase的简单解决方案,他在那里静态创建了购买以供以后使用:`pp = new Purchase(“ inapp”,“ {\” packageName \ “:\” PACKAGE_NAME \“,” +“ \” orderId \“:\” transactionId.android.test.purchased \“,” +“ \” productId \“:\” android.test.purchased \“,\” developerPayload \“:\” \“,\” purchaseTime \“:0:” +“ \” purchaseState \“:0,\” purchaseToken \“:\” inapp:PACKAGE_NAME:android.test.purchased \“}”, “”);`
Deadolus 2014年

54
Google惊人的无能使我感到惊讶。
TimSim 2014年

6
谢谢@GTMDev。这在2015年仍然没有解决,您的回答帮助我恢复了正常。另外,对于以后的读者来说,上述SKU_ANDROID_TEST_PURCHASE_GOOD常量的值应为“ android.test.purchased”。
2015年

16
是的,问题仍然存在。在我购买android.test.purchased之后,我开始收到有关查询库存的错误消息。我只想补充一点,仅清除Google Play商店应用程序的数据并运行一次即可修复您的手机。当您清除Google Play的数据时,它会忘记您购买了android.test.purchased。
罗伯特

10
到底是谁写了这个API?糟透了
user2161301 2015年

50

是的,问题仍然存在。在我购买android.test.purchased之后,我开始收到有关查询库存的错误消息。只需清除Google Play商店应用程序的数据并运行一次Google Play,即可修复您的手机。当您清除Google Play的数据时,它会忘记您购买了android.test.purchased


1
这个对我有用。感谢您提供的非常简单的解决方案。
悉尼

最简单的解决方案。它也对我有用!谢谢!这应该被接受的答案!
vaske 2015年

这是一个更简单的解决方案,并且不易出错,因为您在使用测试购买产品后不会意外忘记删除代码...
DiscDev

2015年10月16日,这仍然会发生。
Pimentoso

1
仍在发生21 / 10.15-android.test.purchased是数小时整理此事件的原因?难以置信的。谢谢您为我节省了一天多的时间,欢呼
neillo 2015年

23

请检查是否base64EncodedPublicKeyPlay开发者控制台中的一个相等。在开发者控制台中重新上传APK后,公钥可能会更改,如果更改,则更新base64EncodedPublicKey


我遇到相同的错误,并且我的键完全相同。必须要进行其他操作。
阿尔菲·汉森

4
我遇到了同样的问题,并且确实对公共密钥不匹配。但是,每次重新上传APK时,公钥似乎都不会更改(非常感谢!)。
Jean-Philippe Pellet

@ Jean-PhilippePellet同样在这里!我不知道何时会更改密钥。
德庆

“一旦您在开发者控制台中重新上传APK,公钥可能会更改”,您是说每次我上传新版本到Play Store时都必须更改base64EncodedPublicKey吗?这真是荒谬。
玛丽安Paździoch

不,正如其他人在此问题中所述,该问题很可能是由于android.test.purchased SKU引起的。不是关键的不匹配。
IgorGanapolsky'9

9

您可以跳过那些“ android.test。*”产品ID的验证过程。如果您正在使用TrivialDrive示例中的示例代码,请打开IabHelper.java,找到以下行代码,将其更改为

   if (Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) { ... }

进入

   boolean verifySignature = !sku.startsWith("android.test."); // or inplace the condition in the following line
   if (verifySignature && !Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) { ... }

即使您忘记回滚代码,它也无害。因此,您可以继续测试进一步的工作流程步骤。


8

根据GMTDev的回答,这是我为解决以最简单的方式消费产品时的测试问题而要做的事情。在Security.java中,用以下代码替换verifyPurchase()方法:

public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
    if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) ||
            TextUtils.isEmpty(signature)) {
        Log.e(TAG, "Purchase verification failed: missing data.");
        return BuildConfig.DEBUG; // Line modified by Cristian. Original line was: return false;
    }

    PublicKey key = Security.generatePublicKey(base64PublicKey);
    return Security.verify(key, signedData, signature);
}

我只修改了一行(请参阅注释),这样您就可以保留代码以进行调试,并且仍然可以安全地发布发行版本。


您好,我有一个问题关于应用内结算:stackoverflow.com/questions/36298320/...
Ruchir Baronia酒店

5

该错误是由于错误的许可证密钥引起的。许可证密钥可能来自另一个应用程序。

解决方案是使用来自以下位置的正确许可证密钥:

播放控制台>应用>开发工具>许可和应用内结算


正是我的问题。决定与新应用程序共享现有应用程序中的许可证代码,并在其中保留了旧许可证密钥,这给了我签名验证失败的错误。
道格西蒙顿

3

在使用应用内结算v3和随附的实用工具类时,对我有用的是在返回的onActivityResult调用中消耗了测试购买费用。

无需更改IabHelper,安全性或任何应用内结算实用程序类,就可以避免在以后的测试购买中发生这种情况。

如果您已经尝试购买测试产品,但现在却停留在购买签名验证失败的错误上(很可能是因为您正在寻找该错误的答案),那么您应该:

  1. 进行GMTDev建议的更改
  2. 运行应用程序以确保它消耗了产品
  3. 删除/撤消GMTDev的更改
  4. 在onActivityResult中实现以下代码。

这不仅可以使购买测试流程变得流畅,而且还可以避免在尝试重新购买测试产品时Iab返回物品已拥有错误的任何冲突问题。

如果从片段内调用此方法,而未调用片段的onActivityResult,则请确保在必要时从父ActivityFragment调用YourFragmentName.onActivityResult(requestCode,resultCode,data)。从Fragment调用startIntentSenderForResult(Android Billing v3)中对此进行了详细说明

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_PURCHASE) {

        //this ensures that the mHelper.flagEndAsync() gets called 
        //prior to starting a new async request.
        mHelper.handleActivityResult(requestCode, resultCode, data);

        //get needed data from Intent extra to recreate product object
        int responseCode = data.getIntExtra("RESPONSE_CODE", 0);
        String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA");
        String dataSignature = data.getStringExtra("INAPP_DATA_SIGNATURE");

        // Strip out getActivity() if not being used within a fragment
        if (resultCode == getActivity().RESULT_OK) {
            try {
                JSONObject jo = new JSONObject(purchaseData);
                String sku = jo.getString("productId");

                //only auto consume the android.test.purchased product
                if (sku.equals("android.test.purchased")) {
                    //build the purchase object from the response data
                    Purchase purchase = new Purchase("inapp", purchaseData, dataSignature);
                    //consume android.test.purchased
                    mHelper.consumeAsync(purchase,null);
                }
            } catch (JSONException je) {
                //failed to parse the purchase data
                je.printStackTrace();
            } catch (IllegalStateException ise) {
                //most likely either disposed, not setup, or 
                //another billing async process is already running
                ise.printStackTrace();
            } catch (Exception e) {
                //unexpected error
                e.printStackTrace();
            }
        }
    }
}

如果它的sku是“ android.test.purchased”,它将仅删除购买的商品,因此应该可以安全使用。


如果您省略了onActivityResult()回调,则确认的IAB将无法正常工作。
Grux


1

签名验证仅对默认测试产品失败。快速修复:

  • 转到IabHelper类。
  • 反转的if条件Security.verifyPurchase

而已!

当测试产品替换为实际产品时,请记住要还原更改


1

今天(2018年10月30日)遇到同一问题(签名验证和摆脱测试购买)。

这些测试sku并不是您的应用程序的真正组成部分,因此没有您应用程序的签名,这可能是导致签名问题的原因。我确实向Google开了票,但不确定他们是否可以解决此问题。正如其他人指出的那样,解决方法是替换代码

if (verifyValidSignature(purchase.getOriginalJson(), purchase.getSignature())) {

if (verifyValidSignature(purchase.getOriginalJson(), purchase.getSignature()) ||
                        (purchase.getSku().startsWith("android.test.")) ) { 

关于“如何摆脱购买android.test.purchased SKU的购买”,我发现该设备只需重新启动,然后等待一分钟左右和/或重新启动您的应用程序几次,即可修复该问题对我来说(即我不必通过代码“消费”购买)。我猜想需要等待,以便Play商店完成与Google服务器的同步。(不确定将来是否会继续以这种方式工作,但是如果现在适合您,这可能会帮助您前进。)


0

检查此答案

测试设备上的主要帐户与Google Play开发者帐户是否相同?

否则,除非该应用程序之前已在Play上发布,否则您不会在android.test。*静态响应上获得签名。

有关 完整的条件,请参见http://developer.android.com/guide/market/billing/billing_testing.html#static-responses-table中的表 。

它的评论是:

我认为静态ID不再返回签名。参见 https://groups.google.com/d/topic/android-developers/PCbCJdOl480/discussion

此外,以前来自Google Play结算库的示例代码(由许多大型应用程序使用)允许空签名。这就是为什么静态购买在那里起作用的原因。
但这是一个安全漏洞,因此在发布时,Google提交了更新


我在应用中的计费的问题:stackoverflow.com/questions/36298320/...
Ruchir Baronia酒店

很抱歉在这里询问@Luten,但是您是否有关于如何使用应用内结算处理增值税税的经验,以及由Google自动在哪些国家/地区进行增值税处理,以及我必须手动向/报告增值税税的任何经验?看到stackoverflow.com/questions/36506835/...
王庙韦斯特内斯

@VidarVestnes,抱歉,无法帮助您。
Luten

0

我有同样的问题,并根据@Deadolus的说法 https://www.gaffga.de/implementing-in-app-billing-for-android/

关键是即使库存查询结果失败,我们也需要使SKU可消耗。以下是我如何做到的示例。

IabHelper.QueryInventoryFinishedListener mGotInventoryListener = new IabHelper.QueryInventoryFinishedListener() {
        public void onQueryInventoryFinished(IabResult result, Inventory inventory) {
            Log.d(TAG, "Query inventory finished.");

            // Have we been disposed of in the meantime? If so, quit.
            if (mHelper == null) return;

            // Is it a failure?
            if (result.isFailure()) {
                try {
                    Purchase purchase = new Purchase("inapp", "{\"packageName\":\"PACKAGE_NAME\","+
                            "\"orderId\":\"transactionId.android.test.purchased\","+
                            "\"productId\":\"android.test.purchased\",\"developerPayload\":\"\",\"purchaseTime\":0,"+
                            "\"purchaseState\":0,\"purchaseToken\":\"inapp:PACKAGE_NAME :android.test.purchased\"}",
                            "");
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                mHelper.consumeAsync(purchase, null);
                complain("Failed to query inventory: " + result);
                return;
            }

            Log.d(TAG, "Query inventory was successful.");

            /*
             * Check for items we own. Notice that for each purchase, we check
             * the developer payload to see if it's correct! See
             * verifyDeveloperPayload().
             */                   
        }
    };

将上面代码中的PACKAGE_NAME替换为您应用的程序包名称。


0

这对我有用:

  1. 调用BillingClient.querySkuDetailsAsync来查询是否可用
  2. 等待SkuDetailsResponseListener.onSkuDetailsResponse
  3. 再等500ms
  4. 开始使用BillingClient.launchBillingFlow ...购买

第3步不是必需的,因为当我收到onSkuDetailsResponse时应该可以,但不是,必须稍等片刻。购买成功后,不再显示“商品不可用错误”。这是我测试的方式:

  1. 清除我的应用数据
  2. 清除Google Play数据
  3. 运行应用
  4. 购买android.test.purchased
  5. 尝试购买我的物品(由于无法购买而失败)
  6. 使用我上面的解决方案,它的工作原理

-1

对于Cordova和Hybrid应用程序,您需要使用this.iap.subscribe(this.productId)方法订阅InAppPurchase。

以下是适合我的代码:

 getProdutIAP() {
        this.navCtrl.push('subscribeDialogPage');
        this.iap
            .getProducts(['productID1']).then((products: any) => {
                this.buy(products);
            })
            .catch((err) => {
                console.log(JSON.stringify(err));
                alert('Finished Purchase' + JSON.stringify(err));
                console.log(err);
            });
    }

    buy(products: any) {
        // this.getProdutIAP();
        // alert(products[0].productId);
        this.iap.subscribe(products[0].productId).then((buydata: any) => {
            alert('buy Purchase' + JSON.stringify(buydata));
            // this.sub();
        }).catch((err) => {
            // this.navCtrl.push('subscribeDialogPage');
            alert('buyError' + JSON.stringify(err));
        });
    }

    sub() {
        this.platform.ready().then(() => {
            this.iap
                .subscribe(this.productId)
                .then((data) => {
                    console.log('subscribe Purchase' + JSON.stringify(data));
                    alert('subscribe Purchase' + JSON.stringify(data));
                    this.getReceipt();
                }).catch((err) => {
                    this.getReceipt();
                    alert('subscribeError' + JSON.stringify(err));
                    console.log(err);
                });
        })
    }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.