diff --git a/vending-app/build.gradle b/vending-app/build.gradle new file mode 100644 index 00000000..8031b91d --- /dev/null +++ b/vending-app/build.gradle @@ -0,0 +1,84 @@ +/* + * SPDX-FileCopyrightText: 2015 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'com.squareup.wire' + +android { + namespace "com.android.vending" + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + defaultConfig { + versionName vendingAppVersionName + versionCode vendingAppVersionCode + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + flavorDimensions = ['target'] + productFlavors { + "default" { + dimension 'target' + } + "huawei" { + dimension 'target' + versionNameSuffix "-hw" + } + "huaweilh" { + dimension 'target' + versionNameSuffix "-lh" + versionCode vendingAppVersionCode - 1000 + matchingFallbacks = ['huawei'] + } + } + + sourceSets { + main { + java { + srcDirs += "build/generated/source/proto/main/java" + } + } + } + + + buildFeatures { + aidl = true + } + + lintOptions { + disable 'MissingTranslation' + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } +} + +dependencies { + implementation project(':fake-signature') + implementation project(':play-services-auth') + + implementation "com.squareup.wire:wire-runtime:$wireVersion" + implementation "com.android.volley:volley:$volleyVersion" +} + +wire { + kotlin { + javaInterop = true + } +} + +if (file('user.gradle').exists()) { + apply from: 'user.gradle' +} + +android.applicationVariants.all { variant -> + variant.outputs.each { output -> + output.outputFileName = variant.applicationId + "-" + variant.versionCode + variant.versionName.substring(vendingAppVersionName.length()) + ".apk" + } +} \ No newline at end of file diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..7156d709 --- /dev/null +++ b/vending-app/src/main/AndroidManifest.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java new file mode 100644 index 00000000..08d6e28a --- /dev/null +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java @@ -0,0 +1,217 @@ +package com.android.vending.licensing; + +import static com.android.volley.Request.Method.GET; + +import android.util.Base64; +import android.util.Log; + +import com.android.vending.AndroidVersionMeta; +import com.android.vending.DeviceMeta; +import com.android.vending.EncodedTriple; +import com.android.vending.EncodedTripleWrapper; +import com.android.vending.IntWrapper; +import com.android.vending.LicenseRequestHeader; +import com.android.vending.LicenseResult; +import com.android.vending.Locality; +import com.android.vending.LocalityWrapper; +import com.android.vending.StringWrapper; +import com.android.vending.Timestamp; +import com.android.vending.TimestampContainer; +import com.android.vending.TimestampContainer1; +import com.android.vending.TimestampContainer1Wrapper; +import com.android.vending.TimestampContainer2; +import com.android.vending.TimestampStringWrapper; +import com.android.vending.TimestampWrapper; +import com.android.vending.UnknownByte12; +import com.android.vending.UserAgent; +import com.android.vending.Util; +import com.android.vending.Uuid; +import com.android.vending.V1Container; +import com.android.volley.NetworkResponse; +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.VolleyError; + +import java.io.IOException; +import java.util.Map; +import java.util.UUID; + +import okio.ByteString; + +public abstract class LicenseRequest extends Request { + + private final String xPsRh; + private final String auth; + private static final String TAG = "FakeLicenseRequest"; + + private static final int BASE64_FLAGS = Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING; + private static final long ANDROID_ID = 1; + + private final Response.Listener successListener; + + + protected LicenseRequest(String url, String auth, Response.Listener successListener, Response.ErrorListener errorListener) { + super(GET, url, errorListener); + this.auth = auth; + + this.successListener = successListener; + + long millis = System.currentTimeMillis(); + TimestampContainer.Builder timestamp = new TimestampContainer.Builder() + .container2(new TimestampContainer2.Builder() + .wrapper(new TimestampWrapper.Builder().timestamp(makeTimestamp(millis)).build()) + .timestamp(makeTimestamp(millis)) + .build()); + millis = System.currentTimeMillis(); + timestamp + .container1Wrapper(new TimestampContainer1Wrapper.Builder() + .androidId(String.valueOf(ANDROID_ID)) + .container(new TimestampContainer1.Builder() + .timestamp(millis + "000") + .wrapper(makeTimestamp(millis)) + .build()) + .build() + ); + String encodedTimestamps = new String( + Base64.encode(Util.encodeGzip(timestamp.build().encode()), BASE64_FLAGS) + ); + + Locality locality = new Locality.Builder() + .unknown1(1) + .unknown2(2) + .countryCode("") + .region(new TimestampStringWrapper.Builder() + .string("").timestamp(makeTimestamp(System.currentTimeMillis())).build()) + .country(new TimestampStringWrapper.Builder() + .string("").timestamp(makeTimestamp(System.currentTimeMillis())).build()) + .unknown3(0) + .build(); + String encodedLocality = new String( + Base64.encode(locality.encode(), BASE64_FLAGS) + ); + + byte[] header = new LicenseRequestHeader.Builder() + .encodedTimestamps(new StringWrapper.Builder().string(encodedTimestamps).build()) + .triple( + new EncodedTripleWrapper.Builder().triple( + new EncodedTriple.Builder() + .encoded1("") + .encoded2("") + .empty("") + .build() + ).build() + ) + .locality(new LocalityWrapper.Builder().encodedLocalityProto(encodedLocality).build()) + .unknown(new IntWrapper.Builder().integer(5).build()) + .empty("") + .deviceMeta(new DeviceMeta.Builder() + .android( + new AndroidVersionMeta.Builder() + .androidSdk(0) + .buildNumber("") + .androidVersion("") + .unknown(0) + .build() + ) + .unknown1(new UnknownByte12.Builder().bytes(new ByteString(new byte[]{} + )).build()) + .unknown2(1) + .build() + ) + .userAgent(new UserAgent.Builder() + .deviceProductName("") + .deviceSoc("") + .deviceModelName("") + .finskyVersion("") + .deviceName("") + .androidId(ANDROID_ID) // must not be 0 + .deviceSignature("") + .build() + ) + .uuid(new Uuid.Builder() + .uuid(UUID.randomUUID().toString()) + .unknown(2) + .build() + ) + .build().encode(); + this.xPsRh = new String(Base64.encode(Util.encodeGzip(header), BASE64_FLAGS)); + + //Log.d(TAG, "Product " + Build.PRODUCT + ", Board " + Build.BOARD + " Model " + Build.MODEL + " Device " + Build.DEVICE); + + Log.v(TAG, "X-PS-RH: " + xPsRh); + } + + @Override + public Map getHeaders() { + return Map.of( + "X-PS-RH", xPsRh, + "Authorization", "Bearer " + auth, + "Connection", "Keep-Alive" + ); + } + + @Override + protected void deliverResponse(T response) { + successListener.onResponse(response); + } + + private static Timestamp makeTimestamp(long millis) { + return new Timestamp.Builder() + .seconds((int) (millis / 1000)) + .nanos(Math.floorMod(millis, 1000) * 1000000) + .build(); + } + + public static class V1 extends LicenseRequest { + + public V1(String packageName, String auth, int versionCode, long nonce, Response.Listener successListener, Response.ErrorListener errorListener) { + super("https://play-fe.googleapis.com/fdfe/apps/checkLicense?pkgn=" + packageName + "&vc=" + versionCode + "&nnc=" + nonce, + auth, successListener, errorListener + ); + } + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + if (response != null && response.data != null) { + try { + LicenseResult result = LicenseResult.ADAPTER.decode(response.data); + return Response.success(result.information.v1, null); + } catch (IOException e) { + return Response.error(new VolleyError(e)); + } catch (NullPointerException e) { + // A field does not exist → user has no license + return Response.success(null, null); + } + } else { + return Response.error(new VolleyError("No response was returned")); + } + } + } + + public static class V2 extends LicenseRequest { + public V2(String packageName, String auth, int versionCode, Response.Listener successListener, + Response.ErrorListener errorListener) { + super( + "https://play-fe.googleapis.com/fdfe/apps/checkLicenseServerFallback?pkgn=" + packageName + "&vc=" + versionCode, + auth, successListener, errorListener + ); + } + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + if (response != null && response.data != null) { + try { + LicenseResult result = LicenseResult.ADAPTER.decode(response.data); + return Response.success(result.information.v2.license.jwt, null); + } catch (IOException e) { + return Response.error(new VolleyError(e)); + } catch (NullPointerException e) { + // A field does not exist → user has no license + return Response.success(null, null); + } + } else { + return Response.error(new VolleyError("No response was returned")); + } + } + } +} diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java new file mode 100644 index 00000000..267ddc84 --- /dev/null +++ b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java @@ -0,0 +1,232 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.vending.licensing; + +import static android.accounts.AccountManager.KEY_AUTHTOKEN; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.app.Service; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.toolbox.Volley; + +import org.microg.gms.auth.AuthConstants; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +public class LicensingService extends Service { + private static final String TAG = "FakeLicenseService"; + private RequestQueue queue; + private AccountManager accountManager; + + private static final String KEY_V2_RESULT_JWT = "LICENSE_DATA"; + + private static final String AUTH_TOKEN_SCOPE = "oauth2:https://www.googleapis.com/auth/googleplay"; + + /* Possible response codes for checkLicense v1, from + * https://developer.android.com/google/play/licensing/licensing-reference#server-response-codes and + * the LVL library. + */ + + /** + * The application is licensed to the user. The user has purchased the application, or is authorized to + * download and install the alpha or beta version of the application. + */ + private static final int LICENSED = 0x0; + /** + * The application is licensed to the user, but there is an updated application version available that is + * signed with a different key. + */ + private static final int NOT_LICENSED = 0x1; + /** + * The application is not licensed to the user. + */ + private static final int LICENSED_OLD_KEY = 0x2; + /** + * Server error — the application (package name) was not recognized by Google Play. + */ + private static final int ERROR_NOT_MARKET_MANAGED = 0x3; + /** + * Server error — the server could not load the application's key pair for licensing. + */ + private static final int ERROR_SERVER_FAILURE = 0x4; + private static final int ERROR_OVER_QUOTA = 0x5; + + /** + * Local error — the Google Play application was not able to reach the licensing server, possibly because + * of network availability problems. + */ + private static final int ERROR_CONTACTING_SERVER = 0x101; + /** + * Local error — the application requested a license check for a package that is not installed on the device. + */ + private static final int ERROR_INVALID_PACKAGE_NAME = 0x102; + /** + * Local error — the application requested a license check for a package whose UID (package, user ID pair) + * does not match that of the requesting application. + */ + private static final int ERROR_NON_MATCHING_UID = 0x103; + + private final ILicensingService.Stub mLicenseService = new ILicensingService.Stub() { + + + @Override + public void checkLicense(long nonce, String packageName, ILicenseResultListener listener) throws RemoteException { + Log.v(TAG, "checkLicense(" + nonce + ", " + packageName + ")"); + try { + PackageInfo packageInfo = getPackageManager().getPackageInfo(packageName, 0); + int versionCode = packageInfo.versionCode; + + // Verify caller identity + if (packageInfo.applicationInfo.uid != getCallingUid()) { + Log.e(TAG, "an app illegally tried to request v1 licenses for another app (caller: " + getCallingUid() + ")"); + listener.verifyLicense(ERROR_NON_MATCHING_UID, null, null); + } else { + + Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE); + + if (accounts.length == 0) { + Log.e(TAG, "not checking license, as user is not signed in"); + } else accountManager.getAuthToken( + accounts[0], AUTH_TOKEN_SCOPE, false, + future -> { + Request request = null; + try { + request = new LicenseRequest.V1( + packageName, + future.getResult().getString(KEY_AUTHTOKEN), + versionCode, nonce, data -> { + if (data != null) { + Log.v(TAG, "licenseV1 result was " + data.result + " with signed data " + + data.signedData); + + try { + if (data.result != null) { + listener.verifyLicense(data.result, data.signedData, data.signature); + } else { + listener.verifyLicense(LICENSED, data.signedData, data.signature); + } + } catch (RemoteException e) { + Log.e(TAG, + "After telling it the licenseV1 result, remote threw an Exception."); + e.printStackTrace(); + } + } else { + Log.v(TAG, "licenseV1 result was that user has no license"); + sendError(listener, NOT_LICENSED); + } + }, error -> { + Log.e(TAG, "licenseV1 request failed with " + error.toString()); + sendError(listener, ERROR_CONTACTING_SERVER); + }); + } catch (AuthenticatorException | IOException | OperationCanceledException e) { + sendError(listener, ERROR_CONTACTING_SERVER); + } + + request.setShouldCache(false); + queue.add(request); + }, null); + } + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "an app tried to request v1 licenses for package " + packageName + ", which does not exist"); + listener.verifyLicense(ERROR_INVALID_PACKAGE_NAME, null, null); + } + } + + @Override + public void checkLicenseV2(String packageName, ILicenseV2ResultListener listener, Bundle extraParams) throws RemoteException { + Log.v(TAG, "checkLicenseV2(" + packageName + ", " + extraParams + ")"); + + try { + PackageInfo packageInfo = getPackageManager().getPackageInfo(packageName, 0); + int versionCode = packageInfo.versionCode; + + // Verify caller identity + if (packageInfo.applicationInfo.uid != getCallingUid()) { + Log.e(TAG, "an app illegally tried to request v2 licenses for another app (caller: " + getCallingUid() + ")"); + listener.verifyLicense(ERROR_NON_MATCHING_UID, new Bundle()); + } else { + Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE); + + if (accounts.length == 0) { + Log.e(TAG, "not checking license, as user is not signed in"); + } else accountManager.getAuthToken( + accounts[0], AUTH_TOKEN_SCOPE, false, + future -> { + try { + Bundle result = future.getResult(10, TimeUnit.SECONDS); + String auth = result.getString(KEY_AUTHTOKEN); + + Request request = new LicenseRequest.V2(packageName, auth, versionCode, jwt -> { + Log.v(TAG, "LicenseV2 returned JWT license value " + jwt); + Bundle bundle = new Bundle(); + bundle.putString(KEY_V2_RESULT_JWT, jwt); + try { + listener.verifyLicense(jwt == null ? NOT_LICENSED : LICENSED, bundle); + } catch (RemoteException e) { + Log.e(TAG, "After returning licenseV2 result, remote threw an Exception."); + e.printStackTrace(); + } + }, error -> { + Log.e(TAG, "licenseV2 request failed with " + error.toString()); + sendError(listener, ERROR_CONTACTING_SERVER); + }); + + request.setShouldCache(false); + queue.add(request); + + } catch (AuthenticatorException | IOException | OperationCanceledException e) { + sendError(listener, ERROR_CONTACTING_SERVER); + e.printStackTrace(); + } + }, null + ); + } + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "an app tried to request v1 licenses for package " + packageName + ", which does not exist"); + listener.verifyLicense(ERROR_INVALID_PACKAGE_NAME, new Bundle()); + } + + } + }; + + private static void sendError(ILicenseResultListener listener, int error) { + try { + listener.verifyLicense(error, null, null); + } catch (RemoteException e) { + Log.e(TAG, "After telling it that licenseV1 had an error (" + error + "), remote threw an Exception."); + } + } + + private static void sendError(ILicenseV2ResultListener listener, int error) { + try { + listener.verifyLicense(error, new Bundle()); + } catch (RemoteException e) { + Log.e(TAG, "After telling it that licenseV2 had an error (" + error + "), remote threw an Exception."); + } + } + + public IBinder onBind(Intent intent) { + queue = Volley.newRequestQueue(this); + accountManager = AccountManager.get(this); + + return mLicenseService; + } + + +}