Update SafetyNet extension implementation

This commit is contained in:
topjohnwu 2018-06-10 02:35:03 +08:00
parent b0a5dbb4c2
commit 90d218ebc8
7 changed files with 53 additions and 75 deletions

3
.gitignore vendored
View File

@ -4,6 +4,9 @@ out
*.apk *.apk
config.prop config.prop
# Manually dumped jars
snet/libs
# Built binaries # Built binaries
native/out native/out

View File

@ -12,8 +12,8 @@
1. Building is tested on macOS, Ubuntu, and Windows 10 using the latest stable NDK and NDK r10e. Officially released binaries were built with NDK r10e. 1. Building is tested on macOS, Ubuntu, and Windows 10 using the latest stable NDK and NDK r10e. Officially released binaries were built with NDK r10e.
2. Set configurations in `config.prop`. A sample file `config.prop.sample` is provided as an example. 2. Set configurations in `config.prop`. A sample file `config.prop.sample` is provided as an example.
3. Run `build.py` with argument `-h` to see the built-in help message. The `-h` option also works for each supported actions, e.g. `./build.py binary -h` 3. Run `build.py` with argument `-h` to see the built-in help message. The `-h` option also works for each supported actions, e.g. `./build.py binary -h`
4. By default, `build.py` will build binaries and Magisk Manager in debug mode. If you want to build Magisk Manager in release mode (through the `--release` flag), you will need to place a Java Keystore file at `release-key.jks` to sign Magisk Manager's APK. For more information, check out [Google's Official Documentation](https://developer.android.com/studio/publish/app-signing.html#signing-manually). 4. By default, `build.py` build binaries and Magisk Manager in debug mode. If you want to build Magisk Manager in release mode (via the `--release` flag), you need a Java Keystore file `release-key.jks` to sign Magisk Manager's APK. For more information, check out [Google's Official Documentation](https://developer.android.com/studio/publish/app-signing.html#signing-manually).
5. The SafetyNet extension pack requires the full Magisk Manager as a `compileOnly` dependency. Build the **release** APK, convert it back to Java `.class` files (I use [dex2jar](https://github.com/pxb1988/dex2jar)), and place the converted JAR under `snet/libs` before compiling.
## License ## License

2
app

@ -1 +1 @@
Subproject commit a0466085fe3b5eb8929c893d02459e6d94ecb115 Subproject commit 499a157946f0063aea0795b204a1f2858591d8d3

View File

@ -83,7 +83,6 @@ def build_all(args):
build_apk(args) build_apk(args)
zip_main(args) zip_main(args)
zip_uninstaller(args) zip_uninstaller(args)
build_snet(args)
def collect_binary(): def collect_binary():
for arch in ['armeabi-v7a', 'x86']: for arch in ['armeabi-v7a', 'x86']:
@ -235,7 +234,11 @@ def build_snet(args):
error('Build snet extention failed!') error('Build snet extention failed!')
source = os.path.join('snet', 'build', 'outputs', 'apk', 'release', 'snet-release-unsigned.apk') source = os.path.join('snet', 'build', 'outputs', 'apk', 'release', 'snet-release-unsigned.apk')
target = os.path.join(config['outdir'], 'snet.apk') target = os.path.join(config['outdir'], 'snet.apk')
mv(source, target) # Re-compress the whole APK for smaller size
with zipfile.ZipFile(target, 'w', compression=zipfile.ZIP_DEFLATED, allowZip64=False) as zout:
with zipfile.ZipFile(source) as zin:
for item in zin.infolist():
zout.writestr(item.filename, zin.read(item))
header('Output: ' + target) header('Output: ' + target)
def gen_update_binary(): def gen_update_binary():

View File

@ -6,7 +6,7 @@ android {
defaultConfig { defaultConfig {
applicationId "com.topjohnwu.snet" applicationId "com.topjohnwu.snet"
minSdkVersion 21 minSdkVersion 14
targetSdkVersion rootProject.ext.compileSdkVersion targetSdkVersion rootProject.ext.compileSdkVersion
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"
@ -15,12 +15,13 @@ android {
buildTypes { buildTypes {
release { release {
minifyEnabled true minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
} }
} }
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) compileOnly fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.google.android.gms:play-services-safetynet:7.0.0' /* The oldest version */ implementation 'com.google.android.gms:play-services-safetynet:7.0.0' /* The oldest version */
} }

View File

@ -1,5 +0,0 @@
package com.topjohnwu.snet;
public interface SafetyNetCallback {
void onResponse(int responseCode);
}

View File

@ -1,7 +1,5 @@
package com.topjohnwu.snet; package com.topjohnwu.snet;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
@ -11,18 +9,19 @@ import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil; import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.ResultCallback; import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.safetynet.SafetyNet; import com.google.android.gms.safetynet.SafetyNet;
import com.google.android.gms.safetynet.SafetyNetApi; import com.google.android.gms.safetynet.SafetyNetApi;
import com.topjohnwu.magisk.asyncs.CheckSafetyNet;
import com.topjohnwu.magisk.components.Activity;
import com.topjohnwu.magisk.utils.ISafetyNetHelper;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import java.lang.reflect.Field;
import java.security.SecureRandom; import java.security.SecureRandom;
public class SafetyNetHelper public class SafetyNetHelper implements ISafetyNetHelper, GoogleApiClient.ConnectionCallbacks,
implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener { GoogleApiClient.OnConnectionFailedListener, ResultCallback<SafetyNetApi.AttestationResult> {
public static final int CAUSE_SERVICE_DISCONNECTED = 0x01; public static final int CAUSE_SERVICE_DISCONNECTED = 0x01;
public static final int CAUSE_NETWORK_LOST = 0x02; public static final int CAUSE_NETWORK_LOST = 0x02;
@ -32,36 +31,24 @@ public class SafetyNetHelper
public static final int BASIC_PASS = 0x10; public static final int BASIC_PASS = 0x10;
public static final int CTS_PASS = 0x20; public static final int CTS_PASS = 0x20;
public static final int SNET_EXT_VER = 7; public static final int SNET_EXT_VER = 8;
private GoogleApiClient mGoogleApiClient; private GoogleApiClient mGoogleApiClient;
private Activity mActivity; private Activity mActivity;
private int responseCode; private Callback callback;
private SafetyNetCallback cb;
private String dexPath;
private boolean isDarkTheme;
public static int getVersion() { @Override
public int getVersion() {
return SNET_EXT_VER; return SNET_EXT_VER;
} }
public SafetyNetHelper(Activity activity, String dexPath, SafetyNetCallback cb) { public SafetyNetHelper(Activity activity, Callback cb) {
mActivity = activity; mActivity = activity;
this.cb = cb; callback = cb;
this.dexPath = dexPath;
responseCode = 0;
// Get theme
try {
Context context = activity.getApplicationContext();
Field theme = context.getClass().getField("isDarkTheme");
isDarkTheme = (boolean) theme.get(context);
} catch (Exception e) {
e.printStackTrace();
}
} }
// Entry point to start test // Entry point to start test
@Override
public void attest() { public void attest() {
// Connect Google Service // Connect Google Service
mGoogleApiClient = new GoogleApiClient.Builder(mActivity) mGoogleApiClient = new GoogleApiClient.Builder(mActivity)
@ -74,26 +61,15 @@ public class SafetyNetHelper
@Override @Override
public void onConnectionSuspended(int i) { public void onConnectionSuspended(int i) {
cb.onResponse(i); callback.onResponse(i);
} }
@Override @Override
public void onConnectionFailed(@NonNull ConnectionResult result) { public void onConnectionFailed(@NonNull ConnectionResult result) {
Class<? extends Activity> clazz = mActivity.getClass(); mActivity.swapResources(CheckSafetyNet.dexPath.getPath());
try { GooglePlayServicesUtil.getErrorDialog(result.getErrorCode(), mActivity, 0).show();
// Use external resources mActivity.restoreResources();
clazz.getMethod("swapResources", String.class, int.class).invoke(mActivity, dexPath, callback.onResponse(CONNECTION_FAIL);
isDarkTheme ? android.R.style.Theme_Material : android.R.style.Theme_Material_Light);
try {
GooglePlayServicesUtil.getErrorDialog(result.getErrorCode(), mActivity, 0).show();
} catch (Exception e) {
e.printStackTrace();
}
clazz.getMethod("restoreResources").invoke(mActivity);
} catch (Exception e) {
e.printStackTrace();
}
cb.onResponse(CONNECTION_FAIL);
} }
@Override @Override
@ -103,28 +79,28 @@ public class SafetyNetHelper
new SecureRandom().nextBytes(nonce); new SecureRandom().nextBytes(nonce);
// Call SafetyNet // Call SafetyNet
SafetyNet.SafetyNetApi.attest(mGoogleApiClient, nonce) SafetyNet.SafetyNetApi.attest(mGoogleApiClient, nonce).setResultCallback(this);
.setResultCallback(new ResultCallback<SafetyNetApi.AttestationResult>() { }
@Override
public void onResult(@NonNull SafetyNetApi.AttestationResult result) {
Status status = result.getStatus();
try {
if (!status.isSuccess()) throw new JSONException("");
String json = new String(Base64.decode(
result.getJwsResult().split("\\.")[1], Base64.DEFAULT));
JSONObject decoded = new JSONObject(json);
responseCode |= decoded.getBoolean("ctsProfileMatch") ? CTS_PASS : 0;
responseCode |= decoded.getBoolean("basicIntegrity") ? BASIC_PASS : 0;
} catch (JSONException e) {
responseCode = RESPONSE_ERR;
}
// Disconnect @Override
mGoogleApiClient.disconnect(); public void onResult(SafetyNetApi.AttestationResult result) {
int code = 0;
try {
if (!result.getStatus().isSuccess())
throw new JSONException("");
String jsonStr = new String(Base64.decode(
result.getJwsResult().split("\\.")[1], Base64.DEFAULT));
JSONObject json = new JSONObject(jsonStr);
code |= json.getBoolean("ctsProfileMatch") ? CTS_PASS : 0;
code |= json.getBoolean("basicIntegrity") ? BASIC_PASS : 0;
} catch (JSONException e) {
code = RESPONSE_ERR;
}
// Return results // Disconnect
cb.onResponse(responseCode); mGoogleApiClient.disconnect();
}
}); // Return results
callback.onResponse(code);
} }
} }