Java tutorial
/* Copyright (c) 2012 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jp.alessandro.android.iab; import android.text.TextUtils; import android.util.Base64; import org.json.JSONException; import org.json.JSONObject; import java.io.UnsupportedEncodingException; import java.security.InvalidKeyException; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.Signature; import java.security.SignatureException; import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; import jp.alessandro.android.iab.logger.Logger; /** * Security-related methods. For a secure implementation, all of this code * should be implemented on a server that communicates with the * application on the device. For the sake of simplicity and clarity of this * example, this code is included here and is executed on the device. If you * must verify the purchases on the phone, you should obfuscate this code to * make it harder for an attacker to replace the code with stubs that treat all * purchases as verified. */ class Security { static final String KEY_FACTORY_ALGORITHM = "RSA"; static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; private final boolean mIsDebug; public Security(boolean isDebug) { mIsDebug = isDebug; } public boolean isDebug() { return mIsDebug; } /** * Verifies that the data was signed with the given signature, and returns * the verified purchase. The data is in JSON format and signed * with a private key. The data also contains the purchase state * and product ID of the purchase. * * @param logger the logger to use for printing events * @param base64PublicKey rsa public key generated by Google Play Developer Console * @param signedData the signed JSON string (signed, not encrypted) * @param signature the signature for the data, signed with the private key */ public boolean verifyPurchase(Logger logger, String base64PublicKey, String signedData, String signature) { if (TextUtils.isEmpty(base64PublicKey) || TextUtils.isEmpty(signedData) || TextUtils.isEmpty(signature)) { // In case of tests it will return true because test purchases doesn't have a signature if (mIsDebug && !TextUtils.isEmpty(signedData) && TextUtils.isEmpty(signature)) { return isTestingStaticResponse(logger, signedData); } logger.e(Logger.TAG, "Purchase verification failed: missing data."); return false; } try { PublicKey key = generatePublicKey(base64PublicKey); return verify(logger, key, signedData, signature); } catch (UnsupportedEncodingException e) { logger.e(Logger.TAG, e.getMessage(), e); } catch (NoSuchAlgorithmException e) { logger.e(Logger.TAG, e.getMessage(), e); } catch (InvalidKeySpecException e) { logger.e(Logger.TAG, e.getMessage(), e); } catch (InvalidKeyException e) { logger.e(Logger.TAG, e.getMessage(), e); } catch (SignatureException e) { logger.e(Logger.TAG, e.getMessage(), e); } catch (IllegalArgumentException e) { logger.e(Logger.TAG, e.getMessage(), e); } return false; } /** * Generates a PublicKey instance from a string containing the * Base64-encoded public key. * * @param encodedPublicKey rsa public key generated by Google Play Developer Console * @throws IllegalArgumentException if encodedPublicKey is invalid */ protected PublicKey generatePublicKey(String encodedPublicKey) throws NoSuchAlgorithmException, InvalidKeySpecException, IllegalArgumentException { byte[] decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT); KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM); return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey)); } /** * Verifies that the signature from the server matches the computed * signature on the data. Returns true if the data is correctly signed. * * @param logger the logger to use for printing events * @param publicKey rsa public key generated by Google Play Developer Console * @param signedData signed data from server * @param signature server signature * @return true if the data and signature match */ protected boolean verify(Logger logger, PublicKey publicKey, String signedData, String signature) throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, SignatureException, IllegalArgumentException { byte[] signatureBytes = Base64.decode(signature, Base64.DEFAULT); Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM); sig.initVerify(publicKey); sig.update(signedData.getBytes("UTF-8")); if (!sig.verify(signatureBytes)) { logger.e(Logger.TAG, "Signature verification failed."); return false; } return true; } /** * In case of tests it will return true because test purchases doesn't have a signature * See https://developer.android.com/google/play/billing/billing_testing.html * * @param signedData the signed JSON string (signed, not encrypted) */ private boolean isTestingStaticResponse(Logger logger, String signedData) { JSONObject obj; try { obj = new JSONObject(signedData); } catch (JSONException e) { return false; } String productId = obj.optString("productId"); logger.e(Logger.TAG, String.format("Testing static response: %s", productId)); if (productId.equals("android.test.purchased") || productId.equals("android.test.canceled") || productId.equals("android.test.refunded") || productId.equals("android.test.item_unavailable")) { return true; } return false; } }