VancedMicroG/play-services-iid/src/main/java/com/google/android/gms/iid/InstanceID.java

283 lines
10 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 com.google.android.gms.iid;
import android.content.Context;
import android.os.Bundle;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import org.microg.gms.common.PublicApi;
import org.microg.gms.gcm.GcmConstants;
import org.microg.gms.iid.InstanceIdRpc;
import org.microg.gms.iid.InstanceIdStore;
import java.io.IOException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import static org.microg.gms.gcm.GcmConstants.EXTRA_DELETE;
import static org.microg.gms.gcm.GcmConstants.EXTRA_SCOPE;
import static org.microg.gms.gcm.GcmConstants.EXTRA_SENDER;
import static org.microg.gms.gcm.GcmConstants.EXTRA_SUBSCIPTION;
import static org.microg.gms.gcm.GcmConstants.EXTRA_SUBTYPE;
/**
* Instance ID provides a unique identifier for each app instance and a mechanism
* to authenticate and authorize actions (for example, sending a GCM message).
* <p/>
* Instance ID is stable but may become invalid, if:
* <ul>
* <li>App deletes Instance ID</li>
* <li>Device is factory reset</li>
* <li>User uninstalls the app</li>
* <li>User clears app data</li>
* </ul>
* If Instance ID has become invalid, the app can call {@link com.google.android.gms.iid.InstanceID#getId()}
* to request a new Instance ID.
* To prove ownership of Instance ID and to allow servers to access data or
* services associated with the app, call {@link com.google.android.gms.iid.InstanceID#getToken(java.lang.String, java.lang.String)}.
*/
@PublicApi
public class InstanceID {
/**
* Error returned when failed requests are retried too often. Use
* exponential backoff when retrying requests
*/
public static final String ERROR_BACKOFF = "RETRY_LATER";
/**
* Blocking methods must not be called on the main thread.
*/
public static final String ERROR_MAIN_THREAD = "MAIN_THREAD";
/**
* Tokens can't be generated. Only devices with Google Play are supported.
*/
public static final String ERROR_MISSING_INSTANCEID_SERVICE = "MISSING_INSTANCEID_SERVICE";
/**
* The device cannot read the response, or there was a server error.
* Application should retry the request later using exponential backoff
* and retry (on each subsequent failure increase delay before retrying).
*/
public static final String ERROR_SERVICE_NOT_AVAILABLE = GcmConstants.ERROR_SERVICE_NOT_AVAILABLE;
/**
* Timeout waiting for a response.
*/
public static final String ERROR_TIMEOUT = "TIMEOUT";
private static final int RSA_KEY_SIZE = 2048;
private static final String TAG = "InstanceID";
private static InstanceIdStore storeInstance;
private static InstanceIdRpc rpc;
private static Map<String, InstanceID> instances = new HashMap<String, InstanceID>();
private final String subtype;
private KeyPair keyPair;
private long creationTime;
private InstanceID(String subtype) {
this.subtype = subtype == null ? "" : subtype;
}
/**
* Resets Instance ID and revokes all tokens.
*
* @throws IOException
*/
public void deleteInstanceID() throws IOException {
deleteToken("*", "*");
creationTime = 0;
storeInstance.delete(subtype + "|");
keyPair = null;
}
/**
* Revokes access to a scope (action) for an entity previously
* authorized by {@link com.google.android.gms.iid.InstanceID#getToken(java.lang.String, java.lang.String)}.
* <p/>
* Do not call this function on the main thread.
*
* @param authorizedEntity Entity that must no longer have access.
* @param scope Action that entity is no longer authorized to perform.
* @throws IOException if the request fails.
*/
public void deleteToken(String authorizedEntity, String scope) throws IOException {
deleteToken(authorizedEntity, scope, null);
}
@PublicApi(exclude = true)
public void deleteToken(String authorizedEntity, String scope, Bundle extras) throws IOException {
if (Looper.getMainLooper() == Looper.myLooper()) throw new IOException(ERROR_MAIN_THREAD);
storeInstance.delete(subtype, authorizedEntity, scope);
if (extras == null) extras = new Bundle();
extras.putString(EXTRA_SENDER, authorizedEntity);
extras.putString(EXTRA_SUBSCIPTION, authorizedEntity);
extras.putString(EXTRA_DELETE, "1");
extras.putString("X-" + EXTRA_DELETE, "1");
extras.putString(EXTRA_SUBTYPE, TextUtils.isEmpty(subtype) ? authorizedEntity : subtype);
extras.putString("X-" + EXTRA_SUBTYPE, TextUtils.isEmpty(subtype) ? authorizedEntity : subtype);
if (scope != null) extras.putString(EXTRA_SCOPE, scope);
rpc.handleRegisterMessageResult(rpc.sendRegisterMessageBlocking(extras, getKeyPair()));
}
/**
* Returns time when instance ID was created.
*
* @return Time when instance ID was created (milliseconds since Epoch).
*/
public long getCreationTime() {
if (creationTime == 0) {
String s = storeInstance.getSecret(subtype, "cre");
if (s != null) {
creationTime = Long.parseLong(s);
}
}
return creationTime;
}
/**
* Returns a stable identifier that uniquely identifies the app instance.
*
* @return The identifier for the application instance.
*/
public String getId() {
return sha1KeyPair(getKeyPair());
}
/**
* Returns an instance of this class.
*
* @return InstanceID instance.
*/
public static InstanceID getInstance(Context context) {
String subtype = "";
if (storeInstance == null) {
storeInstance = new InstanceIdStore(context.getApplicationContext());
rpc = new InstanceIdRpc(context.getApplicationContext());
}
InstanceID instance = instances.get(subtype);
if (instance == null) {
instance = new InstanceID(subtype);
instances.put(subtype, instance);
}
return instance;
}
/**
* Returns a token that authorizes an Entity (example: cloud service) to perform
* an action on behalf of the application identified by Instance ID.
* <p/>
* This is similar to an OAuth2 token except, it applies to the
* application instance instead of a user.
* <p/>
* Do not call this function on the main thread.
*
* @param authorizedEntity Entity authorized by the token.
* @param scope Action authorized for authorizedEntity.
* @param extras additional parameters specific to each token scope.
* Bundle keys starting with 'GCM.' and 'GOOGLE.' are
* reserved.
* @return a token that can identify and authorize the instance of the
* application on the device.
* @throws IOException if the request fails.
*/
public String getToken(String authorizedEntity, String scope, Bundle extras) throws IOException {
if (Looper.getMainLooper() == Looper.myLooper()) throw new IOException(ERROR_MAIN_THREAD);
long tokenTimestamp = storeInstance.getTokenTimestamp(subtype, authorizedEntity, scope);
if (tokenTimestamp > System.currentTimeMillis() - 7 * 24 * 60 * 60 * 1000L) {
String token = storeInstance.getToken(subtype, authorizedEntity, scope);
if (token != null) return token;
}
String token = requestToken(authorizedEntity, scope, extras);
storeInstance.putToken(subtype, authorizedEntity, scope, token);
return token;
}
/**
* Returns a token that authorizes an Entity (example: cloud service) to perform
* an action on behalf of the application identified by Instance ID.
* <p/>
* This is similar to an OAuth2 token except, it applies to the
* application instance instead of a user.
* <p/>
* Do not call this function on the main thread.
*
* @param authorizedEntity Entity authorized by the token.
* @param scope Action authorized for authorizedEntity.
* @return a token that can identify and authorize the instance of the
* application on the device.
* @throws IOException if the request fails.
*/
public String getToken(String authorizedEntity, String scope) throws IOException {
return getToken(authorizedEntity, scope, null);
}
@PublicApi(exclude = true)
public InstanceIdStore getStore() {
return storeInstance;
}
@PublicApi(exclude = true)
public String requestToken(String authorizedEntity, String scope, Bundle extras) {
throw new UnsupportedOperationException();
}
private synchronized KeyPair getKeyPair() {
if (keyPair == null) {
keyPair = storeInstance.getKeyPair(subtype);
if (keyPair == null) {
try {
KeyPairGenerator rsaGenerator = KeyPairGenerator.getInstance("RSA");
rsaGenerator.initialize(RSA_KEY_SIZE);
keyPair = rsaGenerator.generateKeyPair();
creationTime = System.currentTimeMillis();
storeInstance.putKeyPair(subtype, keyPair, creationTime);
} catch (NoSuchAlgorithmException e) {
Log.w(TAG, e);
}
}
}
return keyPair;
}
@PublicApi(exclude = true)
public static String sha1KeyPair(KeyPair keyPair) {
try {
byte[] digest = MessageDigest.getInstance("SHA1").digest(keyPair.getPublic().getEncoded());
digest[0] = (byte) (112 + (0xF & digest[0]) & 0xFF);
return Base64.encodeToString(digest, 0, 8, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
} catch (NoSuchAlgorithmException e) {
Log.w(TAG, e);
return null;
}
}
}