From 8e45b14c6158deaab5f0f67c90103896fdc2fdc1 Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Wed, 29 Feb 2012 18:30:52 +0300 Subject: add in-app billing donation stuff --- .../vending/billing/IMarketBillingService.aidl | 24 + src/org/fox/ttrss/BillingConstants.java | 63 +++ src/org/fox/ttrss/BillingHelper.java | 267 ++++++++++ src/org/fox/ttrss/BillingReceiver.java | 57 ++ src/org/fox/ttrss/BillingSecurity.java | 258 +++++++++ src/org/fox/ttrss/BillingService.java | 60 +++ src/org/fox/ttrss/MainActivity.java | 55 +- src/org/fox/ttrss/util/Base64.java | 582 +++++++++++++++++++++ src/org/fox/ttrss/util/Base64DecoderException.java | 32 ++ 9 files changed, 1393 insertions(+), 5 deletions(-) create mode 100644 src/com/android/vending/billing/IMarketBillingService.aidl create mode 100644 src/org/fox/ttrss/BillingConstants.java create mode 100644 src/org/fox/ttrss/BillingHelper.java create mode 100644 src/org/fox/ttrss/BillingReceiver.java create mode 100644 src/org/fox/ttrss/BillingSecurity.java create mode 100644 src/org/fox/ttrss/BillingService.java create mode 100644 src/org/fox/ttrss/util/Base64.java create mode 100644 src/org/fox/ttrss/util/Base64DecoderException.java (limited to 'src') diff --git a/src/com/android/vending/billing/IMarketBillingService.aidl b/src/com/android/vending/billing/IMarketBillingService.aidl new file mode 100644 index 00000000..6884b41f --- /dev/null +++ b/src/com/android/vending/billing/IMarketBillingService.aidl @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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 com.android.vending.billing; + +import android.os.Bundle; + +interface IMarketBillingService { + /** Given the arguments in bundle form, returns a bundle for results. */ + Bundle sendBillingRequest(in Bundle bundle); +} diff --git a/src/org/fox/ttrss/BillingConstants.java b/src/org/fox/ttrss/BillingConstants.java new file mode 100644 index 00000000..ea8b454c --- /dev/null +++ b/src/org/fox/ttrss/BillingConstants.java @@ -0,0 +1,63 @@ +package org.fox.ttrss; + + +public class BillingConstants { + + // The response codes for a request, defined by Android Market. + public enum ResponseCode { + RESULT_OK, + RESULT_USER_CANCELED, + RESULT_SERVICE_UNAVAILABLE, + RESULT_BILLING_UNAVAILABLE, + RESULT_ITEM_UNAVAILABLE, + RESULT_DEVELOPER_ERROR, + RESULT_ERROR; + + // Converts from an ordinal value to the ResponseCode + public static ResponseCode valueOf(int index) { + ResponseCode[] values = ResponseCode.values(); + if (index < 0 || index >= values.length) { + return RESULT_ERROR; + } + return values[index]; + } + } + + // The possible states of an in-app purchase, as defined by Android Market. + public enum PurchaseState { + // Responses to requestPurchase or restoreTransactions. + PURCHASED, // User was charged for the order. + CANCELED, // The charge failed on the server. + REFUNDED; // User received a refund for the order. + + // Converts from an ordinal value to the PurchaseState + public static PurchaseState valueOf(int index) { + PurchaseState[] values = PurchaseState.values(); + if (index < 0 || index >= values.length) { + return CANCELED; + } + return values[index]; + } + } + + // These are the names of the extras that are passed in an intent from + // Market to this application and cannot be changed. + public static final String NOTIFICATION_ID = "notification_id"; + public static final String INAPP_SIGNED_DATA = "inapp_signed_data"; + public static final String INAPP_SIGNATURE = "inapp_signature"; + public static final String INAPP_REQUEST_ID = "request_id"; + public static final String INAPP_RESPONSE_CODE = "response_code"; + + // Intent actions that we send from the BillingReceiver to the + // BillingService. Defined by this application. + public static final String ACTION_CONFIRM_NOTIFICATION = "com.example.dungeons.CONFIRM_NOTIFICATION"; + public static final String ACTION_GET_PURCHASE_INFORMATION = "com.example.dungeons.GET_PURCHASE_INFORMATION"; + public static final String ACTION_RESTORE_TRANSACTIONS = "com.example.dungeons.RESTORE_TRANSACTIONS"; + + // Intent actions that we receive in the BillingReceiver from Market. + // These are defined by Market and cannot be changed. + public static final String ACTION_NOTIFY = "com.android.vending.billing.IN_APP_NOTIFY"; + public static final String ACTION_RESPONSE_CODE = "com.android.vending.billing.RESPONSE_CODE"; + public static final String ACTION_PURCHASE_STATE_CHANGED = "com.android.vending.billing.PURCHASE_STATE_CHANGED"; + +} diff --git a/src/org/fox/ttrss/BillingHelper.java b/src/org/fox/ttrss/BillingHelper.java new file mode 100644 index 00000000..e29fd2f7 --- /dev/null +++ b/src/org/fox/ttrss/BillingHelper.java @@ -0,0 +1,267 @@ +package org.fox.ttrss; + +import java.util.ArrayList; + +import android.app.PendingIntent; +import android.app.PendingIntent.CanceledException; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.RemoteException; +import android.util.Log; + +import com.android.vending.billing.IMarketBillingService; +import org.fox.ttrss.BillingSecurity.VerifiedPurchase; +import org.fox.ttrss.BillingConstants.ResponseCode; + +public class BillingHelper { + + private static final String TAG = "BillingService"; + + private static IMarketBillingService mService; + private static Context mContext; + private static Handler mCompletedHandler; + + protected static VerifiedPurchase latestPurchase; + + protected static void instantiateHelper(Context context, IMarketBillingService service) { + mService = service; + mContext = context; + } + + protected static void setCompletedHandler(Handler handler){ + mCompletedHandler = handler; + } + + protected static boolean isBillingSupported() { + if (amIDead()) { + return false; + } + Bundle request = makeRequestBundle("CHECK_BILLING_SUPPORTED"); + if (mService != null) { + try { + Bundle response = mService.sendBillingRequest(request); + ResponseCode code = ResponseCode.valueOf((Integer) response.get("RESPONSE_CODE")); + Log.i(TAG, "isBillingSupported response was: " + code.toString()); + if (ResponseCode.RESULT_OK.equals(code)) { + return true; + } else { + return false; + } + } catch (RemoteException e) { + Log.e(TAG, "isBillingSupported response was: RemoteException", e); + return false; + } + } else { + Log.i(TAG, "isBillingSupported response was: BillingService.mService = null"); + return false; + } + } + + /** + * A REQUEST_PURCHASE request also triggers two asynchronous responses (broadcast intents). + * First, the Android Market application sends a RESPONSE_CODE broadcast intent, which provides error information about the request. (which I ignore) + * Next, if the request was successful, the Android Market application sends an IN_APP_NOTIFY broadcast intent. + * This message contains a notification ID, which you can use to retrieve the transaction details for the REQUEST_PURCHASE + * @param activityContext + * @param itemId + */ + protected static void requestPurchase(Context activityContext, String itemId){ + if (amIDead()) { + return; + } + Log.i(TAG, "requestPurchase()"); + Bundle request = makeRequestBundle("REQUEST_PURCHASE"); + request.putString("ITEM_ID", itemId); + try { + Bundle response = mService.sendBillingRequest(request); + + //The RESPONSE_CODE key provides you with the status of the request + Integer responseCodeIndex = (Integer) response.get("RESPONSE_CODE"); + //The PURCHASE_INTENT key provides you with a PendingIntent, which you can use to launch the checkout UI + PendingIntent pendingIntent = (PendingIntent) response.get("PURCHASE_INTENT"); + //The REQUEST_ID key provides you with a unique request identifier for the request + Long requestIndentifier = (Long) response.get("REQUEST_ID"); + Log.i(TAG, "current request is:" + requestIndentifier); + BillingConstants.ResponseCode responseCode = BillingConstants.ResponseCode.valueOf(responseCodeIndex); + Log.i(TAG, "REQUEST_PURCHASE Sync Response code: "+responseCode.toString()); + + startBuyPageActivity(pendingIntent, new Intent(), activityContext); + } catch (RemoteException e) { + Log.e(TAG, "Failed, internet error maybe", e); + Log.e(TAG, "Billing supported: "+isBillingSupported()); + } + } + + /** + * A GET_PURCHASE_INFORMATION request also triggers two asynchronous responses (broadcast intents). + * First, the Android Market application sends a RESPONSE_CODE broadcast intent, which provides status and error information about the request. (which I ignore) + * Next, if the request was successful, the Android Market application sends a PURCHASE_STATE_CHANGED broadcast intent. + * This message contains detailed transaction information. + * The transaction information is contained in a signed JSON string (unencrypted). + * The message includes the signature so you can verify the integrity of the signed string + * @param notifyIds + */ + protected static void getPurchaseInformation(String[] notifyIds){ + if (amIDead()) { + return; + } + Log.i(TAG, "getPurchaseInformation()"); + Bundle request = makeRequestBundle("GET_PURCHASE_INFORMATION"); + // The REQUEST_NONCE key contains a cryptographically secure nonce (number used once) that you must generate. + // The Android Market application returns this nonce with the PURCHASE_STATE_CHANGED broadcast intent so you can verify the integrity of the transaction information. + request.putLong("NONCE", BillingSecurity.generateNonce()); + // The NOTIFY_IDS key contains an array of notification IDs, which you received in the IN_APP_NOTIFY broadcast intent. + request.putStringArray("NOTIFY_IDS", notifyIds); + try { + Bundle response = mService.sendBillingRequest(request); + + //The REQUEST_ID key provides you with a unique request identifier for the request + Long requestIndentifier = (Long) response.get("REQUEST_ID"); + Log.i(TAG, "current request is:" + requestIndentifier); + //The RESPONSE_CODE key provides you with the status of the request + Integer responseCodeIndex = (Integer) response.get("RESPONSE_CODE"); + BillingConstants.ResponseCode responseCode = BillingConstants.ResponseCode.valueOf(responseCodeIndex); + Log.i(TAG, "GET_PURCHASE_INFORMATION Sync Response code: "+responseCode.toString()); + + } catch (RemoteException e) { + Log.e(TAG, "Failed, internet error maybe", e); + Log.e(TAG, "Billing supported: "+isBillingSupported()); + } + } + + /** + * To acknowledge that you received transaction information you send a + * CONFIRM_NOTIFICATIONS request. + * + * A CONFIRM_NOTIFICATIONS request triggers a single asynchronous response�a RESPONSE_CODE broadcast intent. + * This broadcast intent provides status and error information about the request. + * + * Note: As a best practice, you should not send a CONFIRM_NOTIFICATIONS request for a purchased item until you have delivered the item to the user. + * This way, if your application crashes or something else prevents your application from delivering the product, + * your application will still receive an IN_APP_NOTIFY broadcast intent from Android Market indicating that you need to deliver the product + * @param notifyIds + */ + protected static void confirmTransaction(String[] notifyIds) { + if (amIDead()) { + return; + } + Log.i(TAG, "confirmTransaction()"); + Bundle request = makeRequestBundle("CONFIRM_NOTIFICATIONS"); + request.putStringArray("NOTIFY_IDS", notifyIds); + try { + Bundle response = mService.sendBillingRequest(request); + + //The REQUEST_ID key provides you with a unique request identifier for the request + Long requestIndentifier = (Long) response.get("REQUEST_ID"); + Log.i(TAG, "current request is:" + requestIndentifier); + + //The RESPONSE_CODE key provides you with the status of the request + Integer responseCodeIndex = (Integer) response.get("RESPONSE_CODE"); + BillingConstants.ResponseCode responseCode = BillingConstants.ResponseCode.valueOf(responseCodeIndex); + + Log.i(TAG, "CONFIRM_NOTIFICATIONS Sync Response code: "+responseCode.toString()); + } catch (RemoteException e) { + Log.e(TAG, "Failed, internet error maybe", e); + Log.e(TAG, "Billing supported: " + isBillingSupported()); + } + } + + /** + * + * Can be used for when a user has reinstalled the app to give back prior purchases. + * if an item for sale's purchase type is "managed per user account" this means google will have a record ofthis transaction + * + * A RESTORE_TRANSACTIONS request also triggers two asynchronous responses (broadcast intents). + * First, the Android Market application sends a RESPONSE_CODE broadcast intent, which provides status and error information about the request. + * Next, if the request was successful, the Android Market application sends a PURCHASE_STATE_CHANGED broadcast intent. + * This message contains the detailed transaction information. The transaction information is contained in a signed JSON string (unencrypted). + * The message includes the signature so you can verify the integrity of the signed string + * @param nonce + */ + protected static void restoreTransactionInformation(Long nonce) { + if (amIDead()) { + return; + } + Log.i(TAG, "confirmTransaction()"); + Bundle request = makeRequestBundle("RESTORE_TRANSACTIONS"); + // The REQUEST_NONCE key contains a cryptographically secure nonce (number used once) that you must generate + request.putLong("NONCE", nonce); + try { + Bundle response = mService.sendBillingRequest(request); + + //The REQUEST_ID key provides you with a unique request identifier for the request + Long requestIndentifier = (Long) response.get("REQUEST_ID"); + Log.i(TAG, "current request is:" + requestIndentifier); + + //The RESPONSE_CODE key provides you with the status of the request + Integer responseCodeIndex = (Integer) response.get("RESPONSE_CODE"); + BillingConstants.ResponseCode responseCode = BillingConstants.ResponseCode.valueOf(responseCodeIndex); + Log.i(TAG, "RESTORE_TRANSACTIONS Sync Response code: "+responseCode.toString()); + } catch (RemoteException e) { + Log.e(TAG, "Failed, internet error maybe", e); + Log.e(TAG, "Billing supported: " + isBillingSupported()); + } + } + + private static boolean amIDead() { + if (mService == null || mContext == null) { + Log.e(TAG, "BillingHelper not fully instantiated"); + return true; + } else { + return false; + } + } + + private static Bundle makeRequestBundle(String method) { + Bundle request = new Bundle(); + request.putString("BILLING_REQUEST", method); + request.putInt("API_VERSION", 1); + request.putString("PACKAGE_NAME", mContext.getPackageName()); + return request; + } + + /** + * + * + * You must launch the pending intent from an activity context and not an application context + * You cannot use the singleTop launch mode to launch the pending intent + * @param pendingIntent + * @param intent + * @param context + */ + private static void startBuyPageActivity(PendingIntent pendingIntent, Intent intent, Context context){ + //TODO add above 2.0 implementation with reflection, for now just using 1.6 implem + + // This is on Android 1.6. The in-app checkout page activity will be on its + // own separate activity stack instead of on the activity stack of + // the application. + try { + pendingIntent.send(context, 0, intent); + } catch (CanceledException e){ + Log.e(TAG, "startBuyPageActivity CanceledException"); + } + } + + protected static void verifyPurchase(String signedData, String signature) { + ArrayList purchases = BillingSecurity.verifyPurchase(signedData, signature); + latestPurchase = purchases.get(0); + + confirmTransaction(new String[]{latestPurchase.notificationId}); + + if(mCompletedHandler != null){ + mCompletedHandler.sendEmptyMessage(0); + } else { + Log.e(TAG, "verifyPurchase error. Handler not instantiated. Have you called setCompletedHandler()?"); + } + } + + public static void stopService(){ + mContext.stopService(new Intent(mContext, BillingService.class)); + mService = null; + mContext = null; + mCompletedHandler = null; + Log.i(TAG, "Stopping Service"); + } +} diff --git a/src/org/fox/ttrss/BillingReceiver.java b/src/org/fox/ttrss/BillingReceiver.java new file mode 100644 index 00000000..42cd5df6 --- /dev/null +++ b/src/org/fox/ttrss/BillingReceiver.java @@ -0,0 +1,57 @@ +package org.fox.ttrss; + +import static org.fox.ttrss.BillingConstants.*; + +import java.util.ArrayList; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Message; +import android.util.Log; + +import org.fox.ttrss.BillingConstants; +import org.fox.ttrss.BillingSecurity.VerifiedPurchase; + +public class BillingReceiver extends BroadcastReceiver { + + private static final String TAG = "BillingService"; + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + Log.i(TAG, "Received action: " + action); + if (ACTION_PURCHASE_STATE_CHANGED.equals(action)) { + String signedData = intent.getStringExtra(INAPP_SIGNED_DATA); + String signature = intent.getStringExtra(INAPP_SIGNATURE); + purchaseStateChanged(context, signedData, signature); + } else if (ACTION_NOTIFY.equals(action)) { + String notifyId = intent.getStringExtra(NOTIFICATION_ID); + notify(context, notifyId); + } else if (ACTION_RESPONSE_CODE.equals(action)) { + long requestId = intent.getLongExtra(INAPP_REQUEST_ID, -1); + int responseCodeIndex = intent.getIntExtra(INAPP_RESPONSE_CODE, BillingConstants.ResponseCode.RESULT_ERROR.ordinal()); + checkResponseCode(context, requestId, responseCodeIndex); + } else { + Log.e(TAG, "unexpected action: " + action); + } + } + + + private void purchaseStateChanged(Context context, String signedData, String signature) { + Log.i(TAG, "purchaseStateChanged got signedData: " + signedData); + Log.i(TAG, "purchaseStateChanged got signature: " + signature); + BillingHelper.verifyPurchase(signedData, signature); + } + + private void notify(Context context, String notifyId) { + Log.i(TAG, "notify got id: " + notifyId); + String[] notifyIds = {notifyId}; + BillingHelper.getPurchaseInformation(notifyIds); + } + + private void checkResponseCode(Context context, long requestId, int responseCodeIndex) { + Log.i(TAG, "checkResponseCode got requestId: " + requestId); + Log.i(TAG, "checkResponseCode got responseCode: " + BillingConstants.ResponseCode.valueOf(responseCodeIndex)); + } +} \ No newline at end of file diff --git a/src/org/fox/ttrss/BillingSecurity.java b/src/org/fox/ttrss/BillingSecurity.java new file mode 100644 index 00000000..26e19b3e --- /dev/null +++ b/src/org/fox/ttrss/BillingSecurity.java @@ -0,0 +1,258 @@ +// Copyright 2010 Google Inc. All Rights Reserved. + +package org.fox.ttrss; + +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.HashSet; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.text.TextUtils; +import android.util.Log; + +import org.fox.ttrss.BillingConstants.PurchaseState; +import org.fox.ttrss.util.Base64; +import org.fox.ttrss.util.Base64DecoderException; + +/** + * 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 = "BillingService"; + + private static final String KEY_FACTORY_ALGORITHM = "RSA"; + private static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; + private static final SecureRandom RANDOM = new SecureRandom(); + + /** + * 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 sKnownNonces = new HashSet(); + + /** + * 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; + } + + public boolean isPurchased(){ + return purchaseState.equals(PurchaseState.PURCHASED); + } + + + } + + /** Generates a nonce (a random number used once). */ + public static long generateNonce() { + long nonce = RANDOM.nextLong(); + Log.i(TAG, "Nonce generateD: "+nonce); + 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 verifyPurchase(String signedData, String signature) { + if (signedData == null) { + Log.e(TAG, "data is null"); + return null; + } + 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. + */ + String base64EncodedPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApLWBv8eFC4f7h6gz3VE87XX2nqJB2KL2yNnNawmgaL/0nd6nXvVRiZ3iXLLP9k8RpLJ6rZPV778z8WzDLZATV3b2nh21KgjSNoG4em1oSf7pW4+AujqjLfNVRsXoJIWG+OMMd9o9l/D2YJTCXzSvgFIfF5EJRg6APZHEVrVJo8iXwnYM1tFfLjPfp10MtjLmD5tZW8o3hTmXJ3ZMDI12PL22G4KaE+BuQqI6PZ22m/pA85R6AuhNo2IUSE4XFUE8i7ANWDvdfDzQ5J0TTWAeHmUQCstdZ48z+6AjqD3L2omS/dKoBnlYxEUZms3iUa1/Co40nWU7sc2hqpmfNiG5oQIDAQAB"; + PublicKey key = BillingSecurity.generatePublicKey(base64EncodedPublicKey); + 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 purchases = new ArrayList(); + 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 (Base64DecoderException e) { + Log.e(TAG, "Base64DecoderException.", e); + return null; + } + } + + /** + * 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) { + 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, "Base64DecoderException.", e); + } + return false; + } +} diff --git a/src/org/fox/ttrss/BillingService.java b/src/org/fox/ttrss/BillingService.java new file mode 100644 index 00000000..e003df32 --- /dev/null +++ b/src/org/fox/ttrss/BillingService.java @@ -0,0 +1,60 @@ +package org.fox.ttrss; + +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.util.Log; + +import com.android.vending.billing.IMarketBillingService; + +public class BillingService extends Service implements ServiceConnection{ + + private static final String TAG = "BillingService"; + + /** The service connection to the remote MarketBillingService. */ + private IMarketBillingService mService; + + @Override + public void onCreate() { + super.onCreate(); + Log.i(TAG, "Service starting with onCreate"); + + try { + boolean bindResult = bindService(new Intent("com.android.vending.billing.MarketBillingService.BIND"), this, Context.BIND_AUTO_CREATE); + if(bindResult){ + Log.i(TAG,"Market Billing Service Successfully Bound"); + } else { + Log.e(TAG,"Market Billing Service could not be bound."); + //TODO stop user continuing + } + } catch (SecurityException e){ + Log.e(TAG,"Market Billing Service could not be bound. SecurityException: "+e); + //TODO stop user continuing + } + } + + public void setContext(Context context) { + attachBaseContext(context); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + Log.i(TAG, "Market Billing Service Connected."); + mService = IMarketBillingService.Stub.asInterface(service); + BillingHelper.instantiateHelper(getBaseContext(), mService); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + + } + +} diff --git a/src/org/fox/ttrss/MainActivity.java b/src/org/fox/ttrss/MainActivity.java index dd3295c8..5d7ecd09 100644 --- a/src/org/fox/ttrss/MainActivity.java +++ b/src/org/fox/ttrss/MainActivity.java @@ -23,6 +23,7 @@ import android.database.sqlite.SQLiteDatabase; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Bundle; +import android.os.Handler; import android.preference.PreferenceManager; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; @@ -69,6 +70,7 @@ public class MainActivity extends FragmentActivity implements OnlineServices { private boolean m_isLoggingIn = false; private boolean m_isOffline = false; private boolean m_offlineModeReady = false; + private int m_selectedProduct = -1; private SQLiteDatabase m_readableDb; private SQLiteDatabase m_writableDb; @@ -443,7 +445,7 @@ public class MainActivity extends FragmentActivity implements OnlineServices { } super.onCreate(savedInstanceState); - + m_themeName = m_prefs.getString("theme", "THEME_DARK"); if (savedInstanceState != null) { @@ -882,6 +884,45 @@ public class MainActivity extends FragmentActivity implements OnlineServices { .findFragmentById(R.id.headlines_fragment); switch (item.getItemId()) { + case R.id.donate: + if (true) { + CharSequence[] items = { "Silver Donation ($2)", "Gold Donation ($5)", "Platinum Donation ($10)" }; + + Dialog dialog = new Dialog(MainActivity.this); + AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this) + .setTitle(R.string.donate_select) + .setSingleChoiceItems(items, -1, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + m_selectedProduct = which; + } + }).setNegativeButton(R.string.dialog_close, new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }).setPositiveButton(R.string.donate_do, new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + if (m_selectedProduct != -1 && m_selectedProduct < 3) { + CharSequence[] products = { "donation_silver", "donation_gold", "donation_platinum2" }; + + Log.d(TAG, "Selected product: " + products[m_selectedProduct]); + + BillingHelper.requestPurchase(MainActivity.this, (String) products[m_selectedProduct]); + + dialog.dismiss(); + } + } + }); + + dialog = builder.create(); + dialog.show(); + } + return true; case android.R.id.home: goBack(false); return true; @@ -1130,7 +1171,7 @@ public class MainActivity extends FragmentActivity implements OnlineServices { Dialog dialog = new Dialog(MainActivity.this); AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this) - .setTitle("Set labels") + .setTitle(R.string.article_set_labels) .setMultiChoiceItems(items, checkedItems, new OnMultiChoiceClickListener() { @Override @@ -1152,7 +1193,7 @@ public class MainActivity extends FragmentActivity implements OnlineServices { req.execute(map); } - }).setPositiveButton("Close", new OnClickListener() { + }).setPositiveButton(R.string.dialog_close, new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -1358,7 +1399,9 @@ public class MainActivity extends FragmentActivity implements OnlineServices { } m_menu.findItem(R.id.set_labels).setEnabled(m_apiLevel >= 1); - + + m_menu.findItem(R.id.donate).setVisible(BillingHelper.isBillingSupported()); + } else { m_menu.setGroupVisible(R.id.menu_group_logged_in, false); m_menu.setGroupVisible(R.id.menu_group_logged_out, true); @@ -1410,8 +1453,10 @@ public class MainActivity extends FragmentActivity implements OnlineServices { m_isOffline = false; + startService(new Intent(MainActivity.this, BillingService.class)); + initMainMenu(); - + if (m_refreshTask != null) { m_refreshTask.cancel(); m_refreshTask = null; diff --git a/src/org/fox/ttrss/util/Base64.java b/src/org/fox/ttrss/util/Base64.java new file mode 100644 index 00000000..5c8136db --- /dev/null +++ b/src/org/fox/ttrss/util/Base64.java @@ -0,0 +1,582 @@ +// Portions copyright 2002, 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 org.fox.ttrss.util; + +// This code was converted from code at http://iharder.sourceforge.net/base64/ +// Lots of extraneous features were removed. +/* The original code said: + *

+ * I am placing this code in the Public Domain. Do with it as you will. + * This software comes with no guarantees or warranties but with + * plenty of well-wishing instead! + * Please visit + * http://iharder.net/xmlizable + * periodically to check for updates or to contribute improvements. + *

+ * + * @author Robert Harder + * @author rharder@usa.net + * @version 1.3 + */ + +/** + * Base64 converter class. This code is not a complete MIME encoder; it simply + * converts binary data to base64 data and back. + * + *

+ * Note {@link CharBase64} is a GWT-compatible implementation of this class. + */ +public class Base64 { + /** Specify encoding (value is {@code true}). */ + public final static boolean ENCODE = true; + + /** Specify decoding (value is {@code false}). */ + public final static boolean DECODE = false; + + /** The equals sign (=) as a byte. */ + private final static byte EQUALS_SIGN = (byte) '='; + + /** The new line character (\n) as a byte. */ + private final static byte NEW_LINE = (byte) '\n'; + + /** + * The 64 valid Base64 values. + */ + private final static byte[] ALPHABET = { (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', + (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', (byte) 'Q', + (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', (byte) 'a', + (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', + (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', + (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', + (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) '+', (byte) '/' }; + + /** + * The 64 valid web safe Base64 values. + */ + private final static byte[] WEBSAFE_ALPHABET = { (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', + (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', (byte) 'Q', + (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', (byte) 'a', + (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', + (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', + (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', + (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) '-', (byte) '_' }; + + /** + * Translates a Base64 value to either its 6-bit reconstruction value or a + * negative number indicating some other meaning. + **/ + private final static byte[] DECODABET = { -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal + // 0 + // - + // 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - + // 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 + 62, // Plus sign at decimal 43 + -9, -9, -9, // Decimal 44 - 46 + 63, // Slash at decimal 47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through + // 'N' + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' + // through 'Z' + -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' + // through 'm' + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' + // through 'z' + -9, -9, -9, -9, -9 // Decimal 123 - 127 + /* + * ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 + */ + }; + + /** The web safe decodabet */ + private final static byte[] WEBSAFE_DECODABET = { -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal + // 0 + // - + // 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - + // 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44 + 62, // Dash '-' sign at decimal 45 + -9, -9, // Decimal 46-47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through + // 'N' + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' + // through 'Z' + -9, -9, -9, -9, // Decimal 91-94 + 63, // Underscore '_' at decimal 95 + -9, // Decimal 96 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' + // through 'm' + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' + // through 'z' + -9, -9, -9, -9, -9 // Decimal 123 - 127 + /* + * ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 + */ + }; + + // Indicates white space in encoding + private final static byte WHITE_SPACE_ENC = -5; + // Indicates equals sign in encoding + private final static byte EQUALS_SIGN_ENC = -1; + + /** Defeats instantiation. */ + private Base64() { + } + + /* ******** E N C O D I N G M E T H O D S ******** */ + + /** + * Encodes up to three bytes of the array source and writes the + * resulting four Base64 bytes to destination. The source and + * destination arrays can be manipulated anywhere along their length by + * specifying srcOffset and destOffset. This method + * does not check to make sure your arrays are large enough to accommodate + * srcOffset + 3 for the source array or + * destOffset + 4 for the destination array. The + * actual number of significant bytes in your array is given by + * numSigBytes. + * + * @param source + * the array to convert + * @param srcOffset + * the index where conversion begins + * @param numSigBytes + * the number of significant bytes in your array + * @param destination + * the array to hold the conversion + * @param destOffset + * the index where output will be put + * @param alphabet + * is the encoding alphabet + * @return the destination array + * @since 1.3 + */ + private static byte[] encode3to4(byte[] source, int srcOffset, int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) { + // 1 2 3 + // 01234567890123456789012345678901 Bit position + // --------000000001111111122222222 Array position from threeBytes + // --------| || || || | Six bit groups to index alphabet + // >>18 >>12 >> 6 >> 0 Right shift necessary + // 0x3f 0x3f 0x3f Additional AND + + // Create buffer with zero-padding if there are only one or two + // significant bytes passed in the array. + // We have to shift left 24 in order to flush out the 1's that appear + // when Java treats a value as negative that is cast from a byte to an + // int. + int inBuff = (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) + | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) + | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); + + switch (numSigBytes) { + case 3: + destination[destOffset] = alphabet[(inBuff >>> 18)]; + destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = alphabet[(inBuff) & 0x3f]; + return destination; + case 2: + destination[destOffset] = alphabet[(inBuff >>> 18)]; + destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + case 1: + destination[destOffset] = alphabet[(inBuff >>> 18)]; + destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = EQUALS_SIGN; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + default: + return destination; + } // end switch + } // end encode3to4 + + /** + * Encodes a byte array into Base64 notation. Equivalent to calling {@code + * encodeBytes(source, 0, source.length)} + * + * @param source + * The data to convert + * @since 1.4 + */ + public static String encode(byte[] source) { + return encode(source, 0, source.length, ALPHABET, true); + } + + /** + * Encodes a byte array into web safe Base64 notation. + * + * @param source + * The data to convert + * @param doPadding + * is {@code true} to pad result with '=' chars if it does not + * fall on 3 byte boundaries + */ + public static String encodeWebSafe(byte[] source, boolean doPadding) { + return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding); + } + + /** + * Encodes a byte array into Base64 notation. + * + * @param source + * the data to convert + * @param off + * offset in array where conversion should begin + * @param len + * length of data to convert + * @param alphabet + * the encoding alphabet + * @param doPadding + * is {@code true} to pad result with '=' chars if it does not + * fall on 3 byte boundaries + * @since 1.4 + */ + public static String encode(byte[] source, int off, int len, byte[] alphabet, boolean doPadding) { + byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE); + int outLen = outBuff.length; + + // If doPadding is false, set length to truncate '=' + // padding characters + while (doPadding == false && outLen > 0) { + if (outBuff[outLen - 1] != '=') { + break; + } + outLen -= 1; + } + + return new String(outBuff, 0, outLen); + } + + /** + * Encodes a byte array into Base64 notation. + * + * @param source + * the data to convert + * @param off + * offset in array where conversion should begin + * @param len + * length of data to convert + * @param alphabet + * is the encoding alphabet + * @param maxLineLength + * maximum length of one line. + * @return the BASE64-encoded byte array + */ + public static byte[] encode(byte[] source, int off, int len, byte[] alphabet, int maxLineLength) { + int lenDiv3 = (len + 2) / 3; // ceil(len / 3) + int len43 = lenDiv3 * 4; + byte[] outBuff = new byte[len43 // Main 4:3 + + (len43 / maxLineLength)]; // New lines + + int d = 0; + int e = 0; + int len2 = len - 2; + int lineLength = 0; + for (; d < len2; d += 3, e += 4) { + + // The following block of code is the same as + // encode3to4( source, d + off, 3, outBuff, e, alphabet ); + // but inlined for faster encoding (~20% improvement) + int inBuff = ((source[d + off] << 24) >>> 8) | ((source[d + 1 + off] << 24) >>> 16) | ((source[d + 2 + off] << 24) >>> 24); + outBuff[e] = alphabet[(inBuff >>> 18)]; + outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f]; + outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f]; + outBuff[e + 3] = alphabet[(inBuff) & 0x3f]; + + lineLength += 4; + if (lineLength == maxLineLength) { + outBuff[e + 4] = NEW_LINE; + e++; + lineLength = 0; + } // end if: end of line + } // end for: each piece of array + + if (d < len) { + encode3to4(source, d + off, len - d, outBuff, e, alphabet); + + lineLength += 4; + if (lineLength == maxLineLength) { + // Add a last newline + outBuff[e + 4] = NEW_LINE; + e++; + } + e += 4; + } + + assert (e == outBuff.length); + return outBuff; + } + + /* ******** D E C O D I N G M E T H O D S ******** */ + + /** + * Decodes four bytes from array source and writes the resulting + * bytes (up to three of them) to destination. The source and + * destination arrays can be manipulated anywhere along their length by + * specifying srcOffset and destOffset. This method + * does not check to make sure your arrays are large enough to accommodate + * srcOffset + 4 for the source array or + * destOffset + 3 for the destination array. This + * method returns the actual number of bytes that were converted from the + * Base64 encoding. + * + * + * @param source + * the array to convert + * @param srcOffset + * the index where conversion begins + * @param destination + * the array to hold the conversion + * @param destOffset + * the index where output will be put + * @param decodabet + * the decodabet for decoding Base64 content + * @return the number of decoded bytes converted + * @since 1.3 + */ + private static int decode4to3(byte[] source, int srcOffset, byte[] destination, int destOffset, byte[] decodabet) { + // Example: Dk== + if (source[srcOffset + 2] == EQUALS_SIGN) { + int outBuff = ((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12); + + destination[destOffset] = (byte) (outBuff >>> 16); + return 1; + } else if (source[srcOffset + 3] == EQUALS_SIGN) { + // Example: DkL= + int outBuff = ((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) + | ((decodabet[source[srcOffset + 2]] << 24) >>> 18); + + destination[destOffset] = (byte) (outBuff >>> 16); + destination[destOffset + 1] = (byte) (outBuff >>> 8); + return 2; + } else { + // Example: DkLE + int outBuff = ((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) + | ((decodabet[source[srcOffset + 2]] << 24) >>> 18) | ((decodabet[source[srcOffset + 3]] << 24) >>> 24); + + destination[destOffset] = (byte) (outBuff >> 16); + destination[destOffset + 1] = (byte) (outBuff >> 8); + destination[destOffset + 2] = (byte) (outBuff); + return 3; + } + } // end decodeToBytes + + /** + * Decodes data from Base64 notation. + * + * @param s + * the string to decode (decoded in default encoding) + * @return the decoded data + * @since 1.4 + */ + public static byte[] decode(String s) throws Base64DecoderException { + byte[] bytes = s.getBytes(); + return decode(bytes, 0, bytes.length); + } + + /** + * Decodes data from web safe Base64 notation. Web safe encoding uses '-' + * instead of '+', '_' instead of '/' + * + * @param s + * the string to decode (decoded in default encoding) + * @return the decoded data + */ + public static byte[] decodeWebSafe(String s) throws Base64DecoderException { + byte[] bytes = s.getBytes(); + return decodeWebSafe(bytes, 0, bytes.length); + } + + /** + * Decodes Base64 content in byte array format and returns the decoded byte + * array. + * + * @param source + * The Base64 encoded data + * @return decoded data + * @since 1.3 + * @throws Base64DecoderException + */ + public static byte[] decode(byte[] source) throws Base64DecoderException { + return decode(source, 0, source.length); + } + + /** + * Decodes web safe Base64 content in byte array format and returns the + * decoded data. Web safe encoding uses '-' instead of '+', '_' instead of + * '/' + * + * @param source + * the string to decode (decoded in default encoding) + * @return the decoded data + */ + public static byte[] decodeWebSafe(byte[] source) throws Base64DecoderException { + return decodeWebSafe(source, 0, source.length); + } + + /** + * Decodes Base64 content in byte array format 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 + * @return decoded data + * @since 1.3 + * @throws Base64DecoderException + */ + public static byte[] decode(byte[] source, int off, int len) throws Base64DecoderException { + return decode(source, off, len, DECODABET); + } + + /** + * Decodes web safe Base64 content in byte array format and returns the + * decoded byte array. Web safe encoding uses '-' instead of '+', '_' + * instead of '/' + * + * @param source + * the Base64 encoded data + * @param off + * the offset of where to begin decoding + * @param len + * the length of characters to decode + * @return decoded data + */ + public static byte[] decodeWebSafe(byte[] source, int off, int len) throws Base64DecoderException { + return decode(source, off, len, WEBSAFE_DECODABET); + } + + /** + * 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) { + // Ensure you have set your public key + 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; + } +} diff --git a/src/org/fox/ttrss/util/Base64DecoderException.java b/src/org/fox/ttrss/util/Base64DecoderException.java new file mode 100644 index 00000000..7b7be229 --- /dev/null +++ b/src/org/fox/ttrss/util/Base64DecoderException.java @@ -0,0 +1,32 @@ +// Copyright 2002, 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 org.fox.ttrss.util;; + +/** + * Exception thrown when encountering an invalid Base64 input character. + * + * @author nelson + */ +public class Base64DecoderException extends Exception { + public Base64DecoderException() { + super(); + } + + public Base64DecoderException(String s) { + super(s); + } + + private static final long serialVersionUID = 1L; +} -- cgit v1.2.3