SafetyNet updates

- Add more API details
- preliminary support for SafetyNet reCAPTCHA
- prepare for improved DroidGuard handling
This commit is contained in:
Marvin W 2021-05-05 23:29:02 +02:00
parent 7f131c0cfa
commit 1a809e0e47
No known key found for this signature in database
GPG Key ID: 072E9235DB996F2A
32 changed files with 1093 additions and 267 deletions

View File

@ -22,6 +22,7 @@ buildscript {
ext.navigationVersion = '2.3.1'
ext.preferenceVersion = '1.1.1'
ext.recyclerviewVersion = '1.1.0'
ext.webkitVersion = '1.4.0'
ext.supportLibraryVersion = '28.0.0'
ext.slf4jVersion = '1.7.25'

View File

@ -0,0 +1,3 @@
package com.google.android.gms.safetynet;
parcelable HarmfulAppsInfo;

View File

@ -0,0 +1,3 @@
package com.google.android.gms.safetynet;
parcelable RecaptchaResultData;

View File

@ -0,0 +1,3 @@
package com.google.android.gms.safetynet;
parcelable RemoveHarmfulAppData;

View File

@ -3,6 +3,9 @@ package com.google.android.gms.safetynet.internal;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.safetynet.AttestationData;
import com.google.android.gms.safetynet.HarmfulAppsData;
import com.google.android.gms.safetynet.HarmfulAppsInfo;
import com.google.android.gms.safetynet.RecaptchaResultData;
import com.google.android.gms.safetynet.RemoveHarmfulAppData;
import com.google.android.gms.safetynet.SafeBrowsingData;
interface ISafetyNetCallbacks {
@ -11,4 +14,7 @@ interface ISafetyNetCallbacks {
void onSafeBrowsingData(in Status status, in SafeBrowsingData safeBrowsingData) = 2;
void onBoolean(in Status status, boolean b) = 3;
void onHarmfulAppsData(in Status status, in List<HarmfulAppsData> harmfulAppsData) = 4;
}
void onRecaptchaResult(in Status status, in RecaptchaResultData recaptchaResultData) = 5;
void onHarmfulAppsInfo(in Status status, in HarmfulAppsInfo harmfulAppsInfo) = 7;
void onRemoveHarmfulAppData(in Status status, in RemoveHarmfulAppData removeHarmfulAppData) = 14;
}

View File

@ -9,4 +9,5 @@ interface ISafetyNetService {
void lookupUri(ISafetyNetCallbacks callbacks, String s1, in int[] threatTypes, int i, String s2) = 2;
void init(ISafetyNetCallbacks callbacks) = 3;
void getHarmfulAppsList(ISafetyNetCallbacks callbacks) = 4;
}
void verifyWithRecaptcha(ISafetyNetCallbacks callbacks, String siteKey) = 5;
}

View File

@ -1,23 +1,49 @@
/*
* Copyright (C) 2013-2017 microG Project Team
*
* 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.
* SPDX-FileCopyrightText: 2021, microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.safetynet;
import org.microg.gms.common.PublicApi;
import org.microg.safeparcel.AutoSafeParcelable;
/**
* APK information pertaining to one potentially harmful app.
*/
@PublicApi
public class HarmfulAppsData extends AutoSafeParcelable {
/**
* The package name of the potentially harmful app.
*/
@Field(2)
public final String apkPackageName;
/**
* The SHA-256 of the potentially harmful app APK file.
*/
@Field(3)
public final byte[] apkSha256;
/**
* The potentially harmful app category defined in {@link VerifyAppsConstants}.
*/
@Field(4)
public final int apkCategory;
private HarmfulAppsData() {
apkPackageName = null;
apkSha256 = null;
apkCategory = 0;
}
@PublicApi(exclude = true)
public HarmfulAppsData(String apkPackageName, byte[] apkSha256, int apkCategory) {
this.apkPackageName = apkPackageName;
this.apkSha256 = apkSha256;
this.apkCategory = apkCategory;
}
public static final Creator<HarmfulAppsData> CREATOR = new AutoCreator<HarmfulAppsData>(HarmfulAppsData.class);
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (C) 2013-2017 microG Project Team
*
* 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.google.android.gms.safetynet;
import org.microg.safeparcel.AutoSafeParcelable;
public class HarmfulAppsInfo extends AutoSafeParcelable {
@Field(2)
public long field2;
@Field(3)
public HarmfulAppsData[] data;
@Field(4)
public int field4;
@Field(5)
public boolean field5;
public static final Creator<HarmfulAppsInfo> CREATOR = new AutoCreator<HarmfulAppsInfo>(HarmfulAppsInfo.class);
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (C) 2013-2017 microG Project Team
*
* 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.google.android.gms.safetynet;
import org.microg.safeparcel.AutoSafeParcelable;
public class RecaptchaResultData extends AutoSafeParcelable {
@Field(2)
public String token;
public static final Creator<RecaptchaResultData> CREATOR = new AutoCreator<RecaptchaResultData>(RecaptchaResultData.class);
}

View File

@ -0,0 +1,28 @@
/*
* Copyright (C) 2013-2017 microG Project Team
*
* 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.google.android.gms.safetynet;
import org.microg.safeparcel.AutoSafeParcelable;
public class RemoveHarmfulAppData extends AutoSafeParcelable {
@Field(2)
public int field2;
@Field(3)
public boolean field3;
public static final Creator<RemoveHarmfulAppData> CREATOR = new AutoCreator<RemoveHarmfulAppData>(RemoveHarmfulAppData.class);
}

View File

@ -16,18 +16,30 @@
package com.google.android.gms.safetynet;
import android.os.ParcelFileDescriptor;
import com.google.android.gms.common.data.DataHolder;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
import java.io.File;
public class SafeBrowsingData extends AutoSafeParcelable {
@SafeParceled(1)
private int versionCode = 1;
@SafeParceled(2)
private String status;
@SafeParceled(3)
private DataHolder data;
@Field(1)
public int versionCode = 1;
@Field(2)
public String status;
@Field(3)
public DataHolder data;
@Field(4)
public ParcelFileDescriptor fileDescriptor;
public File file;
public byte[] fileContents;
@Field(5)
public long field5;
@Field(6)
public byte[] field6;
public static final Creator<SafeBrowsingData> CREATOR = new AutoCreator<SafeBrowsingData>(SafeBrowsingData.class);
}

View File

@ -0,0 +1,37 @@
/*
* SPDX-FileCopyrightText: 2021, microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.safetynet;
import com.google.android.gms.common.api.CommonStatusCodes;
/**
* Status codes for the SafetyNet API.
*/
public class SafetyNetStatusCodes extends CommonStatusCodes {
public static final int SAFE_BROWSING_UNSUPPORTED_THREAT_TYPES = 12000;
public static final int SAFE_BROWSING_MISSING_API_KEYINT = 12001;
public static final int SAFE_BROWSING_API_NOT_AVAILABLE = 12002;
public static final int VERIFY_APPS_NOT_AVAILABLE = 12003;
public static final int VERIFY_APPS_INTERNAL_ERROR = 12004;
public static final int VERIFY_APPS_NOT_ENABLED = 12005;
public static final int UNSUPPORTED_SDK_VERSION = 12006;
/**
* Cannot start the reCAPTCHA service because site key parameter is not valid.
*/
public static final int RECAPTCHA_INVALID_SITEKEY = 12007;
/**
* Cannot start the reCAPTCHA service because type of site key is not valid.
*/
public static final int RECAPTCHA_INVALID_KEYTYPE = 12008;
public static final int SAFE_BROWSING_API_NOT_INITIALIZED = 12009;
/**
* Cannot start the reCAPTCHA service because calling package name is not matched with site key.
*/
public static final int RECAPTCHA_INVALID_PACKAGE_NAME = 12013;
}

View File

@ -0,0 +1,52 @@
/*
* SPDX-FileCopyrightText: 2021, microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.safetynet;
import org.microg.gms.common.PublicApi;
/**
* Constants pertaining to the Verify Apps SafetyNet API.
*/
@PublicApi
public class VerifyAppsConstants {
/**
* An action that is broadcasted when harmful apps are discovered.
*/
public static final String ACTION_HARMFUL_APPS_FOUND = "com.google.android.gms.safetynet.action.HARMFUL_APPS_FOUND";
/**
* An action that is broadcasted when a harmful app is blocked from installation.
*/
public static final String ACTION_HARMFUL_APP_BLOCKED = "com.google.android.gms.safetynet.action.HARMFUL_APP_BLOCKED";
/**
* An action that is broadcasted when a harmful app is installed.
*/
public static final String ACTION_HARMFUL_APP_INSTALLED = "com.google.android.gms.safetynet.action.HARMFUL_APP_INSTALLED";
public static final int HARMFUL_CATEGORY_RANSOMWARE = 1;
public static final int HARMFUL_CATEGORY_PHISHING = 2;
public static final int HARMFUL_CATEGORY_TROJAN = 3;
public static final int HARMFUL_CATEGORY_UNCOMMON = 4;
public static final int HARMFUL_CATEGORY_FRAUDWARE = 5;
public static final int HARMFUL_CATEGORY_TOLL_FRAUD = 6;
public static final int HARMFUL_CATEGORY_WAP_FRAUD = 7;
public static final int HARMFUL_CATEGORY_CALL_FRAUD = 8;
public static final int HARMFUL_CATEGORY_BACKDOOR = 9;
public static final int HARMFUL_CATEGORY_SPYWARE = 10;
public static final int HARMFUL_CATEGORY_GENERIC_MALWARE = 11;
public static final int HARMFUL_CATEGORY_HARMFUL_SITE = 12;
public static final int HARMFUL_CATEGORY_WINDOWS_MALWARE = 13;
public static final int HARMFUL_CATEGORY_HOSTILE_DOWNLOADER = 14;
public static final int HARMFUL_CATEGORY_NON_ANDROID_THREAT = 15;
public static final int HARMFUL_CATEGORY_ROOTING = 16;
public static final int HARMFUL_CATEGORY_PRIVILEGE_ESCALATION = 17;
public static final int HARMFUL_CATEGORY_TRACKING = 18;
public static final int HARMFUL_CATEGORY_SPAM = 19;
public static final int HARMFUL_CATEGORY_DENIAL_OF_SERVICE = 20;
public static final int HARMFUL_CATEGORY_DATA_COLLECTION = 21;
}

View File

@ -6,6 +6,11 @@
<resources>
<style name="Theme.AppCompat.Light.Dialog.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="Theme.AppCompat.Light.Dialog.Alert.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>

View File

@ -90,18 +90,58 @@ public enum GmsService {
TARGET_DEVICE(76, "com.google.android.gms.smartdevice.d2d.TargetDeviceService.START"),
APP_INVITE(77, "com.google.android.gms.appinvite.service.START"),
TAP_AND_PAY(79, "com.google.android.gms.tapandpay.service.BIND"),
CHROME_SYNC(80, "com.google.android.gms.chromesync.service.START"),
ACCOUNTS(81, "com.google.android.gms.smartdevice.setup.accounts.AccountsService.START"),
CAST_REMOTE_DISPLAY(83, "com.google.android.gms.cast.remote_display.service.START"),
TRUST_AGENT(85, "com.google.android.gms.trustagent.StateApi.START"),
AUTH_SIGN_IN(91, "com.google.android.gms.auth.api.signin.service.START"),
MEASUREMENT(93, "com.google.android.gms.measurement.START"),
FREIGHTER(98, "com.google.android.gms.freighter.service.START"),
GUNS(110, "com.google.android.gms.notifications.service.START"),
BLE(111, "com.google.android.gms.beacon.internal.IBleService.START"),
FIREBASE_AUTH(112, "com.google.firebase.auth.api.gms.service.START"),
APP_INDEXING(113),
GASS(116, "com.google.android.gms.gass.START"),
WORK_ACCOUNT(120),
CAST_FIRSTPATY(122, "com.google.android.gms.cast.firstparty.START"),
AD_CACHE(123, "com.google.android.gms.ads.service.CACHE"),
DYNAMIC_LINKS(131, "com.google.firebase.dynamiclinks.service.START"),
ROMANESCO(135, "com.google.android.gms.romanesco.service.START"),
TRAINER(139, "com.google.android.gms.learning.trainer.START"),
FIDO2_REGULAR(148, "com.google.android.gms.fido.fido2.regular.START"),
FIDO2_PRIVILEGED(149, "com.google.android.gms.fido.fido2.privileged.START"),
DATA_DOWNLOAD(152, "com.google.android.mdd.service.START"),
ACCOUNT_DATA(153, "com.google.android.gms.auth.account.data.service.START"),
CONSTELLATION(155, "com.google.android.gms.constellation.service.START"),
AUDIT(154, "com.google.android.gms.audit.service.START"),
SYSTEM_UPDATE(157, "com.google.android.gms.update.START_API_SERVICE"),
USER_LOCATION(163, "com.google.android.gms.userlocation.service.START"),
LANGUAGE_PROFILE(167, "com.google.android.gms.languageprofile.service.START"),
MDNS(168, "com.google.android.gms.mdns.service.START"),
FIDO2_ZEROPARTY(180, "com.google.android.gms.fido.fido2.zeroparty.START"),
G1_RESTORE(181, "com.google.android.gms.backup.G1_RESTORE"),
G1_BACKUP(182, "com.google.android.gms.backup.G1_BACKUP"),
CARRIER_AUTH(191, "com.google.android.gms.carrierauth.service.START"),
SYSTEM_UPDATE_SINGLE_UESR(192, "com.google.android.gms.update.START_SINGLE_USER_API_SERVICE"),
APP_USAGE(193, "com.google.android.gms.appusage.service.START"),
PHONE_INTERNAL(197, "com.google.android.gms.auth.api.phone.service.InternalService.START"),
PAY(198, "com.google.android.gms.pay.service.BIND"),
ASTERISM(199, "com.google.android.gms.asterism.service.START"),
MODULE_RESTORE(201, "com.google.android.gms.backup.GMS_MODULE_RESTORE"),
FACS_CACHE(202, "com.google.android.gms.facs.cache.service.START"),
RECAPTCHA(205, "com.google.android.gms.recaptcha.service.START"),
CONTACT_SYNC(208, "com.google.android.gms.people.contactssync.service.START"),
IDENTITY_SIGN_IN(212, "com.google.android.gms.auth.api.identity.service.signin.START"),
CREDENTIAL_STORE(214, "com.google.android.gms.fido.credentialstore.internal_service.START"),
EVENT_ATTESTATION(216, "com.google.android.gms.ads.identifier.service.EVENT_ATTESTATION"),
SCHEDULER(218, "com.google.android.gms.scheduler.ACTION_PROXY_SCHEDULE"),
AUTHORIZATION(219, "com.google.android.gms.auth.api.identity.service.authorization.START"),
FACS_SYNC(220, "com.google.android.gms.facs.internal.service.START"),
CONFIG_SYNC(221, "com.google.android.gms.auth.config.service.START"),
CREDENTIAL_SAVING(223, "com.google.android.gms.auth.api.identity.service.credentialsaving.START"),
GOOGLE_AUTH(224, "com.google.android.gms.auth.account.authapi.START"),
ENTERPRISE_LOADER(225, "com.google.android.gms.enterprise.loader.service.START"),
THUNDERBIRD(226, "com.google.android.gms.thunderbird.service.START"),
NEARBY_EXPOSURE(236, "com.google.android.gms.nearby.exposurenotification.START"),
;

View File

@ -64,11 +64,14 @@ dependencies {
implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation "androidx.mediarouter:mediarouter:$mediarouterVersion"
implementation "androidx.preference:preference-ktx:$preferenceVersion"
implementation "androidx.webkit:webkit:$webkitVersion"
// Navigation
implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion"
implementation "androidx.navigation:navigation-ui-ktx:$navigationVersion"
implementation "com.android.volley:volley:$volleyVersion"
implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
}

View File

@ -294,7 +294,7 @@
</intent-filter>
</receiver>
<!-- DroidGuard -->
<!-- DroidGuard / SafetyNet / reCAPTCHA -->
<service android:name="org.microg.gms.droidguard.DroidGuardService">
<intent-filter>
@ -304,6 +304,19 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
<receiver android:name="org.microg.gms.droidguard.ServiceInfoReceiver" />
<service android:name="org.microg.gms.safetynet.SafetyNetClientService">
<intent-filter>
<action android:name="com.google.android.gms.safetynet.service.START" />
</intent-filter>
</service>
<receiver android:name="org.microg.gms.safetynet.ServiceInfoReceiver" />
<!-- TODO: Should be in :ui process and contact DroidGuardService instead of directly invoking droidguard -->
<activity
android:name="org.microg.gms.recaptcha.ReCaptchaActivity"
android:theme="@style/Theme.AppCompat.Light.Dialog.NoActionBar" />
<!-- Car -->
@ -657,13 +670,6 @@
</intent-filter>
</service>
<receiver android:name="org.microg.gms.snet.ServiceInfoReceiver" />
<service android:name="org.microg.gms.snet.SafetyNetClientService">
<intent-filter>
<action android:name="com.google.android.gms.safetynet.service.START" />
</intent-filter>
</service>
<service android:name="org.microg.gms.wallet.PaymentService">
<intent-filter>
<action android:name="com.google.android.gms.wallet.service.BIND" />

View File

@ -1,20 +1,9 @@
/*
* Copyright (C) 2013-2017 microG Project Team
*
* 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.
* SPDX-FileCopyrightText: 2021, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.snet;
package org.microg.gms.safetynet;
import android.annotation.SuppressLint;
import android.content.Context;
@ -24,11 +13,15 @@ import android.content.pm.Signature;
import android.util.Base64;
import android.util.Log;
import com.squareup.wire.Wire;
import org.microg.gms.common.Build;
import org.microg.gms.common.Constants;
import org.microg.gms.common.PackageUtils;
import org.microg.gms.common.Utils;
import org.microg.gms.snet.AttestRequest;
import org.microg.gms.snet.AttestResponse;
import org.microg.gms.snet.FileState;
import org.microg.gms.snet.SELinuxState;
import org.microg.gms.snet.SafetyNetData;
import java.io.ByteArrayInputStream;
import java.io.File;
@ -81,6 +74,7 @@ public class Attestation {
.seLinuxState(new SELinuxState.Builder().enabled(true).supported(true).build())
.suCandidates(Collections.<FileState>emptyList())
.build();
Log.d(TAG, "Payload: "+payload.toString());
return this.payload = payload.encode();
}
@ -108,29 +102,32 @@ public class Attestation {
private ByteString getPackageFileDigest() {
try {
FileInputStream is = new FileInputStream(new File(context.getPackageManager().getApplicationInfo(packageName, 0).sourceDir));
MessageDigest digest = getSha256Digest();
byte[] data = new byte[16384];
while (true) {
int read = is.read(data);
if (read < 0) break;
digest.update(data, 0, read);
}
return ByteString.of(digest.digest());
return ByteString.of(getPackageFileDigest(context, packageName));
} catch (Exception e) {
Log.w(TAG, e);
return null;
}
}
public static byte[] getPackageFileDigest(Context context, String packageName) throws Exception {
FileInputStream is = new FileInputStream(new File(context.getPackageManager().getApplicationInfo(packageName, 0).sourceDir));
MessageDigest digest = getSha256Digest();
byte[] data = new byte[4096];
while (true) {
int read = is.read(data);
if (read < 0) break;
digest.update(data, 0, read);
}
is.close();
return digest.digest();
}
@SuppressLint("PackageManagerGetSignatures")
private List<ByteString> getPackageSignatures() {
try {
PackageInfo pi = context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
ArrayList<ByteString> res = new ArrayList<>();
MessageDigest digest = getSha256Digest();
for (Signature signature : pi.signatures) {
res.add(ByteString.of(digest.digest(signature.toByteArray())));
for (byte[] bytes : getPackageSignatures(context, packageName)) {
res.add(ByteString.of(bytes));
}
return res;
} catch (Exception e) {
@ -139,6 +136,16 @@ public class Attestation {
}
}
public static byte[][] getPackageSignatures(Context context, String packageName) throws Exception {
PackageInfo pi = context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
ArrayList<byte[]> res = new ArrayList<>();
MessageDigest digest = getSha256Digest();
for (Signature signature : pi.signatures) {
res.add(digest.digest(signature.toByteArray()));
}
return res.toArray(new byte[][]{});
}
public String attest(String apiKey) throws IOException {
if (payload == null) {
throw new IllegalStateException("missing payload");
@ -154,6 +161,8 @@ public class Attestation {
connection.setDoOutput(true);
connection.setRequestProperty("content-type", "application/x-protobuf");
connection.setRequestProperty("Accept-Encoding", "gzip");
connection.setRequestProperty("X-Android-Package", packageName);
connection.setRequestProperty("X-Android-Cert", PackageUtils.firstSignatureDigest(context, packageName));
Build build = Utils.getBuild(context);
connection.setRequestProperty("User-Agent", "SafetyNet/" + Constants.GMS_VERSION_CODE + " (" + build.device + " " + build.id + "); gzip");

View File

@ -1,25 +1,13 @@
/*
* Copyright (C) 2017 microG Project Team
*
* 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.
* SPDX-FileCopyrightText: 2021, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.snet;
package org.microg.gms.safetynet;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Log;
import org.microg.gms.common.PackageUtils;

View File

@ -1,37 +0,0 @@
/*
* Copyright (C) 2013-2017 microG Project Team
*
* 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.microg.gms.snet;
import android.os.RemoteException;
import com.google.android.gms.common.internal.GetServiceRequest;
import com.google.android.gms.common.internal.IGmsCallbacks;
import org.microg.gms.BaseService;
import org.microg.gms.common.GmsService;
public class SafetyNetClientService extends BaseService {
public SafetyNetClientService() {
super("GmsSafetyNetClientSvc", GmsService.SAFETY_NET_CLIENT);
}
@Override
public void handleServiceRequest(IGmsCallbacks callback, GetServiceRequest request, GmsService service) throws RemoteException {
callback.onPostInitComplete(0, new SafetyNetClientServiceImpl(this, request.packageName), null);
}
}

View File

@ -1,137 +0,0 @@
/*
* Copyright (C) 2013-2017 microG Project Team
*
* 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.microg.gms.snet;
import android.content.Context;
import android.os.Bundle;
import android.os.Parcel;
import android.os.RemoteException;
import android.util.Base64;
import android.util.Log;
import com.google.android.gms.common.api.CommonStatusCodes;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.safetynet.AttestationData;
import com.google.android.gms.safetynet.HarmfulAppsData;
import com.google.android.gms.safetynet.internal.ISafetyNetCallbacks;
import com.google.android.gms.safetynet.internal.ISafetyNetService;
import org.microg.gms.checkin.LastCheckinInfo;
import org.microg.gms.common.PackageUtils;
import org.microg.gms.droidguard.RemoteDroidGuardConnector;
import java.io.IOException;
import java.util.ArrayList;
public class SafetyNetClientServiceImpl extends ISafetyNetService.Stub {
private static final String TAG = "GmsSafetyNetClientImpl";
private static final String DEFAULT_API_KEY = "AIzaSyDqVnJBjE5ymo--oBJt3On7HQx9xNm1RHA";
private Context context;
private String packageName;
private Attestation attestation;
public SafetyNetClientServiceImpl(Context context, String packageName) {
this.context = context;
this.packageName = packageName;
this.attestation = new Attestation(context, packageName);
}
@Override
public void attest(ISafetyNetCallbacks callbacks, byte[] nonce) throws RemoteException {
attestWithApiKey(callbacks, nonce, DEFAULT_API_KEY);
}
@Override
public void attestWithApiKey(final ISafetyNetCallbacks callbacks, final byte[] nonce, String apiKey) throws RemoteException {
if (nonce == null) {
callbacks.onAttestationData(new Status(CommonStatusCodes.DEVELOPER_ERROR), null);
return;
}
if (!SafetyNetPrefs.get(context).isEnabled()) {
Log.d(TAG, "ignoring SafetyNet request, it's disabled");
callbacks.onAttestationData(Status.CANCELED, null);
return;
}
new Thread(new Runnable() {
@Override
public void run() {
try {
try {
attestation.buildPayload(nonce);
RemoteDroidGuardConnector conn = new RemoteDroidGuardConnector(context);
Bundle bundle = new Bundle();
bundle.putString("contentBinding", attestation.getPayloadHashBase64());
RemoteDroidGuardConnector.Result dg = conn.guard("attest", Long.toString(LastCheckinInfo.read(context).androidId), bundle);
if (!SafetyNetPrefs.get(context).isOfficial() || dg != null && dg.getStatusCode() == 0 && dg.getResult() != null) {
Log.d(TAG, dg == null ? "No dg result" : ("Status: " + dg.getStatusCode() + ", error:" + dg.getErrorMsg()));
if (dg != null && dg.getStatusCode() == 0 && dg.getResult() != null) {
attestation.setDroidGaurdResult(Base64.encodeToString(dg.getResult(), Base64.NO_WRAP + Base64.NO_PADDING + Base64.URL_SAFE));
}
AttestationData data = new AttestationData(attestation.attest(apiKey));
callbacks.onAttestationData(Status.SUCCESS, data);
} else {
callbacks.onAttestationData(dg == null ? Status.INTERNAL_ERROR : new Status(dg.getStatusCode()), null);
}
} catch (IOException e) {
Log.w(TAG, e);
callbacks.onAttestationData(Status.INTERNAL_ERROR, null);
}
} catch (RemoteException e) {
Log.w(TAG, e);
}
}
}).start();
}
@Override
public void getSharedUuid(ISafetyNetCallbacks callbacks) throws RemoteException {
PackageUtils.checkPackageUid(context, packageName, getCallingUid());
PackageUtils.assertExtendedAccess(context);
// TODO
Log.d(TAG, "dummy Method: getSharedUuid");
callbacks.onString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
}
@Override
public void lookupUri(ISafetyNetCallbacks callbacks, String s1, int[] threatTypes, int i, String s2) throws RemoteException {
Log.d(TAG, "unimplemented Method: lookupUri");
}
@Override
public void init(ISafetyNetCallbacks callbacks) throws RemoteException {
Log.d(TAG, "dummy Method: init");
callbacks.onBoolean(Status.SUCCESS, true);
}
@Override
public void getHarmfulAppsList(ISafetyNetCallbacks callbacks) throws RemoteException {
Log.d(TAG, "dummy Method: unknown4");
callbacks.onHarmfulAppsData(Status.SUCCESS, new ArrayList<HarmfulAppsData>());
}
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
if (super.onTransact(code, data, reply, flags)) return true;
Log.d(TAG, "onTransact [unknown]: " + code + ", " + data + ", " + flags);
return false;
}
}

View File

@ -28,9 +28,9 @@ import org.microg.tools.ui.AbstractSettingsActivity;
import org.microg.tools.ui.RadioButtonPreference;
import org.microg.tools.ui.ResourceSettingsFragment;
import static org.microg.gms.snet.SafetyNetPrefs.PREF_SNET_OFFICIAL;
import static org.microg.gms.snet.SafetyNetPrefs.PREF_SNET_SELF_SIGNED;
import static org.microg.gms.snet.SafetyNetPrefs.PREF_SNET_THIRD_PARTY;
import static org.microg.gms.safetynet.SafetyNetPrefs.PREF_SNET_OFFICIAL;
import static org.microg.gms.safetynet.SafetyNetPrefs.PREF_SNET_SELF_SIGNED;
import static org.microg.gms.safetynet.SafetyNetPrefs.PREF_SNET_THIRD_PARTY;
public class SafetyNetAdvancedFragment extends ResourceSettingsFragment {

View File

@ -0,0 +1,70 @@
/*
* SPDX-FileCopyrightText: 2021, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.droidguard
import android.content.Context
import android.content.SharedPreferences
import java.io.File
class DroidGuardPreferences(private val context: Context) {
@Suppress("DEPRECATION")
private val preferences by lazy { context.getSharedPreferences("droidguard", Context.MODE_PRIVATE) }
private val systemDefaultPreferences by lazy {
try {
Context::class.java.getDeclaredMethod("getSharedPreferences", File::class.java, Int::class.javaPrimitiveType).invoke(context, File("/system/etc/microg.xml"), Context.MODE_PRIVATE) as SharedPreferences
} catch (ignored: Exception) {
null
}
}
private var editing: Boolean = false
private val updates: MutableMap<String, Any?> = hashMapOf()
var mode: Mode
get() = try {
getSettingsString(PREF_DROIDGUARD_MODE)?.let { Mode.valueOf(it) } ?: Mode.Connector
} catch (e: Exception) {
Mode.Connector
}
set(value) {
if (editing) updates[PREF_DROIDGUARD_MODE] = value.name
}
var networkServerUrl: String?
get() = getSettingsString(PREF_DROIDGUARD_NETWORK_SERVER_URL)
set(value) {
if (editing) updates[PREF_DROIDGUARD_NETWORK_SERVER_URL] = value
}
private fun getSettingsString(key: String): String? {
return systemDefaultPreferences?.getString(key, null) ?: preferences.getString(key, null)
}
fun edit(commands: DroidGuardPreferences.() -> Unit) {
editing = true
commands(this)
preferences.edit().also {
for ((k, v) in updates) {
when (v) {
is String -> it.putString(k, v)
null -> it.remove(k)
}
}
}.apply()
editing = false
}
enum class Mode {
Disabled,
Connector,
Network
}
companion object {
const val PREF_DROIDGUARD_MODE = "droidguard_mode"
const val PREF_DROIDGUARD_NETWORK_SERVER_URL = "droidguard_network_server_url"
}
}

View File

@ -0,0 +1,70 @@
/*
* SPDX-FileCopyrightText: 2021, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.droidguard
import android.content.Context
import android.os.Bundle
import android.util.Base64
import com.android.volley.VolleyError
import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley
import org.microg.gms.checkin.LastCheckinInfo
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
interface DroidGuardResultCreator {
suspend fun getResult(flow: String, data: Map<String, String>): ByteArray
companion object {
fun getInstance(context: Context): DroidGuardResultCreator = when (DroidGuardPreferences(context).mode) {
DroidGuardPreferences.Mode.Disabled -> throw RuntimeException("DroidGuard disabled")
DroidGuardPreferences.Mode.Connector -> ConnectorDroidGuardResultCreator(context)
DroidGuardPreferences.Mode.Network -> NetworkDroidGuardResultCreator(context)
}
suspend fun getResult(context: Context, flow: String, data: Map<String, String>): ByteArray =
getInstance(context).getResult(flow, data)
}
}
private class ConnectorDroidGuardResultCreator(private val context: Context) : DroidGuardResultCreator {
override suspend fun getResult(flow: String, data: Map<String, String>): ByteArray = suspendCoroutine { continuation ->
Thread {
val bundle = Bundle()
for (entry in data) {
bundle.putString(entry.key, entry.value)
}
val conn = RemoteDroidGuardConnector(context)
val dg = conn.guard(flow, LastCheckinInfo.read(context).androidId.toString(), bundle)
if (dg == null) {
continuation.resumeWithException(RuntimeException("No DroidGuard result"))
} else if (dg.statusCode == 0 && dg.result != null) {
continuation.resume(dg.result)
} else {
continuation.resumeWithException(RuntimeException("Status: " + dg.statusCode + ", error:" + dg.errorMsg))
}
}.start()
}
}
private class NetworkDroidGuardResultCreator(private val context: Context) : DroidGuardResultCreator {
private val queue = Volley.newRequestQueue(context)
private val url: String
get() = DroidGuardPreferences(context).networkServerUrl ?: throw RuntimeException("Network URL required")
override suspend fun getResult(flow: String, data: Map<String, String>): ByteArray = suspendCoroutine { continuation ->
queue.add(PostParamsStringRequest("$url?flow=$flow", data, {
continuation.resume(Base64.decode(it, Base64.NO_WRAP + Base64.NO_PADDING + Base64.URL_SAFE))
}, {
continuation.resumeWithException(RuntimeException(it))
}))
}
}
class PostParamsStringRequest(url: String, private val data: Map<String, String>, listener: (String) -> Unit, errorListener: (VolleyError) -> Unit) : StringRequest(Method.POST, url, listener, errorListener) {
override fun getParams(): Map<String, String> = data
}

View File

@ -0,0 +1,93 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.droidguard
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.util.Log
import java.io.Serializable
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
private const val ACTION_SERVICE_INFO_REQUEST = "org.microg.gms.droidguard.SERVICE_INFO_REQUEST"
private const val ACTION_UPDATE_CONFIGURATION = "org.microg.gms.droidguard.UPDATE_CONFIGURATION"
private const val ACTION_SERVICE_INFO_RESPONSE = "org.microg.gms.droidguard.SERVICE_INFO_RESPONSE"
private const val EXTRA_SERVICE_INFO = "org.microg.gms.droidguard.SERVICE_INFO"
private const val EXTRA_CONFIGURATION = "org.microg.gms.droidguard.CONFIGURATION"
private const val TAG = "GmsGcmStatusInfo"
data class ServiceInfo(val configuration: ServiceConfiguration) : Serializable
data class ServiceConfiguration(val mode: DroidGuardPreferences.Mode, val networkServerUrl: String?) : Serializable {
fun saveToPrefs(context: Context) {
DroidGuardPreferences(context).edit {
mode = this@ServiceConfiguration.mode
networkServerUrl = this@ServiceConfiguration.networkServerUrl
}
}
}
private fun DroidGuardPreferences.toConfiguration(): ServiceConfiguration = ServiceConfiguration(mode, networkServerUrl)
class ServiceInfoReceiver : BroadcastReceiver() {
private fun sendInfoResponse(context: Context) {
context.sendOrderedBroadcast(Intent(ACTION_SERVICE_INFO_RESPONSE).apply {
setPackage(context.packageName)
putExtra(EXTRA_SERVICE_INFO, ServiceInfo(DroidGuardPreferences(context).toConfiguration()))
}, null)
}
override fun onReceive(context: Context, intent: Intent) {
try {
when (intent.action) {
ACTION_UPDATE_CONFIGURATION -> {
(intent.getSerializableExtra(EXTRA_CONFIGURATION) as? ServiceConfiguration)?.saveToPrefs(context)
}
}
sendInfoResponse(context)
} catch (e: Exception) {
Log.w(TAG, e)
}
}
}
private suspend fun sendToServiceInfoReceiver(intent: Intent, context: Context): ServiceInfo = suspendCoroutine {
context.registerReceiver(object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
context.unregisterReceiver(this)
val serviceInfo = try {
intent.getSerializableExtra(EXTRA_SERVICE_INFO) as ServiceInfo
} catch (e: Exception) {
it.resumeWithException(e)
return
}
try {
it.resume(serviceInfo)
} catch (e: Exception) {
Log.w(TAG, e)
}
}
}, IntentFilter(ACTION_SERVICE_INFO_RESPONSE))
try {
context.sendOrderedBroadcast(intent, null)
} catch (e: Exception) {
it.resumeWithException(e)
}
}
suspend fun getDroidGuardServiceInfo(context: Context): ServiceInfo = sendToServiceInfoReceiver(
Intent(context, ServiceInfoReceiver::class.java).apply {
action = ACTION_SERVICE_INFO_REQUEST
}, context)
suspend fun setDroidGuardServiceConfiguration(context: Context, configuration: ServiceConfiguration): ServiceInfo = sendToServiceInfoReceiver(
Intent(context, ServiceInfoReceiver::class.java).apply {
action = ACTION_UPDATE_CONFIGURATION
putExtra(EXTRA_CONFIGURATION, configuration)
}, context)

View File

@ -0,0 +1,204 @@
/*
* SPDX-FileCopyrightText: 2021, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.recaptcha
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.net.http.SslCertificate
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
import android.os.ResultReceiver
import android.util.Base64
import android.util.Log
import android.view.View
import android.view.Window
import android.webkit.*
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.webkit.WebViewClientCompat
import com.google.android.gms.R
import com.google.android.gms.safetynet.SafetyNetStatusCodes.*
import org.microg.gms.droidguard.DroidGuardResultCreator
import java.io.ByteArrayInputStream
import java.net.URLEncoder
import java.security.MessageDigest
import kotlin.math.min
private const val TAG = "GmsReCAPTCHA"
fun StringBuilder.appendUrlEncodedParam(key: String, value: String?) = append("&")
.append(URLEncoder.encode(key, "UTF-8"))
.append("=")
.append(value?.let { URLEncoder.encode(it, "UTF-8") } ?: "")
class ReCaptchaActivity : AppCompatActivity() {
private val receiver: ResultReceiver?
get() = intent?.getParcelableExtra("result") as ResultReceiver?
private val params: String?
get() = intent?.getStringExtra("params")
private val webView: WebView?
get() = findViewById(R.id.recaptcha_webview)
private val loading: View?
get() = findViewById(R.id.recaptcha_loading)
private val density: Float
get() = resources.displayMetrics.density
private val widthPixels: Int
get() = resources.displayMetrics.widthPixels
private val heightPixels: Int
get() {
val base = resources.displayMetrics.heightPixels
val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android")
val statusBarHeight = if (statusBarHeightId > 0) resources.getDimensionPixelSize(statusBarHeightId) else 0
return base - statusBarHeight - (density * 20.0).toInt()
}
@SuppressLint("SetJavaScriptEnabled", "AddJavascriptInterface")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (receiver == null || params == null) {
finish()
return
}
requestWindowFeature(Window.FEATURE_NO_TITLE)
setContentView(R.layout.recaptcha_window)
webView?.apply {
webViewClient = object : WebViewClientCompat() {
fun String.isRecaptchaUrl() = startsWith("https://www.gstatic.com/recaptcha/") || startsWith("https://www.google.com/recaptcha/") || startsWith("https://www.google.com/js/bg/")
override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? {
if (url.isRecaptchaUrl()) {
return null
}
return WebResourceResponse("text/plain", "UTF-8", ByteArrayInputStream(byteArrayOf()))
}
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
if (url.startsWith("https://support.google.com/recaptcha")) {
startActivity(Intent("android.intent.action.VIEW", Uri.parse(url)))
finish()
return true
}
return !url.isRecaptchaUrl()
}
}
settings.apply {
javaScriptEnabled = true
useWideViewPort = true
displayZoomControls = false
setSupportZoom(false)
cacheMode = WebSettings.LOAD_NO_CACHE
}
addJavascriptInterface(object {
@JavascriptInterface
fun challengeReady() {
Log.d(TAG, "challengeReady()")
runOnUiThread { webView?.loadUrl("javascript: RecaptchaMFrame.show(${min(widthPixels / density, 400f)}, ${min(heightPixels / density, 400f)});") }
}
@JavascriptInterface
fun getClientAPIVersion() = 1
@JavascriptInterface
fun onChallengeExpired() {
Log.d(TAG, "onChallengeExpired()")
}
@JavascriptInterface
fun onError(errorCode: Int, finish: Boolean) {
Log.d(TAG, "onError($errorCode, $finish)")
when (errorCode) {
1 -> receiver?.send(ERROR, Bundle().apply { putString("error", "Invalid Input Argument"); putInt("errorCode", ERROR) })
2 -> receiver?.send(TIMEOUT, Bundle().apply { putString("error", "Session Timeout"); putInt("errorCode", TIMEOUT) })
7 -> receiver?.send(RECAPTCHA_INVALID_SITEKEY, Bundle().apply { putString("error", "Invalid Site Key"); putInt("errorCode", RECAPTCHA_INVALID_SITEKEY) })
8 -> receiver?.send(RECAPTCHA_INVALID_KEYTYPE, Bundle().apply { putString("error", "Invalid Type of Site Key"); putInt("errorCode", RECAPTCHA_INVALID_KEYTYPE) })
9 -> receiver?.send(RECAPTCHA_INVALID_PACKAGE_NAME, Bundle().apply { putString("error", "Invalid Package Name for App"); putInt("errorCode", RECAPTCHA_INVALID_PACKAGE_NAME) })
else -> receiver?.send(ERROR, Bundle().apply { putString("error", "error"); putInt("errorCode", ERROR) })
}
if (finish) this@ReCaptchaActivity.finish()
}
@JavascriptInterface
fun onResize(width: Int, height: Int) {
Log.d(TAG, "onResize($width, $height)")
if (webView?.visibility == View.VISIBLE) {
runOnUiThread { setWebViewSize(width, height, true) }
} else {
runOnUiThread { webView?.loadUrl("javascript: RecaptchaMFrame.shown($width, $height, true);") }
}
}
@JavascriptInterface
fun onShow(visible: Boolean, width: Int, height: Int) {
Log.d(TAG, "onShow($visible, $width, $height)")
if (width <= 0 && height <= 0) {
runOnUiThread { webView?.loadUrl("javascript: RecaptchaMFrame.shown($width, $height, $visible);") }
} else {
runOnUiThread {
setWebViewSize(width, height, visible)
loading?.visibility = if (visible) View.GONE else View.VISIBLE
webView?.visibility = if (visible) View.VISIBLE else View.GONE
}
}
}
@JavascriptInterface
fun requestToken(s: String, b: Boolean) {
Log.d(TAG, "requestToken($s, $b)")
runOnUiThread {
val cert = webView?.certificate?.let { Base64.encodeToString(SslCertificate.saveState(it).getByteArray("x509-certificate"), Base64.URL_SAFE + Base64.NO_PADDING + Base64.NO_WRAP) }
?: ""
val params = StringBuilder(params).appendUrlEncodedParam("c", s).appendUrlEncodedParam("sc", cert).appendUrlEncodedParam("mt", System.currentTimeMillis().toString()).toString()
val flow = "recaptcha-android-${if (b) "verify" else "reload"}"
lifecycleScope.launchWhenResumed {
updateToken(flow, params)
}
}
}
@JavascriptInterface
fun verifyCallback(token: String) {
Log.d(TAG, "verifyCallback($token)")
receiver?.send(0, Bundle().apply { putString("token", token) })
finish()
}
}, "RecaptchaEmbedder")
}
lifecycleScope.launchWhenResumed {
open()
}
}
fun setWebViewSize(width: Int, height: Int, visible: Boolean) {
webView?.apply {
layoutParams.width = min(widthPixels, (width * density).toInt())
layoutParams.height = min(heightPixels, (height * density).toInt())
requestLayout()
loadUrl("javascript: RecaptchaMFrame.shown(${(layoutParams.width / density).toInt()}, ${(layoutParams.height / density).toInt()}, $visible);")
}
}
suspend fun updateToken(flow: String, params: String) {
val map = mapOf("contentBinding" to Base64.encodeToString(MessageDigest.getInstance("SHA-256").digest(params.toByteArray()), Base64.NO_WRAP))
val dg = Base64.encodeToString(DroidGuardResultCreator.getResult(this, flow, map), Base64.NO_WRAP + Base64.URL_SAFE + Base64.NO_PADDING)
if (SDK_INT >= 19) {
webView?.evaluateJavascript("RecaptchaMFrame.token('${URLEncoder.encode(dg, "UTF-8")}', '$params');", null)
} else {
webView?.loadUrl("javascript: RecaptchaMFrame.token('${URLEncoder.encode(dg, "UTF-8")}', '$params');")
}
}
suspend fun open() {
val params = StringBuilder(params).appendUrlEncodedParam("mt", System.currentTimeMillis().toString()).toString()
val map = mapOf("contentBinding" to Base64.encodeToString(MessageDigest.getInstance("SHA-256").digest(params.toByteArray()), Base64.NO_WRAP))
val dg = Base64.encodeToString(DroidGuardResultCreator.getResult(this, "recaptcha-android-frame", map), Base64.NO_WRAP + Base64.URL_SAFE + Base64.NO_PADDING)
webView?.postUrl(MFRAME_URL, "mav=1&dg=${URLEncoder.encode(dg, "UTF-8")}&mp=${URLEncoder.encode(params, "UTF-8")}".toByteArray())
}
companion object {
private const val MFRAME_URL = "https://www.google.com/recaptcha/api2/mframe"
}
}

View File

@ -0,0 +1,156 @@
/*
* SPDX-FileCopyrightText: 2021, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.safetynet
import android.content.Context
import android.content.Intent
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
import android.os.Parcel
import android.os.ResultReceiver
import android.util.Base64
import android.util.Log
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.gms.common.api.Status
import com.google.android.gms.common.internal.GetServiceRequest
import com.google.android.gms.common.internal.IGmsCallbacks
import com.google.android.gms.safetynet.AttestationData
import com.google.android.gms.safetynet.RecaptchaResultData
import com.google.android.gms.safetynet.internal.ISafetyNetCallbacks
import com.google.android.gms.safetynet.internal.ISafetyNetService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.microg.gms.BaseService
import org.microg.gms.checkin.LastCheckinInfo
import org.microg.gms.common.GmsService
import org.microg.gms.common.PackageUtils
import org.microg.gms.droidguard.DroidGuardResultCreator
import org.microg.gms.recaptcha.ReCaptchaActivity
import org.microg.gms.recaptcha.appendUrlEncodedParam
import java.io.IOException
import java.util.*
private const val TAG = "GmsSafetyNet"
private const val DEFAULT_API_KEY = "AIzaSyDqVnJBjE5ymo--oBJt3On7HQx9xNm1RHA"
class SafetyNetClientService : BaseService(TAG, GmsService.SAFETY_NET_CLIENT) {
override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) {
callback.onPostInitComplete(0, SafetyNetClientServiceImpl(this, request.packageName, lifecycle), null)
}
}
class SafetyNetClientServiceImpl(private val context: Context, private val packageName: String, private val lifecycle: Lifecycle) : ISafetyNetService.Stub(), LifecycleOwner {
override fun getLifecycle(): Lifecycle = lifecycle
override fun attest(callbacks: ISafetyNetCallbacks, nonce: ByteArray) {
attestWithApiKey(callbacks, nonce, DEFAULT_API_KEY)
}
override fun attestWithApiKey(callbacks: ISafetyNetCallbacks, nonce: ByteArray?, apiKey: String) {
if (nonce == null) {
callbacks.onAttestationData(Status(CommonStatusCodes.DEVELOPER_ERROR), null)
return
}
if (!SafetyNetPrefs.get(context).isEnabled) {
Log.d(TAG, "ignoring SafetyNet request, it's disabled")
callbacks.onAttestationData(Status.CANCELED, null)
return
}
lifecycleScope.launchWhenStarted {
try {
val attestation = Attestation(context, packageName)
attestation.buildPayload(nonce)
try {
val dg = DroidGuardResultCreator.getResult(context, "attest", mapOf("contentBinding" to attestation.payloadHashBase64))
attestation.setDroidGaurdResult(Base64.encodeToString(dg, Base64.NO_WRAP + Base64.NO_PADDING + Base64.URL_SAFE))
} catch (e: Exception) {
if (SafetyNetPrefs.get(context).isOfficial) throw e
Log.w(TAG, e)
null
}
val data = withContext(Dispatchers.IO) { AttestationData(attestation.attest(apiKey)) }
callbacks.onAttestationData(Status.SUCCESS, data)
} catch (e: IOException) {
Log.w(TAG, e)
callbacks.onAttestationData(Status.INTERNAL_ERROR, null)
}
}
}
override fun getSharedUuid(callbacks: ISafetyNetCallbacks) {
PackageUtils.checkPackageUid(context, packageName, getCallingUid())
PackageUtils.assertExtendedAccess(context)
// TODO
Log.d(TAG, "dummy Method: getSharedUuid")
callbacks.onString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
}
override fun lookupUri(callbacks: ISafetyNetCallbacks, s1: String, threatTypes: IntArray, i: Int, s2: String) {
Log.d(TAG, "unimplemented Method: lookupUri")
}
override fun init(callbacks: ISafetyNetCallbacks) {
Log.d(TAG, "dummy Method: init")
callbacks.onBoolean(Status.SUCCESS, true)
}
override fun getHarmfulAppsList(callbacks: ISafetyNetCallbacks) {
Log.d(TAG, "dummy Method: unknown4")
callbacks.onHarmfulAppsData(Status.SUCCESS, ArrayList())
}
override fun verifyWithRecaptcha(callbacks: ISafetyNetCallbacks, siteKey: String?) {
if (siteKey == null) {
callbacks.onAttestationData(Status(CommonStatusCodes.DEVELOPER_ERROR), null)
return
}
if (!SafetyNetPrefs.get(context).isEnabled) {
Log.d(TAG, "ignoring SafetyNet request, it's disabled")
callbacks.onAttestationData(Status.CANCELED, null)
return
}
val intent = Intent(context, ReCaptchaActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
val params = StringBuilder()
params.appendUrlEncodedParam("k", siteKey)
.appendUrlEncodedParam("di", LastCheckinInfo.read(context).androidId.toString())
.appendUrlEncodedParam("pk", packageName)
.appendUrlEncodedParam("sv", SDK_INT.toString())
.appendUrlEncodedParam("gv", "20.47.14 (040306-{{cl}})")
.appendUrlEncodedParam("gm", "260")
.appendUrlEncodedParam("as", Base64.encodeToString(Attestation.getPackageFileDigest(context, packageName), Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING))
for (signature in Attestation.getPackageSignatures(context, packageName)) {
params.appendUrlEncodedParam("ac", Base64.encodeToString(signature, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING))
}
params.appendUrlEncodedParam("ip", "com.android.vending")
.appendUrlEncodedParam("av", false.toString())
.appendUrlEncodedParam("si", null)
intent.putExtra("params", params.toString())
intent.putExtra("result", object : ResultReceiver(null) {
override fun onReceiveResult(resultCode: Int, resultData: Bundle) {
if (resultCode != 0) {
callbacks.onRecaptchaResult(Status(resultData.getInt("errorCode"), resultData.getString("error")), null)
} else {
callbacks.onRecaptchaResult(Status.SUCCESS, RecaptchaResultData().apply { token = resultData.getString("token") })
}
}
})
context.startActivity(intent)
}
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
if (super.onTransact(code, data, reply, flags)) return true
Log.d(TAG, "onTransact [unknown]: $code, $data, $flags")
return false
}
}

View File

@ -1,9 +1,9 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-FileCopyrightText: 2021, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.snet
package org.microg.gms.safetynet
import android.content.BroadcastReceiver
import android.content.Context

View File

@ -13,9 +13,9 @@ import androidx.navigation.fragment.findNavController
import com.google.android.gms.R
import com.google.android.gms.databinding.SafetyNetFragmentBinding
import org.microg.gms.checkin.getCheckinServiceInfo
import org.microg.gms.snet.ServiceInfo
import org.microg.gms.snet.getSafetyNetServiceInfo
import org.microg.gms.snet.setSafetyNetServiceConfiguration
import org.microg.gms.safetynet.ServiceInfo
import org.microg.gms.safetynet.getSafetyNetServiceInfo
import org.microg.gms.safetynet.setSafetyNetServiceConfiguration
class SafetyNetFragment : Fragment(R.layout.safety_net_fragment) {

View File

@ -0,0 +1,64 @@
<!--
~ SPDX-FileCopyrightText: 2021, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M2,24L2,40.5L5.9387,36.5616a22,22 45,0 0,9.6423 7.7639,22 22,0 0,0 23.9753,-4.7692L31.071,31.071A10,10 0,0 1,24 34,10 10,135 0,1 14.7361,27.7642L18.5,24L14,24Z"
android:strokeWidth="0"
android:fillColor="#bdbdbd"
android:strokeColor="#000000"/>
<path
android:pathData="M7.5,2 L11.4386,5.9387A22,22 0,0 0,2 24L14,24A10,10 135,0 1,20.236 14.7361L24,18.5L24,14 24,2l-0.0682,0z"
android:strokeWidth="0"
android:strokeColor="#000000">
<aapt:attr name="android:fillColor">
<gradient
android:startY="24"
android:startX="14"
android:endY="20.75"
android:endX="14.000"
android:type="linear">
<item android:offset="0" android:color="#FF1E88E5"/>
<item android:offset="1" android:color="#FF2196F3"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M46,7.5 L42.0615,11.4386A22,22 0,0 0,24 2V14a10,10 0,0 1,9.264 6.2358l-3.7641,3.7641h4.5,12v-0.0682z"
android:strokeWidth="0"
android:strokeColor="#000000">
<aapt:attr name="android:fillColor">
<gradient
android:startY="14"
android:startX="24"
android:endY="14"
android:endX="27.25"
android:type="linear">
<item android:offset="0" android:color="#FF3949AB"/>
<item android:offset="1" android:color="#FF3F51B5"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M46,7.5 L42.0615,11.4386C37.9491,5.5255 31.2026,2 24,2L46,24v-0.0682z"
android:strokeWidth="0">
<aapt:attr name="android:fillColor">
<gradient
android:startY="2"
android:startX="24"
android:endY="24"
android:endX="46"
android:type="linear">
<item android:offset="0" android:color="#18FFFFFF"/>
<item android:offset="1" android:color="#00FFFFFF"/>
</gradient>
</aapt:attr>
</path>
</vector>

View File

@ -25,7 +25,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/colorBackground"
android:orientation="vertical">
android:divider="?attr/dividerHorizontal"
android:orientation="vertical"
android:showDividers="middle">
<LinearLayout
android:layout_width="match_parent"
@ -58,15 +60,12 @@
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="0.25dp"
android:background="?android:attr/textColorSecondary" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:divider="?attr/dividerHorizontal"
android:orientation="vertical"
android:showDividers="middle">
<Button
android:id="@+id/permission_allow_button"
@ -78,11 +77,6 @@
android:text="@string/allow">
</Button>
<View
android:layout_width="match_parent"
android:layout_height="0.25dp"
android:background="?android:attr/textColorSecondary" />
<Button
android:id="@+id/permission_deny_button"
android:layout_width="match_parent"

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2021, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/recaptcha_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginLeft="20dip"
android:layout_marginTop="20dip"
android:layout_marginRight="20dip"
android:layout_marginBottom="10sp"
android:gravity="center"
android:orientation="horizontal">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_recaptcha" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="10sp"
android:layout_marginLeft="10sp"
android:text="reCAPTCHA"
android:textColor="#FF3949AB"
android:textStyle="bold" />
</LinearLayout>
<ProgressBar
style="?android:progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginLeft="20dip"
android:layout_marginTop="10sp"
android:layout_marginRight="20dip"
android:layout_marginBottom="20dip"
android:indeterminate="true"
android:indeterminateTint="#FF3949AB" />
</LinearLayout>
<WebView
android:id="@+id/recaptcha_webview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:visibility="gone" />
</FrameLayout>