diff --git a/firebase-installations/api.txt b/firebase-installations/api.txt index e344cd2e484..1a7f890c6d3 100644 --- a/firebase-installations/api.txt +++ b/firebase-installations/api.txt @@ -25,6 +25,11 @@ package com.google.firebase.installations { package com.google.firebase.installations.local { + public class IidStore { + ctor public IidStore(); + method @Nullable public String readIid(); + } + public class PersistedInstallation { ctor public PersistedInstallation(@NonNull FirebaseApp); method @NonNull public boolean clear(); diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java index caa4eea58ff..dc569af259c 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java @@ -28,7 +28,9 @@ import static com.google.firebase.installations.FisAndroidTestConstants.TEST_CREATION_TIMESTAMP_2; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_FID_1; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_INSTALLATION_RESPONSE; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_INSTALLATION_RESPONSE_WITH_IID; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_INSTALLATION_TOKEN_RESULT; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_INSTANCE_ID_1; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_PROJECT_ID; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_REFRESH_TOKEN; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_EXPIRATION_TIMESTAMP; @@ -52,6 +54,7 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseException; import com.google.firebase.FirebaseOptions; +import com.google.firebase.installations.local.IidStore; import com.google.firebase.installations.local.PersistedInstallation; import com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus; import com.google.firebase.installations.local.PersistedInstallationEntry; @@ -88,6 +91,7 @@ public class FirebaseInstallationsInstrumentedTest { @Mock private Utils mockUtils; @Mock private PersistedInstallation mockPersistedInstallation; @Mock private FirebaseInstallationServiceClient mockClient; + @Mock private IidStore mockIidStore; private static final PersistedInstallationEntry REGISTERED_INSTALLATION_ENTRY = PersistedInstallationEntry.builder() @@ -99,6 +103,16 @@ public class FirebaseInstallationsInstrumentedTest { .setRegistrationStatus(PersistedInstallation.RegistrationStatus.REGISTERED) .build(); + private static final PersistedInstallationEntry REGISTERED_IID_ENTRY = + PersistedInstallationEntry.builder() + .setFirebaseInstallationId(TEST_INSTANCE_ID_1) + .setAuthToken(TEST_AUTH_TOKEN) + .setRefreshToken(TEST_REFRESH_TOKEN) + .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2) + .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .setRegistrationStatus(PersistedInstallation.RegistrationStatus.REGISTERED) + .build(); + private static final PersistedInstallationEntry EXPIRED_AUTH_TOKEN_ENTRY = PersistedInstallationEntry.builder() .setFirebaseInstallationId(TEST_FID_1) @@ -189,12 +203,20 @@ public void cleanUp() throws Exception { persistedInstallation.clear(); } + private FirebaseInstallations getFirebaseInstallations() { + return new FirebaseInstallations( + executor, + firebaseApp, + backendClientReturnsOk, + persistedInstallation, + mockUtils, + mockIidStore); + } + @Test public void testGetId_PersistedInstallationOk_BackendOk() throws Exception { - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + when(mockUtils.isAuthTokenExpired(REGISTERED_IID_ENTRY)).thenReturn(false); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); // No exception, means success. assertWithMessage("getId Task failed.") @@ -204,7 +226,8 @@ public void testGetId_PersistedInstallationOk_BackendOk() throws Exception { persistedInstallation.readPersistedInstallationEntryValue(); assertThat(entryValue).hasFid(TEST_FID_1); - // Waiting for Task that registers FID on the FIS Servers + // getId() returns fid immediately but registers fid asynchronously. Waiting for half a second + // while we mock fid registration. We dont send an actual request to FIS in tests. executor.awaitTermination(500, TimeUnit.MILLISECONDS); PersistedInstallationEntry updatedInstallationEntry = @@ -213,15 +236,39 @@ public void testGetId_PersistedInstallationOk_BackendOk() throws Exception { assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); } + @Test + public void testGetId_migrateIid_successful() throws Exception { + when(mockIidStore.readIid()).thenReturn(TEST_INSTANCE_ID_1); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); + when(backendClientReturnsOk.createFirebaseInstallation( + anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE_WITH_IID); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); + + // No exception, means success. + assertWithMessage("getId Task failed.") + .that(Tasks.await(firebaseInstallations.getId())) + .isNotEmpty(); + PersistedInstallationEntry entryValue = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entryValue).hasFid(TEST_INSTANCE_ID_1); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + PersistedInstallationEntry updatedInstallationEntry = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(updatedInstallationEntry).hasFid(TEST_INSTANCE_ID_1); + assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); + } + @Test public void testGetId_multipleCalls_sameFIDReturned() throws Exception { when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); when(backendClientReturnsOk.createFirebaseInstallation( anyString(), anyString(), anyString(), anyString())) .thenReturn(TEST_INSTALLATION_RESPONSE); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); // Call getId multiple times Task task1 = firebaseInstallations.getId(); @@ -249,9 +296,7 @@ public void testGetId_invalidFid_storesValidFidFromResponse() throws Exception { // Update local storage with installation entry that has invalid fid. persistedInstallation.insertOrUpdatePersistedInstallationEntry(INVALID_INSTALLATION_ENTRY); when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); // No exception, means success. assertWithMessage("getId Task failed.") @@ -275,7 +320,12 @@ public void testGetId_invalidFid_storesValidFidFromResponse() throws Exception { public void testGetId_PersistedInstallationOk_BackendError() throws Exception { FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsError, persistedInstallation, mockUtils); + executor, + firebaseApp, + backendClientReturnsError, + persistedInstallation, + mockUtils, + mockIidStore); Tasks.await(firebaseInstallations.getId()); @@ -299,9 +349,7 @@ public void testGetId_ServerError_UnregisteredFID() throws Exception { anyString(), anyString(), anyString(), anyString())) .thenReturn(SERVER_ERROR_INSTALLATION_RESPONSE); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); Tasks.await(firebaseInstallations.getId()); @@ -326,7 +374,8 @@ public void testGetId_PersistedInstallationError_BackendOk() throws InterruptedE firebaseApp, backendClientReturnsOk, persistedInstallationReturnsError, - mockUtils); + mockUtils, + mockIidStore); // Expect exception try { @@ -355,7 +404,7 @@ public void testGetId_fidRegistrationUncheckedException_statusUpdated() throws E FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, mockClient, persistedInstallation, mockUtils); + executor, firebaseApp, mockClient, persistedInstallation, mockUtils, mockIidStore); Tasks.await(firebaseInstallations.getId()); @@ -387,7 +436,7 @@ public void testGetId_expiredAuthTokenUncheckedException_statusUpdated() throws FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, mockClient, persistedInstallation, mockUtils); + executor, firebaseApp, mockClient, persistedInstallation, mockUtils, mockIidStore); assertWithMessage("getId Task failed") .that(Tasks.await(firebaseInstallations.getId())) @@ -412,9 +461,7 @@ public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception { persistedInstallation.insertOrUpdatePersistedInstallationEntry(EXPIRED_AUTH_TOKEN_ENTRY); when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(true); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); assertWithMessage("getId Task failed") .that(Tasks.await(firebaseInstallations.getId())) @@ -439,9 +486,7 @@ public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception { @Test public void testGetAuthToken_fidDoesNotExist_successful() throws Exception { when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); Tasks.await(firebaseInstallations.getAuthToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); @@ -459,7 +504,8 @@ public void testGetAuthToken_PersistedInstallationError_failure() throws Excepti firebaseApp, backendClientReturnsOk, persistedInstallationReturnsError, - mockUtils); + mockUtils, + mockIidStore); // Expect exception try { @@ -485,7 +531,12 @@ public void testGetAuthToken_fidExists_successful() throws Exception { FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, mockPersistedInstallation, mockUtils); + executor, + firebaseApp, + backendClientReturnsOk, + mockPersistedInstallation, + mockUtils, + mockIidStore); InstallationTokenResult installationTokenResult = Tasks.await( @@ -504,9 +555,7 @@ public void testGetAuthToken_expiredAuthToken_fetchedNewTokenFromFIS() throws Ex when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(true); when(mockUtils.isAuthTokenExpired(UPDATED_AUTH_TOKEN_ENTRY)).thenReturn(false); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); InstallationTokenResult installationTokenResult = Tasks.await( @@ -526,9 +575,7 @@ public void testGetAuthToken_unregisteredFid_fetchedNewTokenFromFIS() throws Exc persistedInstallation.insertOrUpdatePersistedInstallationEntry(UNREGISTERED_INSTALLATION_ENTRY); when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); InstallationTokenResult installationTokenResult = Tasks.await( @@ -552,7 +599,12 @@ public void testGetAuthToken_serverError_failure() throws Exception { FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsError, mockPersistedInstallation, mockUtils); + executor, + firebaseApp, + backendClientReturnsError, + mockPersistedInstallation, + mockUtils, + mockIidStore); // Expect exception try { @@ -579,9 +631,7 @@ public void testGetAuthToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce( when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(true); when(mockUtils.isAuthTokenExpired(UPDATED_AUTH_TOKEN_ENTRY)).thenReturn(false); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); // Call getAuthToken multiple times with DO_NOT_FORCE_REFRESH option Task task1 = @@ -630,9 +680,7 @@ public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() th .generateAuthToken(anyString(), anyString(), anyString(), anyString()); when(mockUtils.isAuthTokenExpired(any())).thenReturn(false); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); // Call getAuthToken multiple times with FORCE_REFRESH option. Task task1 = @@ -659,9 +707,7 @@ public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() th public void testDelete_registeredFID_successful() throws Exception { // Update local storage with a registered installation entry persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); Tasks.await(firebaseInstallations.delete()); @@ -676,9 +722,7 @@ public void testDelete_registeredFID_successful() throws Exception { public void testDelete_unregisteredFID_successful() throws Exception { // Update local storage with a unregistered installation entry persistedInstallation.insertOrUpdatePersistedInstallationEntry(UNREGISTERED_INSTALLATION_ENTRY); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); Tasks.await(firebaseInstallations.delete()); @@ -691,9 +735,7 @@ public void testDelete_unregisteredFID_successful() throws Exception { @Test public void testDelete_emptyPersistedFidEntry_successful() throws Exception { - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); Tasks.await(firebaseInstallations.delete()); @@ -710,7 +752,12 @@ public void testDelete_serverError_failure() throws Exception { persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsError, persistedInstallation, mockUtils); + executor, + firebaseApp, + backendClientReturnsError, + persistedInstallation, + mockUtils, + mockIidStore); // Expect exception try { diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java index 1f17b79aee9..f0cefaa133e 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java @@ -43,6 +43,8 @@ public final class FisAndroidTestConstants { public static final long TEST_CREATION_TIMESTAMP_1 = 2000L; public static final long TEST_CREATION_TIMESTAMP_2 = 2L; + public static final String TEST_INSTANCE_ID_1 = "ccccccccccc"; + public static final PersistedInstallationEntry DEFAULT_PERSISTED_INSTALLATION_ENTRY = PersistedInstallationEntry.builder().build(); public static final InstallationResponse TEST_INSTALLATION_RESPONSE = @@ -59,6 +61,20 @@ public final class FisAndroidTestConstants { .setResponseCode(ResponseCode.OK) .build(); + public static final InstallationResponse TEST_INSTALLATION_RESPONSE_WITH_IID = + InstallationResponse.builder() + .setUri("/projects/" + TEST_PROJECT_ID + "/installations/" + TEST_INSTANCE_ID_1) + .setFid(TEST_INSTANCE_ID_1) + .setRefreshToken(TEST_REFRESH_TOKEN) + .setAuthToken( + InstallationTokenResult.builder() + .setToken(TEST_AUTH_TOKEN) + .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .setTokenCreationTimestamp(TEST_CREATION_TIMESTAMP_1) + .build()) + .setResponseCode(ResponseCode.OK) + .build(); + public static final InstallationTokenResult TEST_INSTALLATION_TOKEN_RESULT = InstallationTokenResult.builder() .setToken(TEST_AUTH_TOKEN_2) diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java index a29fa0e7d9e..8a9574d09ee 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -24,6 +24,7 @@ import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseException; +import com.google.firebase.installations.local.IidStore; import com.google.firebase.installations.local.PersistedInstallation; import com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus; import com.google.firebase.installations.local.PersistedInstallationEntry; @@ -56,6 +57,7 @@ public class FirebaseInstallations implements FirebaseInstallationsApi { private final PersistedInstallation persistedInstallation; private final ExecutorService executor; private final Utils utils; + private final IidStore iidStore; private final Object lock = new Object(); @GuardedBy("lock") @@ -71,7 +73,8 @@ public class FirebaseInstallations implements FirebaseInstallationsApi { firebaseApp, new FirebaseInstallationServiceClient(firebaseApp.getApplicationContext()), new PersistedInstallation(firebaseApp), - new Utils(DefaultClock.getInstance())); + new Utils(DefaultClock.getInstance()), + new IidStore()); } FirebaseInstallations( @@ -79,12 +82,14 @@ public class FirebaseInstallations implements FirebaseInstallationsApi { FirebaseApp firebaseApp, FirebaseInstallationServiceClient serviceClient, PersistedInstallation persistedInstallation, - Utils utils) { + Utils utils, + IidStore iidStore) { this.firebaseApp = firebaseApp; this.serviceClient = serviceClient; this.executor = executor; this.persistedInstallation = persistedInstallation; this.utils = utils; + this.iidStore = iidStore; } /** @@ -220,7 +225,10 @@ private final void doRegistration() { // New FID needs to be created if (persistedInstallationEntry.isNotGenerated()) { - String fid = utils.createRandomFid(); + + // For a default firebase installation read the existing iid. For other custom firebase + // installations create a new fid + String fid = readExistingIidOrCreateFid(); persistFid(fid); persistedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); } @@ -277,6 +285,19 @@ private final void doRegistration() { } } + private String readExistingIidOrCreateFid() { + // Check if this firebase app is the default (first initialized) instance + if (!firebaseApp.equals(FirebaseApp.getInstance())) { + return utils.createRandomFid(); + } + // For a default firebase installation, read the existing iid from shared prefs + String fid = iidStore.readIid(); + if (fid == null) { + fid = utils.createRandomFid(); + } + return fid; + } + private void persistFid(String fid) throws FirebaseInstallationsException { boolean firstUpdateCacheResult = persistedInstallation.insertOrUpdatePersistedInstallationEntry( diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/IidStore.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/IidStore.java new file mode 100644 index 00000000000..4e8366df7bf --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/IidStore.java @@ -0,0 +1,135 @@ +// Copyright 2019 Google LLC +// +// 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.firebase.installations.local; + +import static android.content.ContentValues.TAG; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Base64; +import android.util.Log; +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.firebase.FirebaseApp; +import java.security.KeyFactory; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; + +/** + * Read existing iid only for default (first initialized) instance of this firebase application.* + */ +public class IidStore { + private static final String IID_SHARED_PREFS_NAME = "com.google.android.gms.appid"; + private static final String STORE_KEY_PUB = "|S||P|"; + private static final String STORE_KEY_ID = "|S|id"; + + @GuardedBy("iidPrefs") + private final SharedPreferences iidPrefs; + + public IidStore() { + // Different FirebaseApp in the same Android application should have the same application + // context and same dir path. We only read existing Iids for the default firebase application. + iidPrefs = + FirebaseApp.getInstance() + .getApplicationContext() + .getSharedPreferences(IID_SHARED_PREFS_NAME, Context.MODE_PRIVATE); + } + + @Nullable + public String readIid() { + synchronized (iidPrefs) { + // Background: Some versions of the IID-SDK store the Instance-ID in local storage, + // others only store the App-Instance's Public-Key that can be used to calculate the + // Instance-ID. + + // If such a version was used by this App-Instance, we can directly read the existing + // Instance-ID from storage and return it + String id = readInstanceIdFromLocalStorage(); + + if (id != null) { + return id; + } + + // If this App-Instance did not store the Instance-ID in local storage, we may be able to find + // its Public-Key in order to calculate the App-Instance's Instance-ID. + return readPublicKeyFromLocalStorageAndCalculateInstanceId(); + } + } + + @Nullable + private String readInstanceIdFromLocalStorage() { + synchronized (iidPrefs) { + return iidPrefs.getString(STORE_KEY_ID, /* defaultValue= */ null); + } + } + + @Nullable + private String readPublicKeyFromLocalStorageAndCalculateInstanceId() { + synchronized (iidPrefs) { + String base64PublicKey = iidPrefs.getString(STORE_KEY_PUB, /* defaultValue= */ null); + if (base64PublicKey == null) { + return null; + } + + PublicKey publicKey = parseKey(base64PublicKey); + if (publicKey == null) { + return null; + } + + return getIdFromPublicKey(publicKey); + } + } + + @Nullable + private static String getIdFromPublicKey(@NonNull PublicKey publicKey) { + // The ID is the sha of the public key truncated to 60 bit, with first 4 bits switched to + // 0x9 and base64 encoded + // This allows the id to be used internally for legacy systems and differentiate from + // old android ids and gcm ids + + byte[] derPub = publicKey.getEncoded(); + try { + MessageDigest md = MessageDigest.getInstance("SHA1"); + + byte[] digest = md.digest(derPub); + int b0 = digest[0]; + b0 = 0x70 + (0xF & b0); + digest[0] = (byte) (b0 & 0xFF); + return Base64.encodeToString( + digest, 0, 8, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING); + } catch (NoSuchAlgorithmException e) { + Log.w(TAG, "Unexpected error, device missing required algorithms"); + } + return null; + } + + /** Parse the public key from stored data. */ + @Nullable + private PublicKey parseKey(String base64PublicKey) { + byte[] publicKeyBytes; + try { + publicKeyBytes = Base64.decode(base64PublicKey, Base64.URL_SAFE); + KeyFactory kf = KeyFactory.getInstance("RSA"); + return kf.generatePublic(new X509EncodedKeySpec(publicKeyBytes)); + } catch (IllegalArgumentException | InvalidKeySpecException | NoSuchAlgorithmException e) { + Log.w(TAG, "Invalid key stored " + e); + } + return null; + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java index aa47c16d8b1..fdee274945d 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java @@ -63,6 +63,7 @@ public enum RegistrationStatus { private static final String TOKEN_CREATION_TIME_IN_SECONDS_KEY = "TokenCreationEpochInSecs"; private static final String EXPIRES_IN_SECONDS_KEY = "ExpiresInSecs"; private static final String PERSISTED_STATUS_KEY = "Status"; + private static final String FIS_ERROR_KEY = "FisError"; private static final List FID_PREF_KEYS = Arrays.asList( @@ -71,7 +72,8 @@ public enum RegistrationStatus { REFRESH_TOKEN_KEY, TOKEN_CREATION_TIME_IN_SECONDS_KEY, EXPIRES_IN_SECONDS_KEY, - PERSISTED_STATUS_KEY); + PERSISTED_STATUS_KEY, + FIS_ERROR_KEY); @GuardedBy("prefs") private final SharedPreferences prefs; @@ -98,6 +100,7 @@ public PersistedInstallationEntry readPersistedInstallationEntryValue() { long tokenCreationTime = prefs.getLong(getSharedPreferencesKey(TOKEN_CREATION_TIME_IN_SECONDS_KEY), 0); long expiresIn = prefs.getLong(getSharedPreferencesKey(EXPIRES_IN_SECONDS_KEY), 0); + String fisError = prefs.getString(getSharedPreferencesKey(FIS_ERROR_KEY), null); if (fid == null || !(status >= 0 && status < RegistrationStatus.values().length)) { return PersistedInstallationEntry.builder().build(); @@ -109,6 +112,7 @@ public PersistedInstallationEntry readPersistedInstallationEntryValue() { .setRefreshToken(refreshToken) .setTokenCreationEpochInSecs(tokenCreationTime) .setExpiresInSecs(expiresIn) + .setFisError(fisError) .build(); } } @@ -131,6 +135,7 @@ public boolean insertOrUpdatePersistedInstallationEntry( entryValue.getTokenCreationEpochInSecs()); editor.putLong( getSharedPreferencesKey(EXPIRES_IN_SECONDS_KEY), entryValue.getExpiresInSecs()); + editor.putString(getSharedPreferencesKey(FIS_ERROR_KEY), entryValue.getFisError()); return editor.commit(); } }