Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Verified Commit b33e43c1 authored by Marvin W.'s avatar Marvin W. 🐿️
Browse files

Add initial (non-functional) implementation of SafetyNet

SafetyNet requires DroidGuard for full functionality, see #181
parent 190a0316
Loading
Loading
Loading
Loading
+16 −2
Original line number Diff line number Diff line
@@ -133,7 +133,10 @@
            android:name="org.microg.gms.wearable.location.WearableLocationService">
            <intent-filter>
                <action android:name="com.google.android.gms.wearable.MESSAGE_RECEIVED"/>
                <data android:scheme="wear" android:host="*" android:pathPrefix="/com/google/android/location/fused/wearable" />
                <data
                    android:host="*"
                    android:pathPrefix="/com/google/android/location/fused/wearable"
                    android:scheme="wear"/>
            </intent-filter>
        </service>

@@ -282,6 +285,12 @@
                android:value="1"/>
        </service>

        <service android:name=".auth.FirebaseAuthService">
            <intent-filter>
                <action android:name="com.google.firebase.auth.api.gms.service.START"/>
            </intent-filter>
        </service>

        <activity
            android:name="org.microg.tools.AccountPickerActivity"
            android:excludeFromRecents="true"
@@ -442,6 +451,12 @@
            </intent-filter>
        </service>

        <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.DummyService">
            <intent-filter>
                <action android:name="com.google.android.gms.plus.service.START"/>
@@ -468,7 +483,6 @@
                <action android:name="com.google.android.gms.usagereporting.service.START"/>
                <action android:name="com.google.android.gms.kids.service.START"/>
                <action android:name="com.google.android.gms.common.download.START"/>
                <action android:name="com.google.android.gms.safetynet.service.START"/>
                <action android:name="com.google.android.contextmanager.service.ContextManagerService.START"/>
                <action android:name="com.google.android.gms.audiomodem.service.AudioModemService.START"/>
                <action android:name="com.google.android.gms.nearby.sharing.service.NearbySharingService.START"/>
+37 −0
Original line number Diff line number Diff line
/*
 * 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.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);
    }
}
+220 −0
Original line number Diff line number Diff line
/*
 * 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.os.RemoteException;
import android.util.Base64;
import android.util.Log;

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 com.squareup.wire.Wire;

import org.microg.gms.common.Constants;
import org.microg.gms.common.PackageUtils;
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.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;

    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<ByteString> getPackageSignatures() {
        try {
            PackageInfo pi = context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
            ArrayList<ByteString> 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;
        }
    }

    @Override
    public void attest(final ISafetyNetCallbacks callbacks, final byte[] nonce) throws RemoteException {
        if (nonce == null) {
            callbacks.onAttestationData(new Status(10), null);
            return;
        }

        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.<FileState>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));
                    } catch (IOException e) {
                        Log.w(TAG, e);
                        callbacks.onAttestationData(Status.INTERNAL_ERROR, null);
                    }
                } catch (RemoteException e) {
                    Log.w(TAG, e);
                }
            }
        }).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());
        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 unknown4(ISafetyNetCallbacks callbacks) throws RemoteException {
        Log.d(TAG, "dummy Method: unknown4");
        callbacks.onHarmfulAppsData(Status.SUCCESS, new ArrayList<HarmfulAppsData>());
    }
}
+83 −0
Original line number Diff line number Diff line
// Code generated by Wire protocol buffer compiler, do not edit.
// Source file: protos-repo/snet.proto
package org.microg.gms.snet;

import com.squareup.wire.Message;
import com.squareup.wire.ProtoField;
import okio.ByteString;

import static com.squareup.wire.Message.Datatype.BYTES;
import static com.squareup.wire.Message.Datatype.STRING;

public final class AttestRequest extends Message {

  public static final ByteString DEFAULT_SAFETYNETDATA = ByteString.EMPTY;
  public static final String DEFAULT_DROIDGUARDRESULT = "";

  @ProtoField(tag = 1, type = BYTES)
  public final ByteString safetyNetData;

  @ProtoField(tag = 2, type = STRING)
  public final String droidGuardResult;

  public AttestRequest(ByteString safetyNetData, String droidGuardResult) {
    this.safetyNetData = safetyNetData;
    this.droidGuardResult = droidGuardResult;
  }

  private AttestRequest(Builder builder) {
    this(builder.safetyNetData, builder.droidGuardResult);
    setBuilder(builder);
  }

  @Override
  public boolean equals(Object other) {
    if (other == this) return true;
    if (!(other instanceof AttestRequest)) return false;
    AttestRequest o = (AttestRequest) other;
    return equals(safetyNetData, o.safetyNetData)
        && equals(droidGuardResult, o.droidGuardResult);
  }

  @Override
  public int hashCode() {
    int result = hashCode;
    if (result == 0) {
      result = safetyNetData != null ? safetyNetData.hashCode() : 0;
      result = result * 37 + (droidGuardResult != null ? droidGuardResult.hashCode() : 0);
      hashCode = result;
    }
    return result;
  }

  public static final class Builder extends Message.Builder<AttestRequest> {

    public ByteString safetyNetData;
    public String droidGuardResult;

    public Builder() {
    }

    public Builder(AttestRequest message) {
      super(message);
      if (message == null) return;
      this.safetyNetData = message.safetyNetData;
      this.droidGuardResult = message.droidGuardResult;
    }

    public Builder safetyNetData(ByteString safetyNetData) {
      this.safetyNetData = safetyNetData;
      return this;
    }

    public Builder droidGuardResult(String droidGuardResult) {
      this.droidGuardResult = droidGuardResult;
      return this;
    }

    @Override
    public AttestRequest build() {
      return new AttestRequest(this);
    }
  }
}
+62 −0
Original line number Diff line number Diff line
// Code generated by Wire protocol buffer compiler, do not edit.
// Source file: protos-repo/snet.proto
package org.microg.gms.snet;

import com.squareup.wire.Message;
import com.squareup.wire.ProtoField;

import static com.squareup.wire.Message.Datatype.STRING;

public final class AttestResponse extends Message {

  public static final String DEFAULT_RESULT = "";

  @ProtoField(tag = 2, type = STRING)
  public final String result;

  public AttestResponse(String result) {
    this.result = result;
  }

  private AttestResponse(Builder builder) {
    this(builder.result);
    setBuilder(builder);
  }

  @Override
  public boolean equals(Object other) {
    if (other == this) return true;
    if (!(other instanceof AttestResponse)) return false;
    return equals(result, ((AttestResponse) other).result);
  }

  @Override
  public int hashCode() {
    int result = hashCode;
    return result != 0 ? result : (hashCode = this.result != null ? this.result.hashCode() : 0);
  }

  public static final class Builder extends Message.Builder<AttestResponse> {

    public String result;

    public Builder() {
    }

    public Builder(AttestResponse message) {
      super(message);
      if (message == null) return;
      this.result = message.result;
    }

    public Builder result(String result) {
      this.result = result;
      return this;
    }

    @Override
    public AttestResponse build() {
      return new AttestResponse(this);
    }
  }
}
Loading