关于Android计费的异常:末尾字符问题

8

我从崩溃报告中得到了以下异常信息:

java.lang.RuntimeException: Unable to start service com.problemio.BillingService@4132b868 with Intent { act=com.android.vending.billing.PURCHASE_STATE_CHANGED cmp=com.problemio/.BillingService (has extras) }: java.lang.IllegalArgumentException: utils.Base64DecoderException: single trailing character at offset 19
at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:2376)
at android.app.ActivityThread.access$1900(ActivityThread.java:123)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1210)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4424)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:551)
at dalvik.system.NativeStart.main(Native Method)
Caused by: java.lang.IllegalArgumentException: utils.Base64DecoderException: single trailing character at offset 19
at utils.Security.generatePublicKey(Security.java:199)
at utils.Security.verifyPurchase(Security.java:118)
at com.problemio.BillingService.purchaseStateChanged(BillingService.java:545)
at com.problemio.BillingService.handleCommand(BillingService.java:421)
at com.problemio.BillingService.onStart(BillingService.java:398)
at android.app.Service.onStartCommand(Service.java:438)
at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:2359)
... 10 more
Caused by: utils.Base64DecoderException: single trailing character at offset 19
at utils.Base64.decode(Base64.java:529)
at utils.Base64.decode(Base64.java:444)
at utils.Base64.decode(Base64.java:390)
at utils.Security.generatePublicKey(Security.java:189)
... 16 more
java.lang.IllegalArgumentException: utils.Base64DecoderException: single trailing character at offset 19
at utils.Security.generatePublicKey(Security.java:199)
at utils.Security.verifyPurchase(Security.java:118)
at com.problemio.BillingService.purchaseStateChanged(BillingService.java:545)
at com.problemio.BillingService.handleCommand(BillingService.java:421)
at com.problemio.BillingService.onStart(BillingService.java:398)
at android.app.Service.onStartCommand(Service.java:438)
at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:2359)
at android.app.ActivityThread.access$1900(ActivityThread.java:123)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1210)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4424)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:551)
at dalvik.system.NativeStart.main(Native Method)
Caused by: utils.Base64DecoderException: single trailing character at offset 19
at utils.Base64.decode(Base64.java:529)
at utils.Base64.decode(Base64.java:444)
at utils.Base64.decode(Base64.java:390)
at utils.Security.generatePublicKey(Security.java:189)
... 16 more
utils.Base64DecoderException: single trailing character at offset 19
at utils.Base64.decode(Base64.java:529)
at utils.Base64.decode(Base64.java:444)
at utils.Base64.decode(Base64.java:390)
at utils.Security.generatePublicKey(Security.java:189)
at utils.Security.verifyPurchase(Security.java:118)
at com.problemio.BillingService.purchaseStateChanged(BillingService.java:545)
at com.problemio.BillingService.handleCommand(BillingService.java:421)
at com.problemio.BillingService.onStart(BillingService.java:398)
at android.app.Service.onStartCommand(Service.java:438)
at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:2359)
at android.app.ActivityThread.access$1900(ActivityThread.java:123)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1210)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4424)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:551)
at dalvik.system.NativeStart.main(Native Method)

但我不太明白问题出在哪里。有什么建议吗?
它指向这个方法:
  /**
   * Decodes Base64 content using the supplied decodabet and returns
   * the decoded byte array.
   *
   * @param source the Base64 encoded data
   * @param off the offset of where to begin decoding
   * @param len the length of characters to decode
   * @param decodabet the decodabet for decoding Base64 content
   * @return decoded data
   */
  public static byte[] decode(byte[] source, int off, int len, byte[] decodabet)
      throws Base64DecoderException {
    int len34 = len * 3 / 4;
    byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output
    int outBuffPosn = 0;

    byte[] b4 = new byte[4];
    int b4Posn = 0;
    int i = 0;
    byte sbiCrop = 0;
    byte sbiDecode = 0;
    for (i = 0; i < len; i++) {
      sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits
      sbiDecode = decodabet[sbiCrop];

      if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better
        if (sbiDecode >= EQUALS_SIGN_ENC) {
          // An equals sign (for padding) must not occur at position 0 or 1
          // and must be the last byte[s] in the encoded value
          if (sbiCrop == EQUALS_SIGN) {
            int bytesLeft = len - i;
            byte lastByte = (byte) (source[len - 1 + off] & 0x7f);
            if (b4Posn == 0 || b4Posn == 1) {
              throw new Base64DecoderException(
                  "invalid padding byte '=' at byte offset " + i);
            } else if ((b4Posn == 3 && bytesLeft > 2)
                || (b4Posn == 4 && bytesLeft > 1)) {
              throw new Base64DecoderException(
                  "padding byte '=' falsely signals end of encoded value "
                      + "at offset " + i);
            } else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) {
              throw new Base64DecoderException(
                  "encoded value has invalid trailing byte");
            }
            break;
          }

          b4[b4Posn++] = sbiCrop;
          if (b4Posn == 4) {
            outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
            b4Posn = 0;
          }
        }
      } else {
        throw new Base64DecoderException("Bad Base64 input character at " + i
            + ": " + source[i + off] + "(decimal)");
      }
    }

    // Because web safe encoding allows non padding base64 encodes, we
    // need to pad the rest of the b4 buffer with equal signs when
    // b4Posn != 0.  There can be at most 2 equal signs at the end of
    // four characters, so the b4 buffer must have two or three
    // characters.  This also catches the case where the input is
    // padded with EQUALS_SIGN
    if (b4Posn != 0) {
      if (b4Posn == 1) {
        throw new Base64DecoderException("single trailing character at offset "
            + (len - 1));
      }
      b4[b4Posn++] = EQUALS_SIGN;
      outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
    }

    byte[] out = new byte[outBuffPosn];
    System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
    return out;
  }

错误似乎指向我的BillingService.java文件中的这些行。
at com.problemio.BillingService.purchaseStateChanged(BillingService.java:585)
at com.problemio.BillingService.handleCommand(BillingService.java:461)
at com.problemio.BillingService.onStart(BillingService.java:438)

585上的这条线是一条直线。
purchases = Security.verifyPurchase(signedData, signature);

在这种方法中:
private void purchaseStateChanged(int startId, String signedData, String signature) {
    ArrayList<Security.VerifiedPurchase> purchases;
    purchases = Security.verifyPurchase(signedData, signature);
    if (purchases == null) {
        return;
    }

而第461行是这一行:

purchaseStateChanged(startId, signedData, signature);

在这种方法中:
public void handleCommand(Intent intent, int startId) {
    String action = intent.getAction();
    if (Consts.DEBUG) {
        Log.i(TAG, "handleCommand() action: " + action);
    }
    if (Consts.ACTION_CONFIRM_NOTIFICATION.equals(action)) {
        String[] notifyIds = intent.getStringArrayExtra(Consts.NOTIFICATION_ID);
        confirmNotifications(startId, notifyIds);
    } else if (Consts.ACTION_GET_PURCHASE_INFORMATION.equals(action)) {
        String notifyId = intent.getStringExtra(Consts.NOTIFICATION_ID);
        getPurchaseInformation(startId, new String[] { notifyId });
    } else if (Consts.ACTION_PURCHASE_STATE_CHANGED.equals(action)) {
        String signedData = intent.getStringExtra(Consts.INAPP_SIGNED_DATA);
        String signature = intent.getStringExtra(Consts.INAPP_SIGNATURE);
        purchaseStateChanged(startId, signedData, signature);
    } else if (Consts.ACTION_RESPONSE_CODE.equals(action)) {
        long requestId = intent.getLongExtra(Consts.INAPP_REQUEST_ID, -1);
        int responseCodeIndex = intent.getIntExtra(Consts.INAPP_RESPONSE_CODE,
                ResponseCode.RESULT_ERROR.ordinal());
        ResponseCode responseCode = ResponseCode.valueOf(responseCodeIndex);
        checkResponseCode(requestId, responseCode);
    }
}

第438行是这一行:

handleCommand(intent, startId);

在这种方法中:
@Override
public void onStart(Intent intent, int startId) {
    handleCommand(intent, startId);
}

谢谢!


问题不在您上面包含的代码中,而是在您(Base64编码的)公钥中。我不是Android开发人员,但是:utils.Security是否是第三方计费库的一部分?它传递给generatePublicKey的是哪个公钥? - Michael Petrotta
@MichaelPetrotta Android会提供一个公钥,可以将其粘贴到代码中。我已经粘贴并检查过没有空格。utils.Security是Android代码的一部分,用于处理应用内购买相关的内容。 - GeekedOut
你的Base64公钥有问题。尝试自行解码查看。除了嵌入空格之外,Base64字符串还有很多其他可能出错的地方。 - Michael Petrotta
@MichaelPetrotta 很有趣...我该如何解码呢? :) 问题是我把它粘贴在那里时没有解码.....也许这就是问题所在 - 也许我应该自己编码并将编码后的字符串粘贴在那里? - GeekedOut
也许可以在使用密钥的代码中发布它。我找到的utils.Security.verifyPurchase示例代码将Base64字符串传递给Security.generatePublicKey。如果这与您的操作相匹配,那么您肯定需要在将其放入类之前对PK进行Base64编码。 - Michael Petrotta
显示剩余2条评论
3个回答

4

这种情况是一直发生还是只有一次/几次?你接收到的签名 purchaseStateChanged(startId, signedData, signature) 也是Base64编码,而错误提示说编码不正确。如果只是偶尔出现,可能是Google Play IAP服务器的故障。


1
我遇到这个问题是因为我使用了错误的公钥!请确保你使用的公钥是从市场上获取的公钥。

-2
我曾看到过上述错误,当给构造函数
IabHelper(Context ctx, String base64PublicKey)

传递一个错误的密钥时。
解决方案是:
进入Google Play开发者控制台网页。
选择应用程序,
然后在“服务和API”下,您会看到“此应用程序的许可证密钥”
使用该字符串。


另一个解决方案是进入Security.java文件并进行一些更改。请查看我在其中有注释“// ADD_THIS_LINE”的行。

    public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
    if (signedData == null) {
        Log.e(TAG, "data is null");
        return false;
    }

    boolean verified = false;
    if (!TextUtils.isEmpty(signature)) {
        PublicKey key = Security.generatePublicKey(base64PublicKey);

        if( key != null) // ADD_THIS_LINE
            verified = Security.verify(key, signedData, signature);

        if (!verified) {
            Log.w(TAG, "signature does not match data.");
            return false;
        }
    }
    return true;
}    
////////////////////////////////////////////////////////////////////////
//  :     :     
///////////////////////////////////////////////////////////////////////
 public static PublicKey generatePublicKey(String encodedPublicKey) {
    try {
        byte[] decodedKey = Base64.decode(encodedPublicKey);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
        return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
    } catch (NoSuchAlgorithmException e) {
        throw new RuntimeException(e);
    } catch (InvalidKeySpecException e) {
        Log.e(TAG, "Invalid key specification.");
        throw new IllegalArgumentException(e);
    } catch (Base64DecoderException e) {
        Log.e(TAG, "Base64 decoding failed.");
        return null;  // ADD_THIS_LINE
        // COMMENT_OUT_THIS_LINE:   throw new IllegalArgumentException(e);
    }
}


//////////////////////////////////////////////////////////////

你添加的这行代码所做的只是在没有密钥的情况下跳过签名检查,使其静默失败。 - Attila O.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接