mirror of https://github.com/YTVanced/VancedMicroG
424 lines
17 KiB
Java
424 lines
17 KiB
Java
/*
|
|
* 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.iid;
|
|
|
|
import android.app.PendingIntent;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.pm.ApplicationInfo;
|
|
import android.content.pm.PackageManager;
|
|
import android.content.pm.ResolveInfo;
|
|
import android.os.Build;
|
|
import android.os.Bundle;
|
|
import android.os.ConditionVariable;
|
|
import android.os.Handler;
|
|
import android.os.Looper;
|
|
import android.os.Message;
|
|
import android.os.Messenger;
|
|
import android.os.Parcelable;
|
|
import android.os.RemoteException;
|
|
import android.os.SystemClock;
|
|
import android.text.TextUtils;
|
|
import android.util.Base64;
|
|
import android.util.Log;
|
|
|
|
import com.google.android.gms.iid.InstanceID;
|
|
import com.google.android.gms.iid.MessengerCompat;
|
|
|
|
import java.io.IOException;
|
|
import java.io.UnsupportedEncodingException;
|
|
import java.security.GeneralSecurityException;
|
|
import java.security.KeyPair;
|
|
import java.security.PrivateKey;
|
|
import java.security.Signature;
|
|
import java.security.interfaces.RSAPrivateKey;
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
import java.util.Random;
|
|
|
|
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
|
import static com.google.android.gms.iid.InstanceID.ERROR_BACKOFF;
|
|
import static com.google.android.gms.iid.InstanceID.ERROR_MISSING_INSTANCEID_SERVICE;
|
|
import static com.google.android.gms.iid.InstanceID.ERROR_SERVICE_NOT_AVAILABLE;
|
|
import static com.google.android.gms.iid.InstanceID.ERROR_TIMEOUT;
|
|
import static org.microg.gms.common.Constants.GMS_PACKAGE_NAME;
|
|
import static org.microg.gms.common.Constants.GSF_PACKAGE_NAME;
|
|
import static org.microg.gms.common.Constants.GMS_VERSION_CODE;
|
|
import static org.microg.gms.gcm.GcmConstants.ACTION_C2DM_REGISTER;
|
|
import static org.microg.gms.gcm.GcmConstants.ACTION_C2DM_REGISTRATION;
|
|
import static org.microg.gms.gcm.GcmConstants.ACTION_INSTANCE_ID;
|
|
import static org.microg.gms.gcm.GcmConstants.EXTRA_APP;
|
|
import static org.microg.gms.gcm.GcmConstants.EXTRA_APP_ID;
|
|
import static org.microg.gms.gcm.GcmConstants.EXTRA_APP_VERSION_CODE;
|
|
import static org.microg.gms.gcm.GcmConstants.EXTRA_APP_VERSION_NAME;
|
|
import static org.microg.gms.gcm.GcmConstants.EXTRA_CLIENT_VERSION;
|
|
import static org.microg.gms.gcm.GcmConstants.EXTRA_ERROR;
|
|
import static org.microg.gms.gcm.GcmConstants.EXTRA_GMS_VERSION;
|
|
import static org.microg.gms.gcm.GcmConstants.EXTRA_GSF_INTENT;
|
|
import static org.microg.gms.gcm.GcmConstants.EXTRA_IS_MESSENGER2;
|
|
import static org.microg.gms.gcm.GcmConstants.EXTRA_KID;
|
|
import static org.microg.gms.gcm.GcmConstants.EXTRA_MESSENGER;
|
|
import static org.microg.gms.gcm.GcmConstants.EXTRA_OS_VERSION;
|
|
import static org.microg.gms.gcm.GcmConstants.EXTRA_PUBLIC_KEY;
|
|
import static org.microg.gms.gcm.GcmConstants.EXTRA_REGISTRATION_ID;
|
|
import static org.microg.gms.gcm.GcmConstants.EXTRA_SIGNATURE;
|
|
import static org.microg.gms.gcm.GcmConstants.EXTRA_UNREGISTERED;
|
|
import static org.microg.gms.gcm.GcmConstants.EXTRA_USE_GSF;
|
|
import static org.microg.gms.gcm.GcmConstants.PERMISSION_RECEIVE;
|
|
|
|
public class InstanceIdRpc {
|
|
private static final String TAG = "InstanceID/Rpc";
|
|
|
|
private static final int BLOCKING_WAIT_TIME = 30000;
|
|
|
|
private static String iidPackageName;
|
|
private static int lastRequestId;
|
|
private static int retryCount;
|
|
private static Map<String, Object> blockingResponses = new HashMap<String, Object>();
|
|
|
|
private long nextAttempt;
|
|
private int interval;
|
|
private Context context;
|
|
private PendingIntent selfAuthToken;
|
|
private Messenger messenger;
|
|
private Messenger myMessenger;
|
|
private MessengerCompat messengerCompat;
|
|
|
|
public InstanceIdRpc(Context context) {
|
|
this.context = context;
|
|
}
|
|
|
|
public static String getIidPackageName(Context context) {
|
|
if (iidPackageName != null) {
|
|
return iidPackageName;
|
|
}
|
|
PackageManager packageManager = context.getPackageManager();
|
|
for (ResolveInfo resolveInfo : packageManager.queryIntentServices(new Intent(ACTION_C2DM_REGISTER), 0)) {
|
|
if (packageManager.checkPermission(PERMISSION_RECEIVE, resolveInfo.serviceInfo.packageName) == PERMISSION_GRANTED) {
|
|
return iidPackageName = resolveInfo.serviceInfo.packageName;
|
|
}
|
|
}
|
|
try {
|
|
ApplicationInfo appInfo = packageManager.getApplicationInfo(GMS_PACKAGE_NAME, 0);
|
|
return iidPackageName = appInfo.packageName;
|
|
} catch (PackageManager.NameNotFoundException ignored) {
|
|
}
|
|
try {
|
|
ApplicationInfo appInfo = packageManager.getApplicationInfo(GSF_PACKAGE_NAME, 0);
|
|
return iidPackageName = appInfo.packageName;
|
|
} catch (PackageManager.NameNotFoundException ex3) {
|
|
Log.w(TAG, "Both Google Play Services and legacy GSF package are missing");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static int getGmsVersionCode(final Context context) {
|
|
final PackageManager packageManager = context.getPackageManager();
|
|
try {
|
|
return packageManager.getPackageInfo(getIidPackageName(context), 0).versionCode;
|
|
} catch (PackageManager.NameNotFoundException ex) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
private static int getSelfVersionCode(final Context context) {
|
|
try {
|
|
return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode;
|
|
} catch (PackageManager.NameNotFoundException neverHappens) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
private static String getSelfVersionName(final Context context) {
|
|
try {
|
|
return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName;
|
|
} catch (PackageManager.NameNotFoundException neverHappens) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
void initialize() {
|
|
if (myMessenger != null) return;
|
|
getIidPackageName(context);
|
|
myMessenger = new Messenger(new Handler(Looper.getMainLooper()) {
|
|
@Override
|
|
public void handleMessage(Message msg) {
|
|
if (msg == null) {
|
|
return;
|
|
}
|
|
if (msg.obj instanceof Intent) {
|
|
Intent intent = (Intent) msg.obj;
|
|
intent.setExtrasClassLoader(MessengerCompat.class.getClassLoader());
|
|
if (intent.hasExtra(EXTRA_MESSENGER)) {
|
|
Parcelable messengerCandidate = intent.getParcelableExtra(EXTRA_MESSENGER);
|
|
if (messengerCandidate instanceof MessengerCompat) {
|
|
messengerCompat = (MessengerCompat) messengerCandidate;
|
|
} else if (messengerCandidate instanceof Messenger) {
|
|
messenger = (Messenger) messengerCandidate;
|
|
}
|
|
}
|
|
handleResponseInternal(intent);
|
|
} else {
|
|
Log.w(TAG, "Dropping invalid message");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
public void handleResponseInternal(Intent resultIntent) {
|
|
if (resultIntent == null) return;
|
|
if (!ACTION_C2DM_REGISTRATION.equals(resultIntent.getAction()) && !ACTION_INSTANCE_ID.equals(resultIntent.getAction()))
|
|
return;
|
|
String result = resultIntent.getStringExtra(EXTRA_REGISTRATION_ID);
|
|
if (result == null) result = resultIntent.getStringExtra(EXTRA_UNREGISTERED);
|
|
if (result == null) {
|
|
handleError(resultIntent);
|
|
return;
|
|
}
|
|
retryCount = 0;
|
|
nextAttempt = 0;
|
|
interval = 0;
|
|
|
|
String requestId = null;
|
|
if (result.startsWith("|")) {
|
|
// parse structured response
|
|
String[] split = result.split("\\|");
|
|
if (!"ID".equals(split[1])) {
|
|
Log.w(TAG, "Unexpected structured response " + result);
|
|
}
|
|
requestId = split[2];
|
|
if (split.length > 4) {
|
|
if ("SYNC".equals(split[3])) {
|
|
// TODO: sync
|
|
} else if("RST".equals(split[3])) {
|
|
// TODO: rst
|
|
resultIntent.removeExtra(EXTRA_REGISTRATION_ID);
|
|
return;
|
|
}
|
|
}
|
|
result = split[split.length-1];
|
|
if (result.startsWith(":"))
|
|
result = result.substring(1);
|
|
resultIntent.putExtra(EXTRA_REGISTRATION_ID, result);
|
|
}
|
|
setResponse(requestId, resultIntent);
|
|
}
|
|
|
|
private void handleError(Intent resultIntent) {
|
|
String error = resultIntent.getStringExtra("error");
|
|
if (error == null) return;
|
|
String requestId = null;
|
|
if (error.startsWith("|")) {
|
|
// parse structured error message
|
|
String[] split = error.split("\\|");
|
|
if (!"ID".equals(split[1])) {
|
|
Log.w(TAG, "Unexpected structured response " + error);
|
|
}
|
|
if (split.length > 2) {
|
|
requestId = split[2];
|
|
error = split[3];
|
|
if (error.startsWith(":"))
|
|
error = error.substring(1);
|
|
} else {
|
|
error = "UNKNOWN";
|
|
}
|
|
resultIntent.putExtra("error", error);
|
|
}
|
|
setResponse(requestId, resultIntent);
|
|
long retryAfter = resultIntent.getLongExtra("Retry-After", 0);
|
|
if (retryAfter > 0) {
|
|
interval = (int) (retryAfter * 1000);
|
|
nextAttempt = SystemClock.elapsedRealtime() + interval;
|
|
Log.d(TAG, "Server requested retry delay: " + interval);
|
|
} else if (ERROR_SERVICE_NOT_AVAILABLE.equals(error) || "AUTHENTICATION_FAILED".equals(error)
|
|
&& GSF_PACKAGE_NAME.equals(getIidPackageName(context))) {
|
|
retryCount++;
|
|
if (retryCount < 3) return;
|
|
if (retryCount == 3) interval = 1000 + new Random().nextInt(1000);
|
|
interval = interval * 2;
|
|
nextAttempt = SystemClock.elapsedRealtime() + interval;
|
|
Log.d(TAG, "Setting retry delay to " + interval);
|
|
}
|
|
}
|
|
|
|
private synchronized PendingIntent getSelfAuthToken() {
|
|
if (selfAuthToken == null) {
|
|
Intent intent = new Intent();
|
|
intent.setPackage("com.google.example.invalidpackage");
|
|
selfAuthToken = PendingIntent.getBroadcast(context, 0, intent, 0);
|
|
}
|
|
return selfAuthToken;
|
|
}
|
|
|
|
private static synchronized String getRequestId() {
|
|
return Integer.toString(lastRequestId++);
|
|
}
|
|
|
|
private void sendRegisterMessage(Bundle data, KeyPair keyPair, String requestId) throws IOException {
|
|
long elapsedRealtime = SystemClock.elapsedRealtime();
|
|
if (nextAttempt != 0 && elapsedRealtime <= nextAttempt) {
|
|
Log.w(TAG, "Had to wait for " + interval + ", that's still " + (nextAttempt - elapsedRealtime));
|
|
throw new IOException(ERROR_BACKOFF);
|
|
}
|
|
initialize();
|
|
if (iidPackageName == null) {
|
|
throw new IOException(ERROR_MISSING_INSTANCEID_SERVICE);
|
|
}
|
|
Intent intent = new Intent(ACTION_C2DM_REGISTER);
|
|
intent.setPackage(iidPackageName);
|
|
data.putString(EXTRA_GMS_VERSION, Integer.toString(getGmsVersionCode(context)));
|
|
data.putString(EXTRA_OS_VERSION, Integer.toString(Build.VERSION.SDK_INT));
|
|
data.putString(EXTRA_APP_VERSION_CODE, Integer.toString(getSelfVersionCode(context)));
|
|
data.putString(EXTRA_APP_VERSION_NAME, getSelfVersionName(context));
|
|
data.putString(EXTRA_CLIENT_VERSION, "iid-" + GMS_VERSION_CODE);
|
|
data.putString(EXTRA_APP_ID, InstanceID.sha1KeyPair(keyPair));
|
|
String pub = base64encode(keyPair.getPublic().getEncoded());
|
|
data.putString(EXTRA_PUBLIC_KEY, pub);
|
|
data.putString(EXTRA_SIGNATURE, sign(keyPair, context.getPackageName(), pub));
|
|
intent.putExtras(data);
|
|
intent.putExtra(EXTRA_APP, getSelfAuthToken());
|
|
sendRequest(intent, requestId);
|
|
}
|
|
|
|
private static String sign(KeyPair keyPair, String... payload) {
|
|
byte[] bytes;
|
|
try {
|
|
bytes = TextUtils.join("\n", payload).getBytes("UTF-8");
|
|
} catch (UnsupportedEncodingException e) {
|
|
Log.e(TAG, "Unable to encode", e);
|
|
return null;
|
|
}
|
|
PrivateKey privateKey = keyPair.getPrivate();
|
|
try {
|
|
Signature signature = Signature.getInstance(privateKey instanceof RSAPrivateKey ? "SHA256withRSA" : "SHA256withECDSA");
|
|
signature.initSign(privateKey);
|
|
signature.update(bytes);
|
|
return base64encode(signature.sign());
|
|
} catch (GeneralSecurityException e) {
|
|
Log.e(TAG, "Unable to sign", e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static String base64encode(byte[] bytes) {
|
|
return Base64.encodeToString(bytes, Base64.URL_SAFE + Base64.NO_PADDING + Base64.NO_WRAP);
|
|
}
|
|
|
|
private void sendRequest(Intent intent, String requestId) {
|
|
intent.putExtra(EXTRA_KID, "|ID|" + requestId + "|");
|
|
intent.putExtra("X-" + EXTRA_KID, "|ID|" + requestId + "|");
|
|
Log.d(TAG, "Sending " + intent.getExtras());
|
|
if (messenger != null) {
|
|
intent.putExtra(EXTRA_MESSENGER, myMessenger);
|
|
Message msg = Message.obtain();
|
|
msg.obj = intent;
|
|
try {
|
|
messenger.send(msg);
|
|
return;
|
|
} catch (RemoteException e) {
|
|
Log.d(TAG, "Messenger failed, falling back to service");
|
|
}
|
|
}
|
|
|
|
boolean useGsf = iidPackageName.endsWith(".gsf");
|
|
if (intent.hasExtra(EXTRA_USE_GSF))
|
|
useGsf = "1".equals(intent.getStringExtra(EXTRA_USE_GSF));
|
|
|
|
if (useGsf) {
|
|
Intent holder = new Intent(ACTION_INSTANCE_ID);
|
|
holder.setPackage(context.getPackageName());
|
|
holder.putExtra(EXTRA_GSF_INTENT, intent);
|
|
context.startService(holder);
|
|
} else {
|
|
intent.putExtra(EXTRA_MESSENGER, myMessenger);
|
|
intent.putExtra(EXTRA_IS_MESSENGER2, "1");
|
|
if (messengerCompat != null) {
|
|
Message msg = Message.obtain();
|
|
msg.obj = intent;
|
|
try {
|
|
messengerCompat.send(msg);
|
|
return;
|
|
} catch (RemoteException e) {
|
|
Log.d(TAG, "Messenger failed, falling back to service");
|
|
}
|
|
}
|
|
context.startService(intent);
|
|
}
|
|
}
|
|
|
|
public Intent sendRegisterMessageBlocking(Bundle data, KeyPair keyPair) throws IOException {
|
|
Intent intent = sendRegisterMessageBlockingInternal(data, keyPair);
|
|
if (intent != null && intent.hasExtra(EXTRA_MESSENGER)) {
|
|
// Now with a messenger
|
|
intent = sendRegisterMessageBlockingInternal(data, keyPair);
|
|
}
|
|
return intent;
|
|
}
|
|
|
|
private Intent sendRegisterMessageBlockingInternal(Bundle data, KeyPair keyPair) throws IOException {
|
|
ConditionVariable cv = new ConditionVariable();
|
|
String requestId = getRequestId();
|
|
synchronized (InstanceIdRpc.class) {
|
|
blockingResponses.put(requestId, cv);
|
|
}
|
|
|
|
sendRegisterMessage(data, keyPair, requestId);
|
|
|
|
cv.block(BLOCKING_WAIT_TIME);
|
|
synchronized (InstanceIdRpc.class) {
|
|
Object res = blockingResponses.remove(requestId);
|
|
if (res instanceof Intent) {
|
|
return (Intent) res;
|
|
} else if (res instanceof String) {
|
|
throw new IOException((String) res);
|
|
}
|
|
Log.w(TAG, "No response " + res);
|
|
throw new IOException(ERROR_TIMEOUT);
|
|
}
|
|
}
|
|
|
|
public String handleRegisterMessageResult(Intent resultIntent) throws IOException {
|
|
if (resultIntent == null) throw new IOException(ERROR_SERVICE_NOT_AVAILABLE);
|
|
String result = resultIntent.getStringExtra(EXTRA_REGISTRATION_ID);
|
|
if (result == null) result = resultIntent.getStringExtra(EXTRA_UNREGISTERED);
|
|
if (result != null) return result;
|
|
result = resultIntent.getStringExtra(EXTRA_ERROR);
|
|
throw new IOException(result != null ? result : ERROR_SERVICE_NOT_AVAILABLE);
|
|
}
|
|
|
|
private void setResponse(String requestId, Object response) {
|
|
if (requestId == null) {
|
|
for (String r : blockingResponses.keySet()) {
|
|
setResponse(r, response);
|
|
}
|
|
}
|
|
Object old = blockingResponses.get(requestId);
|
|
blockingResponses.put(requestId, response);
|
|
if (old instanceof ConditionVariable) {
|
|
((ConditionVariable) old).open();
|
|
} else if (old instanceof Messenger) {
|
|
Message msg = Message.obtain();
|
|
msg.obj = response;
|
|
try {
|
|
((Messenger) old).send(msg);
|
|
} catch (RemoteException e) {
|
|
Log.w(TAG, "Failed to send response", e);
|
|
}
|
|
}
|
|
}
|
|
}
|