From 40835c36180cbac1394a8a48685404a93131a743 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Sat, 24 Sep 2016 21:19:26 +0200 Subject: [PATCH] Add initial support for SafetyNet, requiring DroidGuard Helper to be installed --- .gitmodules | 3 + extern/RemoteDroidGuard | 1 + play-services-core/build.gradle | 4 +- .../microg/gms/checkin/CheckinManager.java | 7 +- .../java/org/microg/gms/common/PhoneInfo.java | 12 ++ .../java/org/microg/gms/common/Utils.java | 9 +- .../gms/droidguard/DroidGuardService.java | 3 + .../java/org/microg/gms/snet/Attestation.java | 199 ++++++++++++++++++ .../gms/snet/SafetyNetClientServiceImpl.java | 141 ++----------- remote-droid-guard-lib | 1 + settings.gradle | 2 + 11 files changed, 252 insertions(+), 130 deletions(-) create mode 160000 extern/RemoteDroidGuard create mode 100644 play-services-core/src/main/java/org/microg/gms/snet/Attestation.java create mode 120000 remote-droid-guard-lib diff --git a/.gitmodules b/.gitmodules index c1af66f8..bf2f0190 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "extern/vtm"] path = extern/vtm url = https://github.com/microg/android_external_vtm.git +[submodule "extern/RemoteDroidGuard"] + path = extern/RemoteDroidGuard + url = https://github.com/microg/android_packages_apps_RemoteDroidGuard.git diff --git a/extern/RemoteDroidGuard b/extern/RemoteDroidGuard new file mode 160000 index 00000000..99b7d048 --- /dev/null +++ b/extern/RemoteDroidGuard @@ -0,0 +1 @@ +Subproject commit 99b7d04824112355ff820adbee1d35624c22f453 diff --git a/play-services-core/build.gradle b/play-services-core/build.gradle index e86e401a..7031713e 100644 --- a/play-services-core/build.gradle +++ b/play-services-core/build.gradle @@ -20,12 +20,14 @@ dependencies { compile 'de.hdodenhof:circleimageview:1.2.1' compile 'com.squareup.wire:wire-runtime:1.6.1' - compile project(":microg-ui-tools") + compile project(':microg-ui-tools') compile project(':play-services-api') compile project(':play-services-wearable') compile project(':unifiednlp-base') compile project(':wearable-lib') + compile project(':remote-droid-guard-lib') + compile project(':vtm-android') compile project(':vtm-extras') compile project(':vtm-jts') diff --git a/play-services-core/src/main/java/org/microg/gms/checkin/CheckinManager.java b/play-services-core/src/main/java/org/microg/gms/checkin/CheckinManager.java index 6670f774..1076d211 100644 --- a/play-services-core/src/main/java/org/microg/gms/checkin/CheckinManager.java +++ b/play-services-core/src/main/java/org/microg/gms/checkin/CheckinManager.java @@ -23,12 +23,9 @@ import android.content.Context; import com.google.android.gms.R; -import org.microg.gms.auth.AuthManager; import org.microg.gms.auth.AuthRequest; import org.microg.gms.common.Constants; import org.microg.gms.common.DeviceConfiguration; -import org.microg.gms.common.DeviceIdentifier; -import org.microg.gms.common.PhoneInfo; import org.microg.gms.common.Utils; import org.microg.gms.gservices.GServices; @@ -57,8 +54,8 @@ public class CheckinManager { } } CheckinRequest request = CheckinClient.makeRequest(Utils.getBuild(context), - new DeviceConfiguration(context), new DeviceIdentifier(), new PhoneInfo(), info, - Utils.getLocale(context), accounts); // TODO + new DeviceConfiguration(context), Utils.getDeviceIdentifier(context), + Utils.getPhoneInfo(context), info, Utils.getLocale(context), accounts); return handleResponse(context, CheckinClient.request(request)); } diff --git a/play-services-core/src/main/java/org/microg/gms/common/PhoneInfo.java b/play-services-core/src/main/java/org/microg/gms/common/PhoneInfo.java index 2eb527ce..32ed2bbb 100644 --- a/play-services-core/src/main/java/org/microg/gms/common/PhoneInfo.java +++ b/play-services-core/src/main/java/org/microg/gms/common/PhoneInfo.java @@ -16,8 +16,20 @@ package org.microg.gms.common; +import java.util.Random; + public class PhoneInfo { public String cellOperator = "26207"; public String roaming = "mobile-notroaming"; public String simOperator = "26207"; + public String imsi = randomImsi(); + + private String randomImsi() { + Random random = new Random(); + StringBuilder sb = new StringBuilder(simOperator); + while (sb.length() < 15) { + sb.append(random.nextInt(10)); + } + return sb.toString(); + } } diff --git a/play-services-core/src/main/java/org/microg/gms/common/Utils.java b/play-services-core/src/main/java/org/microg/gms/common/Utils.java index ad2b82c7..f180fff8 100644 --- a/play-services-core/src/main/java/org/microg/gms/common/Utils.java +++ b/play-services-core/src/main/java/org/microg/gms/common/Utils.java @@ -16,7 +16,6 @@ package org.microg.gms.common; -import android.Manifest; import android.content.Context; import android.support.v4.content.ContextCompat; import android.widget.Toast; @@ -45,6 +44,14 @@ public class Utils { return new Build(); } + public static DeviceIdentifier getDeviceIdentifier(Context context) { + return new DeviceIdentifier(); + } + + public static PhoneInfo getPhoneInfo(Context context) { + return new PhoneInfo(); + } + public static boolean hasSelfPermissionOrNotify(Context context, String permission) { if (ContextCompat.checkSelfPermission(context, permission) != PERMISSION_GRANTED) { Toast.makeText(context, context.getString(R.string.lacking_permission_toast, permission), Toast.LENGTH_SHORT).show(); diff --git a/play-services-core/src/main/java/org/microg/gms/droidguard/DroidGuardService.java b/play-services-core/src/main/java/org/microg/gms/droidguard/DroidGuardService.java index 585c3549..3e6a5023 100644 --- a/play-services-core/src/main/java/org/microg/gms/droidguard/DroidGuardService.java +++ b/play-services-core/src/main/java/org/microg/gms/droidguard/DroidGuardService.java @@ -16,6 +16,8 @@ package org.microg.gms.droidguard; +import android.util.Log; + import com.google.android.gms.common.internal.GetServiceRequest; import com.google.android.gms.common.internal.IGmsCallbacks; @@ -31,5 +33,6 @@ public class DroidGuardService extends BaseService { @Override public void handleServiceRequest(IGmsCallbacks callback, GetServiceRequest request, GmsService service) { // TODO + Log.d(TAG, "handleServiceRequest"); } } diff --git a/play-services-core/src/main/java/org/microg/gms/snet/Attestation.java b/play-services-core/src/main/java/org/microg/gms/snet/Attestation.java new file mode 100644 index 00000000..73e63b54 --- /dev/null +++ b/play-services-core/src/main/java/org/microg/gms/snet/Attestation.java @@ -0,0 +1,199 @@ +/* + * Copyright 2013-2016 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.annotation.SuppressLint; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +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.Utils; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.zip.GZIPInputStream; + +import okio.ByteString; + +public class Attestation { + private static final String TAG = "GmsSafetyNetAttest"; + private static final String ATTEST_URL = "https://www.googleapis.com/androidcheck/v1/attestations/attest?alt=PROTO&key=AIzaSyDqVnJBjE5ymo--oBJt3On7HQx9xNm1RHA"; + + private Context context; + private String packageName; + private byte[] payload; + private String droidGaurdResult; + + public Attestation(Context context, String packageName) { + this.context = context; + this.packageName = packageName; + } + + public void setPayload(byte[] payload) { + this.payload = payload; + } + + public byte[] buildPayload(byte[] nonce) { + this.droidGaurdResult = null; + SafetyNetData payload = new SafetyNetData.Builder() + .nonce(ByteString.of(nonce)) + .currentTimeMs(System.currentTimeMillis()) + .packageName(packageName) + .fileDigest(getPackageFileDigest()) + .signatureDigest(getPackageSignatures()) + .gmsVersionCode(Constants.MAX_REFERENCE_VERSION) + //.googleCn(false) + .seLinuxState(new SELinuxState(true, true)) + .suCandidates(Collections.emptyList()) + .build(); + return this.payload = payload.toByteArray(); + } + + public byte[] getPayload() { + return payload; + } + + public String getPayloadHashBase64() { + try { + MessageDigest digest = getSha256Digest(); + return Base64.encodeToString(digest.digest(payload), Base64.NO_WRAP); + } catch (NoSuchAlgorithmException e) { + Log.w(TAG, e); + return null; + } + } + + private static MessageDigest getSha256Digest() throws NoSuchAlgorithmException { + return MessageDigest.getInstance("SHA-256"); + } + + public void setDroidGaurdResult(String droidGaurdResult) { + this.droidGaurdResult = droidGaurdResult; + } + + 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()); + } catch (Exception e) { + Log.w(TAG, e); + return null; + } + } + + @SuppressLint("PackageManagerGetSignatures") + private List getPackageSignatures() { + try { + PackageInfo pi = context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES); + ArrayList res = new ArrayList<>(); + MessageDigest digest = getSha256Digest(); + for (Signature signature : pi.signatures) { + res.add(ByteString.of(digest.digest(signature.toByteArray()))); + } + return res; + } catch (Exception e) { + Log.w(TAG, e); + return null; + } + } + + public String attest() throws IOException { + if (payload == null) { + throw new IllegalStateException("missing payload"); + } + if (droidGaurdResult == null || droidGaurdResult.isEmpty()) { + throw new IllegalStateException("missing droidGuard"); + } + return attest(new AttestRequest(ByteString.of(payload), droidGaurdResult)).result; + } + + private AttestResponse attest(AttestRequest request) throws IOException { + HttpURLConnection connection = (HttpURLConnection) new URL(ATTEST_URL).openConnection(); + connection.setRequestMethod("POST"); + connection.setDoInput(true); + connection.setDoOutput(true); + connection.setRequestProperty("content-type", "application/x-protobuf"); + connection.setRequestProperty("Accept-Encoding", "gzip"); + Build build = Utils.getBuild(context); + connection.setRequestProperty("User-Agent", "SafetyNet/" + Constants.MAX_REFERENCE_VERSION + " (" + build.device + " " + build.id + "); gzip"); + + OutputStream os = connection.getOutputStream(); + os.write(request.toByteArray()); + os.close(); + + if (connection.getResponseCode() != 200) { + byte[] bytes = null; + String ex = null; + try { + bytes = Utils.readStreamToEnd(connection.getErrorStream()); + ex = new String(Utils.readStreamToEnd(new GZIPInputStream(new ByteArrayInputStream(bytes)))); + } catch (Exception e) { + if (bytes != null) { + throw new IOException(getBytesAsString(bytes), e); + } + throw new IOException(connection.getResponseMessage(), e); + } + throw new IOException(ex); + } + + InputStream is = connection.getInputStream(); + AttestResponse response = new Wire().parseFrom(new GZIPInputStream(is), AttestResponse.class); + is.close(); + return response; + } + + + private String getBytesAsString(byte[] bytes) { + if (bytes == null) return "null"; + try { + CharsetDecoder d = Charset.forName("US-ASCII").newDecoder(); + CharBuffer r = d.decode(ByteBuffer.wrap(bytes)); + return r.toString(); + } catch (Exception e) { + return Base64.encodeToString(bytes, Base64.NO_WRAP); + } + } +} diff --git a/play-services-core/src/main/java/org/microg/gms/snet/SafetyNetClientServiceImpl.java b/play-services-core/src/main/java/org/microg/gms/snet/SafetyNetClientServiceImpl.java index 6ae7b7bb..e5ed62e1 100644 --- a/play-services-core/src/main/java/org/microg/gms/snet/SafetyNetClientServiceImpl.java +++ b/play-services-core/src/main/java/org/microg/gms/snet/SafetyNetClientServiceImpl.java @@ -16,11 +16,8 @@ package org.microg.gms.snet; -import android.annotation.SuppressLint; import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.Signature; +import android.os.Bundle; import android.os.RemoteException; import android.util.Base64; import android.util.Log; @@ -30,76 +27,26 @@ 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 com.squareup.wire.Wire; -import org.microg.gms.common.Constants; +import org.microg.gms.checkin.LastCheckinInfo; import org.microg.gms.common.PackageUtils; import org.microg.gms.common.Utils; +import org.microg.gms.droidguard.RemoteDroidGuardConnector; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.Charset; -import java.nio.charset.CharsetDecoder; -import java.security.MessageDigest; import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; - -import okio.ByteString; public class SafetyNetClientServiceImpl extends ISafetyNetService.Stub { private static final String TAG = "GmsSafetyNetClientImpl"; - public static final String ATTEST_URL = "https://www.googleapis.com/androidcheck/v1/attestations/attest?alt=PROTO&key=AIzaSyDqVnJBjE5ymo--oBJt3On7HQx9xNm1RHA"; private Context context; private String packageName; + private Attestation attestation; public SafetyNetClientServiceImpl(Context context, String packageName) { this.context = context; this.packageName = packageName; - } - - private ByteString getPackageFileDigest() { - try { - FileInputStream is = new FileInputStream(new File(context.getPackageManager().getApplicationInfo(packageName, 0).sourceDir)); - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - 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()); - } catch (Exception e) { - Log.w(TAG, e); - return null; - } - } - - @SuppressLint("PackageManagerGetSignatures") - private List getPackageSignatures() { - try { - PackageInfo pi = context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES); - ArrayList res = new ArrayList<>(); - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - for (Signature signature : pi.signatures) { - res.add(ByteString.of(digest.digest(signature.toByteArray()))); - } - return res; - } catch (Exception e) { - Log.w(TAG, e); - return null; - } + this.attestation = new Attestation(context, packageName); } @Override @@ -112,26 +59,21 @@ public class SafetyNetClientServiceImpl extends ISafetyNetService.Stub { new Thread(new Runnable() { @Override public void run() { - SafetyNetData payload = new SafetyNetData.Builder() - .nonce(ByteString.of(nonce)) - .currentTimeMs(System.currentTimeMillis()) - .packageName(packageName) - .fileDigest(getPackageFileDigest()) - .signatureDigest(getPackageSignatures()) - .gmsVersionCode(Constants.MAX_REFERENCE_VERSION) - .googleCn(false) - .seLinuxState(new SELinuxState(true, true)) - .suCandidates(Collections.emptyList()) - .build(); - - AttestRequest request = new AttestRequest(ByteString.of(payload.toByteArray()), ""); - - Log.d(TAG, "attest: " + payload); - try { try { - AttestResponse response = attest(request); - callbacks.onAttestationData(Status.SUCCESS, new AttestationData(response.result)); + 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, Utils.getDeviceIdentifier(context).meid, Utils.getPhoneInfo(context).imsi); + 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()); + 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); @@ -143,53 +85,6 @@ public class SafetyNetClientServiceImpl extends ISafetyNetService.Stub { }).start(); } - private AttestResponse attest(AttestRequest request) throws IOException { - HttpURLConnection connection = (HttpURLConnection) new URL(ATTEST_URL).openConnection(); - connection.setRequestMethod("POST"); - connection.setDoInput(true); - connection.setDoOutput(true); - connection.setRequestProperty("Content-type", "application/x-protobuf"); - connection.setRequestProperty("Content-Encoding", "gzip"); - connection.setRequestProperty("Accept-Encoding", "gzip"); - connection.setRequestProperty("User-Agent", "SafetyNet/" + Constants.MAX_REFERENCE_VERSION); - - Log.d(TAG, "-- Request --\n" + request); - OutputStream os = new GZIPOutputStream(connection.getOutputStream()); - os.write(request.toByteArray()); - os.close(); - - if (connection.getResponseCode() != 200) { - byte[] bytes = null; - String ex = null; - try { - bytes = Utils.readStreamToEnd(connection.getErrorStream()); - ex = new String(Utils.readStreamToEnd(new GZIPInputStream(new ByteArrayInputStream(bytes)))); - } catch (Exception e) { - if (bytes != null) { - throw new IOException(getBytesAsString(bytes), e); - } - throw new IOException(connection.getResponseMessage(), e); - } - throw new IOException(ex); - } - - InputStream is = connection.getInputStream(); - AttestResponse response = new Wire().parseFrom(new GZIPInputStream(is), AttestResponse.class); - is.close(); - return response; - } - - private String getBytesAsString(byte[] bytes) { - if (bytes == null) return "null"; - try { - CharsetDecoder d = Charset.forName("US-ASCII").newDecoder(); - CharBuffer r = d.decode(ByteBuffer.wrap(bytes)); - return r.toString(); - } catch (Exception e) { - return Base64.encodeToString(bytes, Base64.NO_WRAP); - } - } - @Override public void getSharedUuid(ISafetyNetCallbacks callbacks) throws RemoteException { PackageUtils.checkPackageUid(context, packageName, getCallingUid()); diff --git a/remote-droid-guard-lib b/remote-droid-guard-lib new file mode 120000 index 00000000..8bb78493 --- /dev/null +++ b/remote-droid-guard-lib @@ -0,0 +1 @@ +extern/RemoteDroidGuard/remote-droid-guard-lib \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index dc7876de..6cf00ed6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -27,3 +27,5 @@ include ':vtm-android' include ':vtm-extras' include ':vtm-jts' include ':vtm-themes' + +include ':remote-droid-guard-lib'