Add initial (non-functional) implementation of SafetyNet

SafetyNet requires DroidGuard for full functionality, see #181
This commit is contained in:
Marvin W 2016-09-17 01:40:32 +02:00
parent 190a031662
commit b33e43c1f6
No known key found for this signature in database
GPG Key ID: 072E9235DB996F2A
9 changed files with 802 additions and 2 deletions

View File

@ -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"/>

View File

@ -0,0 +1,37 @@
/*
* 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);
}
}

View File

@ -0,0 +1,220 @@
/*
* 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>());
}
}

View File

@ -0,0 +1,83 @@
// 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);
}
}
}

View File

@ -0,0 +1,62 @@
// 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);
}
}
}

View File

@ -0,0 +1,83 @@
// 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 FileState extends Message {
public static final String DEFAULT_FILENAME = "";
public static final ByteString DEFAULT_DIGEST = ByteString.EMPTY;
@ProtoField(tag = 1, type = STRING)
public final String fileName;
@ProtoField(tag = 2, type = BYTES)
public final ByteString digest;
public FileState(String fileName, ByteString digest) {
this.fileName = fileName;
this.digest = digest;
}
private FileState(Builder builder) {
this(builder.fileName, builder.digest);
setBuilder(builder);
}
@Override
public boolean equals(Object other) {
if (other == this) return true;
if (!(other instanceof FileState)) return false;
FileState o = (FileState) other;
return equals(fileName, o.fileName)
&& equals(digest, o.digest);
}
@Override
public int hashCode() {
int result = hashCode;
if (result == 0) {
result = fileName != null ? fileName.hashCode() : 0;
result = result * 37 + (digest != null ? digest.hashCode() : 0);
hashCode = result;
}
return result;
}
public static final class Builder extends Message.Builder<FileState> {
public String fileName;
public ByteString digest;
public Builder() {
}
public Builder(FileState message) {
super(message);
if (message == null) return;
this.fileName = message.fileName;
this.digest = message.digest;
}
public Builder fileName(String fileName) {
this.fileName = fileName;
return this;
}
public Builder digest(ByteString digest) {
this.digest = digest;
return this;
}
@Override
public FileState build() {
return new FileState(this);
}
}
}

View File

@ -0,0 +1,81 @@
// 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.BOOL;
public final class SELinuxState extends Message {
public static final Boolean DEFAULT_SUPPORTED = false;
public static final Boolean DEFAULT_ENABLED = false;
@ProtoField(tag = 1, type = BOOL)
public final Boolean supported;
@ProtoField(tag = 2, type = BOOL)
public final Boolean enabled;
public SELinuxState(Boolean supported, Boolean enabled) {
this.supported = supported;
this.enabled = enabled;
}
private SELinuxState(Builder builder) {
this(builder.supported, builder.enabled);
setBuilder(builder);
}
@Override
public boolean equals(Object other) {
if (other == this) return true;
if (!(other instanceof SELinuxState)) return false;
SELinuxState o = (SELinuxState) other;
return equals(supported, o.supported)
&& equals(enabled, o.enabled);
}
@Override
public int hashCode() {
int result = hashCode;
if (result == 0) {
result = supported != null ? supported.hashCode() : 0;
result = result * 37 + (enabled != null ? enabled.hashCode() : 0);
hashCode = result;
}
return result;
}
public static final class Builder extends Message.Builder<SELinuxState> {
public Boolean supported;
public Boolean enabled;
public Builder() {
}
public Builder(SELinuxState message) {
super(message);
if (message == null) return;
this.supported = message.supported;
this.enabled = message.enabled;
}
public Builder supported(Boolean supported) {
this.supported = supported;
return this;
}
public Builder enabled(Boolean enabled) {
this.enabled = enabled;
return this;
}
@Override
public SELinuxState build() {
return new SELinuxState(this);
}
}
}

View File

@ -0,0 +1,186 @@
// 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 java.util.Collections;
import java.util.List;
import okio.ByteString;
import static com.squareup.wire.Message.Datatype.BOOL;
import static com.squareup.wire.Message.Datatype.BYTES;
import static com.squareup.wire.Message.Datatype.INT32;
import static com.squareup.wire.Message.Datatype.INT64;
import static com.squareup.wire.Message.Datatype.STRING;
import static com.squareup.wire.Message.Label.REPEATED;
public final class SafetyNetData extends Message {
public static final ByteString DEFAULT_NONCE = ByteString.EMPTY;
public static final String DEFAULT_PACKAGENAME = "";
public static final List<ByteString> DEFAULT_SIGNATUREDIGEST = Collections.emptyList();
public static final ByteString DEFAULT_FILEDIGEST = ByteString.EMPTY;
public static final Integer DEFAULT_GMSVERSIONCODE = 0;
public static final List<FileState> DEFAULT_SUCANDIDATES = Collections.emptyList();
public static final Long DEFAULT_CURRENTTIMEMS = 0L;
public static final Boolean DEFAULT_GOOGLECN = false;
@ProtoField(tag = 1, type = BYTES)
public final ByteString nonce;
@ProtoField(tag = 2, type = STRING)
public final String packageName;
@ProtoField(tag = 3, type = BYTES, label = REPEATED)
public final List<ByteString> signatureDigest;
@ProtoField(tag = 4, type = BYTES)
public final ByteString fileDigest;
@ProtoField(tag = 5, type = INT32)
public final Integer gmsVersionCode;
@ProtoField(tag = 6, label = REPEATED, messageType = FileState.class)
public final List<FileState> suCandidates;
@ProtoField(tag = 7)
public final SELinuxState seLinuxState;
@ProtoField(tag = 8, type = INT64)
public final Long currentTimeMs;
@ProtoField(tag = 9, type = BOOL)
public final Boolean googleCn;
public SafetyNetData(ByteString nonce, String packageName, List<ByteString> signatureDigest, ByteString fileDigest, Integer gmsVersionCode, List<FileState> suCandidates, SELinuxState seLinuxState, Long currentTimeMs, Boolean googleCn) {
this.nonce = nonce;
this.packageName = packageName;
this.signatureDigest = immutableCopyOf(signatureDigest);
this.fileDigest = fileDigest;
this.gmsVersionCode = gmsVersionCode;
this.suCandidates = immutableCopyOf(suCandidates);
this.seLinuxState = seLinuxState;
this.currentTimeMs = currentTimeMs;
this.googleCn = googleCn;
}
private SafetyNetData(Builder builder) {
this(builder.nonce, builder.packageName, builder.signatureDigest, builder.fileDigest, builder.gmsVersionCode, builder.suCandidates, builder.seLinuxState, builder.currentTimeMs, builder.googleCn);
setBuilder(builder);
}
@Override
public boolean equals(Object other) {
if (other == this) return true;
if (!(other instanceof SafetyNetData)) return false;
SafetyNetData o = (SafetyNetData) other;
return equals(nonce, o.nonce)
&& equals(packageName, o.packageName)
&& equals(signatureDigest, o.signatureDigest)
&& equals(fileDigest, o.fileDigest)
&& equals(gmsVersionCode, o.gmsVersionCode)
&& equals(suCandidates, o.suCandidates)
&& equals(seLinuxState, o.seLinuxState)
&& equals(currentTimeMs, o.currentTimeMs)
&& equals(googleCn, o.googleCn);
}
@Override
public int hashCode() {
int result = hashCode;
if (result == 0) {
result = nonce != null ? nonce.hashCode() : 0;
result = result * 37 + (packageName != null ? packageName.hashCode() : 0);
result = result * 37 + (signatureDigest != null ? signatureDigest.hashCode() : 1);
result = result * 37 + (fileDigest != null ? fileDigest.hashCode() : 0);
result = result * 37 + (gmsVersionCode != null ? gmsVersionCode.hashCode() : 0);
result = result * 37 + (suCandidates != null ? suCandidates.hashCode() : 1);
result = result * 37 + (seLinuxState != null ? seLinuxState.hashCode() : 0);
result = result * 37 + (currentTimeMs != null ? currentTimeMs.hashCode() : 0);
result = result * 37 + (googleCn != null ? googleCn.hashCode() : 0);
hashCode = result;
}
return result;
}
public static final class Builder extends Message.Builder<SafetyNetData> {
public ByteString nonce;
public String packageName;
public List<ByteString> signatureDigest;
public ByteString fileDigest;
public Integer gmsVersionCode;
public List<FileState> suCandidates;
public SELinuxState seLinuxState;
public Long currentTimeMs;
public Boolean googleCn;
public Builder() {
}
public Builder(SafetyNetData message) {
super(message);
if (message == null) return;
this.nonce = message.nonce;
this.packageName = message.packageName;
this.signatureDigest = copyOf(message.signatureDigest);
this.fileDigest = message.fileDigest;
this.gmsVersionCode = message.gmsVersionCode;
this.suCandidates = copyOf(message.suCandidates);
this.seLinuxState = message.seLinuxState;
this.currentTimeMs = message.currentTimeMs;
this.googleCn = message.googleCn;
}
public Builder nonce(ByteString nonce) {
this.nonce = nonce;
return this;
}
public Builder packageName(String packageName) {
this.packageName = packageName;
return this;
}
public Builder signatureDigest(List<ByteString> signatureDigest) {
this.signatureDigest = checkForNulls(signatureDigest);
return this;
}
public Builder fileDigest(ByteString fileDigest) {
this.fileDigest = fileDigest;
return this;
}
public Builder gmsVersionCode(Integer gmsVersionCode) {
this.gmsVersionCode = gmsVersionCode;
return this;
}
public Builder suCandidates(List<FileState> suCandidates) {
this.suCandidates = checkForNulls(suCandidates);
return this;
}
public Builder seLinuxState(SELinuxState seLinuxState) {
this.seLinuxState = seLinuxState;
return this;
}
public Builder currentTimeMs(Long currentTimeMs) {
this.currentTimeMs = currentTimeMs;
return this;
}
public Builder googleCn(Boolean googleCn) {
this.googleCn = googleCn;
return this;
}
@Override
public SafetyNetData build() {
return new SafetyNetData(this);
}
}
}

View File

@ -0,0 +1,34 @@
option java_package = "org.microg.gms.snet";
option java_outer_classname = "SafetyNetProto";
message SELinuxState {
optional bool supported = 1;
optional bool enabled = 2;
}
message FileState {
optional string fileName = 1;
optional bytes digest = 2;
}
message SafetyNetData {
optional bytes nonce = 1;
optional string packageName = 2;
repeated bytes signatureDigest = 3;
optional bytes fileDigest = 4;
optional int32 gmsVersionCode = 5;
repeated FileState suCandidates = 6;
optional SELinuxState seLinuxState = 7;
optional int64 currentTimeMs = 8;
optional bool googleCn = 9;
}
message AttestRequest {
optional bytes safetyNetData = 1;
optional string droidGuardResult = 2;
}
message AttestResponse {
optional string result = 2;
}