Java tutorial
// Copyright 2010 Google Inc. All Rights Reserved. /* Copyright 2011 jfabrix101 (http://www.fabrizio-russo.it) * * 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 jfabrix101.billing; import java.security.InvalidKeyException; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.SecureRandom; import java.security.Signature; import java.security.SignatureException; import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; import jfabrix101.billing.BillingConsts.PurchaseState; import jfabrix101.security.AESObfuscator; import jfabrix101.security.Base64; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.content.Context; import android.content.SharedPreferences; import android.text.TextUtils; import android.util.Log; /** * 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. */ public class BillingSecurity { private static final String TAG = "jfabrix101-Security"; private static final String KEY_FACTORY_ALGORITHM = "RSA"; private static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; private static final SecureRandom RANDOM = new SecureRandom(); public static final byte[] secretKey = new byte[] { 41, -90, -116, -41, 66, -53, 121, -110, -127, -96, -88, -22, 127, 115, 1, 73, 57, 110, 48, -116 }; /** * This keeps track of the nonces that we generated and sent to the * server. We need to keep track of these until we get back the purchase * state and send a confirmation message back to Android Market. If we are * killed and lose this list of nonces, it is not fatal. Android Market will * send us a new "notify" message and we will re-generate a new nonce. * This has to be "static" so that the {@link BillingReceiver} can * check if a nonce exists. */ private static HashSet<Long> sKnownNonces = new HashSet<Long>(); /** * A class to hold the verified purchase information. */ public static class VerifiedPurchase { public PurchaseState purchaseState; public String notificationId; public String productId; public String orderId; public long purchaseTime; public String developerPayload; public VerifiedPurchase(PurchaseState purchaseState, String notificationId, String productId, String orderId, long purchaseTime, String developerPayload) { this.purchaseState = purchaseState; this.notificationId = notificationId; this.productId = productId; this.orderId = orderId; this.purchaseTime = purchaseTime; this.developerPayload = developerPayload; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("purchaseState:").append(purchaseState).append(", notificationId:").append(notificationId) .append(", productId:").append(productId).append(", orderId:").append(orderId) .append(", purchaseTime:").append(new Date(purchaseTime)).append(", developerPayload:") .append(developerPayload); return sb.toString(); } } /** Generates a nonce (a random number used once). */ public static long generateNonce() { long nonce = RANDOM.nextLong(); sKnownNonces.add(nonce); return nonce; } public static void removeNonce(long nonce) { sKnownNonces.remove(nonce); } public static boolean isNonceKnown(long nonce) { return sKnownNonces.contains(nonce); } /** * Verifies that the data was signed with the given signature, and returns * the list of verified purchases. The data is in JSON format and contains * a nonce (number used once) that we generated and that was signed * (as part of the whole data string) with a private key. The data also * contains the {@link PurchaseState} and product ID of the purchase. * In the general case, there can be an array of purchase transactions * because there may be delays in processing the purchase on the backend * and then several purchases can be batched together. * @param signedData the signed JSON string (signed, not encrypted) * @param signature the signature for the data, signed with the private key */ public static ArrayList<VerifiedPurchase> verifyPurchase(Context context, String signedData, String signature) { if (signedData == null) { Log.e(TAG, "data is null"); return null; } if (BillingConsts.DEBUG) { Log.i(TAG, "signedData: " + signedData); } boolean verified = false; if (!TextUtils.isEmpty(signature)) { /** * Compute your public key (that you got from the Android Market publisher site). * * Instead of just storing the entire literal string here embedded in the * program, construct the key at runtime from pieces or * use bit manipulation (for example, XOR with some other string) to hide * the actual key. The key itself is not secret information, but we don't * want to make it easy for an adversary to replace the public key with one * of their own and then fake messages from the server. * * Generally, encryption keys / passwords should only be kept in memory * long enough to perform the operation they need to perform. */ SharedPreferences pref = context.getSharedPreferences("billingPK", 0); AESObfuscator obfuscator = AESObfuscator.getDefaultObfuscator(context); String publicKey = ""; try { publicKey = obfuscator.unobfuscate(pref.getString("pk", "")); } catch (Exception e) { Log.e(TAG, "Error getting publicc key from repository"); return null; } PublicKey key = BillingSecurity.generatePublicKey(publicKey); verified = BillingSecurity.verify(key, signedData, signature); if (!verified) { Log.w(TAG, "signature does not match data."); return null; } } JSONObject jObject; JSONArray jTransactionsArray = null; int numTransactions = 0; long nonce = 0L; try { jObject = new JSONObject(signedData); // The nonce might be null if the user backed out of the buy page. nonce = jObject.optLong("nonce"); jTransactionsArray = jObject.optJSONArray("orders"); if (jTransactionsArray != null) { numTransactions = jTransactionsArray.length(); } } catch (JSONException e) { return null; } if (!BillingSecurity.isNonceKnown(nonce)) { Log.w(TAG, "Nonce not found: " + nonce); return null; } ArrayList<VerifiedPurchase> purchases = new ArrayList<VerifiedPurchase>(); try { for (int i = 0; i < numTransactions; i++) { JSONObject jElement = jTransactionsArray.getJSONObject(i); int response = jElement.getInt("purchaseState"); PurchaseState purchaseState = PurchaseState.valueOf(response); String productId = jElement.getString("productId"); //String packageName = jElement.getString("packageName"); long purchaseTime = jElement.getLong("purchaseTime"); String orderId = jElement.optString("orderId", ""); String notifyId = null; if (jElement.has("notificationId")) { notifyId = jElement.getString("notificationId"); } String developerPayload = jElement.optString("developerPayload", null); // If the purchase state is PURCHASED, then we require a verified nonce. if (purchaseState == PurchaseState.PURCHASED && !verified) { continue; } purchases.add(new VerifiedPurchase(purchaseState, notifyId, productId, orderId, purchaseTime, developerPayload)); } } catch (JSONException e) { Log.e(TAG, "JSON exception: ", e); return null; } removeNonce(nonce); return purchases; } /** * Generates a PublicKey instance from a string containing the * Base64-encoded public key. * * @param encodedPublicKey Base64-encoded public key * @throws IllegalArgumentException if encodedPublicKey is invalid */ 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 (Exception e) { Log.e(TAG, "Base64 decoding failed."); throw new IllegalArgumentException(e); } } /** * Verifies that the signature from the server matches the computed * signature on the data. Returns true if the data is correctly signed. * * @param publicKey public key associated with the developer account * @param signedData signed data from server * @param signature server signature * @return true if the data and signature match */ public static boolean verify(PublicKey publicKey, String signedData, String signature) { if (BillingConsts.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 (Exception e) { Log.e(TAG, "Base64 decoding failed."); } return false; } public static void registerPublicKey(Context context, String publicKey) { SharedPreferences pref = context.getSharedPreferences("billingPK", 0); AESObfuscator obfuscator = AESObfuscator.getDefaultObfuscator(context); String obuscatedKey = obfuscator.obfuscate(publicKey); pref.edit().putString("pk", obuscatedKey).commit(); } }