From 8446e0962deab4d3c8f26f6aceb48ff9d6dd22ac Mon Sep 17 00:00:00 2001 From: emanuele-f Date: Sat, 9 Jul 2022 23:49:22 +0200 Subject: [PATCH] Access purchased paid features on beta builds Since PCAPdroid 1.5.3, users which purchased paid features in official builds will be able to use them in beta builds. --- .../com/emanuelef/remote_capture/Billing.java | 9 +++ .../emanuelef/remote_capture/PCAPdroid.java | 2 +- .../com/emanuelef/remote_capture/Utils.java | 11 +-- .../activities/CaptureCtrl.java | 36 ++++++++++ .../activities/ErrorActivity.java | 4 +- .../activities/MainActivity.java | 68 +++++++++++++++++++ 6 files changed, 122 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/emanuelef/remote_capture/Billing.java b/app/src/main/java/com/emanuelef/remote_capture/Billing.java index df5ba02d..2f68ba5f 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/Billing.java +++ b/app/src/main/java/com/emanuelef/remote_capture/Billing.java @@ -40,6 +40,7 @@ import java.security.SignatureException; import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; import java.util.Arrays; +import java.util.HashSet; import java.util.List; /* Billing stub */ @@ -69,6 +70,7 @@ public class Billing { protected final Context mContext; protected SharedPreferences mPrefs; + private final HashSet mPeerSkus = new HashSet<>(); protected Billing(Context ctx) { mContext = ctx; @@ -84,6 +86,9 @@ public class Billing { } public boolean isPurchased(String sku) { + if(mPeerSkus.contains(sku)) + return true; + return !getLicense().isEmpty(); } @@ -188,4 +193,8 @@ public class Billing { else return !Prefs.isRootCaptureEnabled(mPrefs); } + + public void addPeerSku(String sku) { + mPeerSkus.add(sku); + } } diff --git a/app/src/main/java/com/emanuelef/remote_capture/PCAPdroid.java b/app/src/main/java/com/emanuelef/remote_capture/PCAPdroid.java index ba589783..8188fc82 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/PCAPdroid.java +++ b/app/src/main/java/com/emanuelef/remote_capture/PCAPdroid.java @@ -60,7 +60,7 @@ public class PCAPdroid extends Application { public void onCreate() { super.onCreate(); - Utils.BuildType buildtp = Utils.getBuildType(this); + Utils.BuildType buildtp = Utils.getVerifiedBuild(this); Log.d(TAG, "Build type: " + buildtp); CaocConfig.Builder builder = CaocConfig.Builder.create(); diff --git a/app/src/main/java/com/emanuelef/remote_capture/Utils.java b/app/src/main/java/com/emanuelef/remote_capture/Utils.java index 3f14a0a9..5b1893c3 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/Utils.java +++ b/app/src/main/java/com/emanuelef/remote_capture/Utils.java @@ -59,7 +59,6 @@ import android.os.Handler; import android.os.Looper; import android.provider.MediaStore; import android.provider.OpenableColumns; -import android.text.Html; import android.text.SpannableString; import android.text.SpannedString; import android.text.TextUtils; @@ -1020,16 +1019,16 @@ public class Utils { return false; } - @SuppressWarnings("deprecation") - public static BuildType getBuildType(Context ctx) { + public static BuildType getVerifiedBuild(Context ctx, String package_name) { try { Signature[] signatures; if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { // NOTE: PCAPdroid does not use multiple signatures - PackageInfo pInfo = ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), PackageManager.GET_SIGNING_CERTIFICATES); + PackageInfo pInfo = ctx.getPackageManager().getPackageInfo(package_name, PackageManager.GET_SIGNING_CERTIFICATES); signatures = (pInfo.signingInfo == null) ? null : pInfo.signingInfo.getSigningCertificateHistory(); } else { + @SuppressLint("PackageManagerGetSignatures") PackageInfo pInfo = ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), PackageManager.GET_SIGNATURES); signatures = pInfo.signatures; } @@ -1057,6 +1056,10 @@ public class Utils { return BuildType.UNKNOWN; } + public static BuildType getVerifiedBuild(Context ctx) { + return getVerifiedBuild(ctx, ctx.getPackageName()); + } + public static X509Certificate x509FromPem(String pem) { int begin = pem.indexOf('\n') + 1; int end = pem.indexOf('-', begin); diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/CaptureCtrl.java b/app/src/main/java/com/emanuelef/remote_capture/activities/CaptureCtrl.java index 5969e201..76f31eb2 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/activities/CaptureCtrl.java +++ b/app/src/main/java/com/emanuelef/remote_capture/activities/CaptureCtrl.java @@ -42,6 +42,7 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import com.emanuelef.remote_capture.AppsResolver; +import com.emanuelef.remote_capture.Billing; import com.emanuelef.remote_capture.BuildConfig; import com.emanuelef.remote_capture.CaptureHelper; import com.emanuelef.remote_capture.CaptureService; @@ -53,10 +54,13 @@ import com.emanuelef.remote_capture.model.CaptureSettings; import com.emanuelef.remote_capture.model.CtrlPermissions; import com.emanuelef.remote_capture.model.CaptureStats; +import java.util.HashSet; + public class CaptureCtrl extends AppCompatActivity { public static final String ACTION_START = "start"; public static final String ACTION_STOP = "stop"; public static final String ACTION_STATUS = "get_status"; + public static final String ACTION_PEER_INFO = "get_peer_info"; public static final String ACTION_NOTIFY_STATUS = "com.emanuelef.remote_capture.CaptureStatus"; private static final String TAG = "CaptureCtrl"; private static AppDescriptor mStarterApp = null; // the app which started the capture, may be unknown @@ -100,6 +104,11 @@ public class CaptureCtrl extends AppCompatActivity { return; } + if(action.equals(ACTION_PEER_INFO)) { + getPeerInfo(); + return; + } + // Check if a control permission rule was set mPermissions = PCAPdroid.getInstance().getCtrlPermissions(); AppDescriptor app = getCallingApp(); @@ -260,4 +269,31 @@ public class CaptureCtrl extends AppCompatActivity { intent.putExtra("pkts_rcvd", stats.pkts_rcvd); intent.putExtra("pkts_dropped", stats.pkts_dropped); } + + // A request sent from a debug build of PCAPdroid to a non-debug one + private void getPeerInfo() { + // Verify the peer app + String package_name = getCallingPackage(); + if((package_name == null) || !package_name.equals(BuildConfig.APPLICATION_ID + ".debug")) { + Log.w(TAG, "getPeerInfo: package name mismatch"); + abort(); + return; + } + + Billing billing = Billing.newInstance(this); + billing.setLicense(billing.getLicense()); + + Intent res = new Intent(); + HashSet purchased = new HashSet<>(); + + for(String sku: Billing.ALL_SKUS) { + if(billing.isPurchased(sku)) + purchased.add(sku); + } + + res.putExtra("skus", purchased); + + setResult(RESULT_OK, res); + finish(); + } } diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/ErrorActivity.java b/app/src/main/java/com/emanuelef/remote_capture/activities/ErrorActivity.java index bad61b5d..f96676b4 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/activities/ErrorActivity.java +++ b/app/src/main/java/com/emanuelef/remote_capture/activities/ErrorActivity.java @@ -38,8 +38,6 @@ import androidx.core.content.res.ResourcesCompat; import com.emanuelef.remote_capture.Utils; -import java.util.Arrays; - import cat.ereza.customactivityoncrash.CustomActivityOnCrash; import cat.ereza.customactivityoncrash.R; import cat.ereza.customactivityoncrash.config.CaocConfig; @@ -158,7 +156,7 @@ public final class ErrorActivity extends AppCompatActivity { } private String getErrorDetails() { - return "Build type: " + Utils.getBuildType(this).toString().toLowerCase() + "\n" + + return "Build type: " + Utils.getVerifiedBuild(this).toString().toLowerCase() + "\n" + CustomActivityOnCrash.getAllErrorDetailsFromIntent(ErrorActivity.this, getIntent()); } } \ No newline at end of file diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/MainActivity.java b/app/src/main/java/com/emanuelef/remote_capture/activities/MainActivity.java index 2250498b..aefbc7b7 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/activities/MainActivity.java +++ b/app/src/main/java/com/emanuelef/remote_capture/activities/MainActivity.java @@ -27,6 +27,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.UriPermission; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; @@ -61,12 +62,15 @@ import android.view.MenuItem; import android.view.View; import android.widget.TextView; +import com.emanuelef.remote_capture.AppsResolver; import com.emanuelef.remote_capture.Billing; +import com.emanuelef.remote_capture.BuildConfig; import com.emanuelef.remote_capture.CaptureHelper; import com.emanuelef.remote_capture.MitmReceiver; import com.emanuelef.remote_capture.fragments.ConnectionsFragment; import com.emanuelef.remote_capture.fragments.StatusFragment; import com.emanuelef.remote_capture.interfaces.AppStateListener; +import com.emanuelef.remote_capture.model.AppDescriptor; import com.emanuelef.remote_capture.model.AppState; import com.emanuelef.remote_capture.CaptureService; import com.emanuelef.remote_capture.model.CaptureSettings; @@ -82,6 +86,8 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; +import java.io.Serializable; +import java.util.HashSet; public class MainActivity extends BaseActivity implements NavigationView.OnNavigationItemSelectedListener { private Billing mIab; @@ -121,6 +127,8 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig registerForActivityResult(new RequestPermission(), isGranted -> Log.d(TAG, "Write permission " + (isGranted ? "granted" : "denied")) ); + private final ActivityResultLauncher peerInfoLauncher = + registerForActivityResult(new StartActivityForResult(), this::peerInfoResult); @Override protected void onCreate(Bundle savedInstanceState) { @@ -143,6 +151,7 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig mIab = Billing.newInstance(this); mIab.setLicense(mIab.getLicense()); + initPeerAppInfo(); initAppState(); checkPermissions(); @@ -316,6 +325,65 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig } } + // On debug builds, if the user also has the non-debug app installed (peer app), unlock the + // already-purchased features also on this beta app + private void initPeerAppInfo() { + if(!BuildConfig.DEBUG) + return; + + final String peerAppPackage = "com.emanuelef.remote_capture"; + + AppDescriptor peer = AppsResolver.resolve(getPackageManager(), peerAppPackage, 0); + if(peer == null) { + Log.d(TAG, "Peer app not found"); + return; + } + + PackageInfo pInfo = peer.getPackageInfo(); + if((pInfo == null) || (pInfo.versionCode < 56)) { + Log.d(TAG, "Unsupported peer app version found"); + return; + } + + // Verify that the peer signature + Utils.BuildType buildType = Utils.getVerifiedBuild(this, peerAppPackage); + if((buildType != Utils.BuildType.FDROID) && (buildType != Utils.BuildType.PLAYSTORE) && (buildType != Utils.BuildType.GITHUB)) { + Log.d(TAG, "Unsupported peer app build: " + buildType.name()); + return; + } + + Log.d(TAG, "Valid peer app found (" + pInfo.versionName + " - " + pInfo.versionCode + ")"); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setClassName(peerAppPackage, "com.emanuelef.remote_capture.activities.CaptureCtrl"); + intent.putExtra("action", "get_peer_info"); + + try { + peerInfoLauncher.launch(intent); + } catch (ActivityNotFoundException e) { + Log.d(TAG, "Peer app launch failed"); + } + } + + private void peerInfoResult(final ActivityResult result) { + if((result.getResultCode() == RESULT_OK) && (result.getData() != null)) { + Intent data = result.getData(); + Serializable skus_extra = data.getSerializableExtra("skus"); + if(skus_extra instanceof HashSet) { + HashSet skus = (HashSet) skus_extra; + Log.d(TAG, "Found peer app info"); + + for(String sku: skus) { + Log.d(TAG, "Peer sku: " + sku); + mIab.addPeerSku(sku); + } + + return; + } + } + + Log.d(TAG, "Invalid peer app result"); + } + private static class MainStateAdapter extends FragmentStateAdapter { MainStateAdapter(final FragmentActivity fa) { super(fa); }