Loading core/java/android/security/recoverablekeystore/RecoverableKeyStoreLoader.java +2 −0 Original line number Diff line number Diff line Loading @@ -45,6 +45,8 @@ public class RecoverableKeyStoreLoader { public static final int NO_ERROR = KeyStore.NO_ERROR; public static final int SYSTEM_ERROR = KeyStore.SYSTEM_ERROR; public static final int UNINITIALIZED_RECOVERY_PUBLIC_KEY = 20; public static final int NO_SNAPSHOT_PENDING_ERROR = 21; /** * Rate limit is enforced to prevent using too many trusted remote devices, since each device * can have its own number of user secret guesses allowed. Loading services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java +38 −16 Original line number Diff line number Diff line Loading @@ -16,14 +16,20 @@ package com.android.server.locksettings.recoverablekeystore; import static android.security.recoverablekeystore.KeyStoreRecoveryMetadata.TYPE_LOCKSCREEN; import android.annotation.NonNull; import android.content.Context; import android.security.recoverablekeystore.KeyDerivationParameters; import android.security.recoverablekeystore.KeyEntryRecoveryData; import android.security.recoverablekeystore.KeyStoreRecoveryData; import android.security.recoverablekeystore.KeyStoreRecoveryMetadata; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.widget.LockPatternUtils; import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb; import com.android.server.locksettings.recoverablekeystore.storage.RecoverySnapshotStorage; import java.nio.ByteBuffer; import java.nio.ByteOrder; Loading @@ -36,6 +42,8 @@ import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.SecureRandom; import java.security.UnrecoverableKeyException; import java.util.ArrayList; import java.util.List; import java.util.Map; import javax.crypto.KeyGenerator; Loading @@ -58,28 +66,27 @@ public class KeySyncTask implements Runnable { private final RecoverableKeyStoreDb mRecoverableKeyStoreDb; private final int mUserId; private final RecoverableSnapshotConsumer mSnapshotConsumer; private final int mCredentialType; private final String mCredential; private final PlatformKeyManager.Factory mPlatformKeyManagerFactory; private final VaultKeySupplier mVaultKeySupplier; private final RecoverySnapshotStorage mRecoverySnapshotStorage; public static KeySyncTask newInstance( Context context, RecoverableKeyStoreDb recoverableKeyStoreDb, RecoverySnapshotStorage snapshotStorage, int userId, int credentialType, String credential ) throws NoSuchAlgorithmException, KeyStoreException, InsecureUserException { return new KeySyncTask( recoverableKeyStoreDb, snapshotStorage, userId, credentialType, credential, () -> PlatformKeyManager.getInstance(context, recoverableKeyStoreDb, userId), (salt, recoveryKey, applicationKeys) -> { // TODO: implement sending intent }, () -> { throw new UnsupportedOperationException("Not implemented vault key."); }); Loading @@ -99,19 +106,19 @@ public class KeySyncTask implements Runnable { @VisibleForTesting KeySyncTask( RecoverableKeyStoreDb recoverableKeyStoreDb, RecoverySnapshotStorage snapshotStorage, int userId, int credentialType, String credential, PlatformKeyManager.Factory platformKeyManagerFactory, RecoverableSnapshotConsumer snapshotConsumer, VaultKeySupplier vaultKeySupplier) { mRecoverableKeyStoreDb = recoverableKeyStoreDb; mUserId = userId; mCredentialType = credentialType; mCredential = credential; mPlatformKeyManagerFactory = platformKeyManagerFactory; mSnapshotConsumer = snapshotConsumer; mVaultKeySupplier = vaultKeySupplier; mRecoverySnapshotStorage = snapshotStorage; } @Override Loading Loading @@ -185,7 +192,21 @@ public class KeySyncTask implements Runnable { return; } mSnapshotConsumer.accept(salt, encryptedRecoveryKey, encryptedApplicationKeys); // TODO: why is the secret sent here? I thought it wasn't sent in the raw at all. KeyStoreRecoveryMetadata metadata = new KeyStoreRecoveryMetadata( /*userSecretType=*/ TYPE_LOCKSCREEN, /*lockScreenUiFormat=*/ mCredentialType, /*keyDerivationParameters=*/ KeyDerivationParameters.createSHA256Parameters(salt), /*secret=*/ new byte[0]); ArrayList<KeyStoreRecoveryMetadata> metadataList = new ArrayList<>(); metadataList.add(metadata); // TODO: implement snapshot version mRecoverySnapshotStorage.put(mUserId, new KeyStoreRecoveryData( /*snapshotVersion=*/ 1, /*recoveryMetadata=*/ metadataList, /*applicationKeyBlobs=*/ createApplicationKeyEntries(encryptedApplicationKeys), /*encryptedRecoveryKeyblob=*/ encryptedRecoveryKey)); } private PublicKey getVaultPublicKey() { Loading Loading @@ -290,15 +311,16 @@ public class KeySyncTask implements Runnable { return keyGenerator.generateKey(); } /** * TODO: just replace with the Intent call. I'm not sure exactly what this looks like, hence * this interface, so I can test in the meantime. */ public interface RecoverableSnapshotConsumer { void accept( byte[] salt, byte[] encryptedRecoveryKey, Map<String, byte[]> encryptedApplicationKeys); private static List<KeyEntryRecoveryData> createApplicationKeyEntries( Map<String, byte[]> encryptedApplicationKeys) { ArrayList<KeyEntryRecoveryData> keyEntries = new ArrayList<>(); for (String alias : encryptedApplicationKeys.keySet()) { keyEntries.add( new KeyEntryRecoveryData( alias.getBytes(StandardCharsets.UTF_8), encryptedApplicationKeys.get(alias))); } return keyEntries; } /** Loading services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java +13 −20 Original line number Diff line number Diff line Loading @@ -34,6 +34,7 @@ import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb; import com.android.server.locksettings.recoverablekeystore.storage.RecoverySessionStorage; import com.android.server.locksettings.recoverablekeystore.storage.RecoverySnapshotStorage; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; Loading Loading @@ -75,6 +76,7 @@ public class RecoverableKeyStoreManager { private final ExecutorService mExecutorService; private final ListenersStorage mListenersStorage; private final RecoverableKeyGenerator mRecoverableKeyGenerator; private final RecoverySnapshotStorage mSnapshotStorage; /** * Returns a new or existing instance. Loading @@ -89,7 +91,8 @@ public class RecoverableKeyStoreManager { db, new RecoverySessionStorage(), Executors.newSingleThreadExecutor(), ListenersStorage.getInstance()); ListenersStorage.getInstance(), new RecoverySnapshotStorage()); } return mInstance; } Loading @@ -100,12 +103,14 @@ public class RecoverableKeyStoreManager { RecoverableKeyStoreDb recoverableKeyStoreDb, RecoverySessionStorage recoverySessionStorage, ExecutorService executorService, ListenersStorage listenersStorage) { ListenersStorage listenersStorage, RecoverySnapshotStorage snapshotStorage) { mContext = context; mDatabase = recoverableKeyStoreDb; mRecoverySessionStorage = recoverySessionStorage; mExecutorService = executorService; mListenersStorage = listenersStorage; mSnapshotStorage = snapshotStorage; try { mRecoverableKeyGenerator = RecoverableKeyGenerator.newInstance(mDatabase); } catch (NoSuchAlgorithmException e) { Loading Loading @@ -143,24 +148,12 @@ public class RecoverableKeyStoreManager { public @NonNull KeyStoreRecoveryData getRecoveryData(@NonNull byte[] account, int userId) throws RemoteException { checkRecoverKeyStorePermission(); final int callingUid = Binder.getCallingUid(); // Recovery agent uid. final int callingUserId = UserHandle.getCallingUserId(); final long callingIdentiy = Binder.clearCallingIdentity(); try { // TODO: Return the latest snapshot for the calling recovery agent. } finally { Binder.restoreCallingIdentity(callingIdentiy); } // KeyStoreRecoveryData without application keys and empty recovery blob. KeyStoreRecoveryData recoveryData = new KeyStoreRecoveryData( /*snapshotVersion=*/ 1, new ArrayList<KeyStoreRecoveryMetadata>(), new ArrayList<KeyEntryRecoveryData>(), /*encryptedRecoveryKeyBlob=*/ new byte[] {}); throw new ServiceSpecificException( RecoverableKeyStoreLoader.UNINITIALIZED_RECOVERY_PUBLIC_KEY); KeyStoreRecoveryData snapshot = mSnapshotStorage.get(UserHandle.getCallingUserId()); if (snapshot == null) { throw new ServiceSpecificException(RecoverableKeyStoreLoader.NO_SNAPSHOT_PENDING_ERROR); } return snapshot; } public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent, int userId) Loading Loading @@ -480,7 +473,7 @@ public class RecoverableKeyStoreManager { // So as not to block the critical path unlocking the phone, defer to another thread. try { mExecutorService.execute(KeySyncTask.newInstance( mContext, mDatabase, userId, storedHashType, credential)); mContext, mDatabase, mSnapshotStorage, userId, storedHashType, credential)); } catch (NoSuchAlgorithmException e) { Log.wtf(TAG, "Should never happen - algorithm unavailable for KeySync", e); } catch (KeyStoreException e) { Loading services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java +16 −0 Original line number Diff line number Diff line /* * Copyright (C) 2017 The Android Open Source Project * * 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.android.server.locksettings.recoverablekeystore.storage; import android.content.Context; Loading services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java 0 → 100644 +60 −0 Original line number Diff line number Diff line /* * Copyright (C) 2017 The Android Open Source Project * * 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.android.server.locksettings.recoverablekeystore.storage; import android.annotation.Nullable; import android.security.recoverablekeystore.KeyStoreRecoveryData; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; /** * In-memory storage for recovery snapshots. * * <p>Recovery snapshots are generated after a successful screen unlock. They are only generated if * the recoverable keystore has been mutated since the previous snapshot. This class stores only the * latest snapshot for each user. * * <p>This class is thread-safe. It is used both on the service thread and the * {@link com.android.server.locksettings.recoverablekeystore.KeySyncTask} thread. */ public class RecoverySnapshotStorage { @GuardedBy("this") private final SparseArray<KeyStoreRecoveryData> mSnapshotByUserId = new SparseArray<>(); /** * Sets the latest {@code snapshot} for the user {@code userId}. */ public synchronized void put(int userId, KeyStoreRecoveryData snapshot) { mSnapshotByUserId.put(userId, snapshot); } /** * Returns the latest snapshot for user {@code userId}, or null if none exists. */ @Nullable public synchronized KeyStoreRecoveryData get(int userId) { return mSnapshotByUserId.get(userId); } /** * Removes any (if any) snapshot associated with user {@code userId}. */ public synchronized void remove(int userId) { mSnapshotByUserId.remove(userId); } } Loading
core/java/android/security/recoverablekeystore/RecoverableKeyStoreLoader.java +2 −0 Original line number Diff line number Diff line Loading @@ -45,6 +45,8 @@ public class RecoverableKeyStoreLoader { public static final int NO_ERROR = KeyStore.NO_ERROR; public static final int SYSTEM_ERROR = KeyStore.SYSTEM_ERROR; public static final int UNINITIALIZED_RECOVERY_PUBLIC_KEY = 20; public static final int NO_SNAPSHOT_PENDING_ERROR = 21; /** * Rate limit is enforced to prevent using too many trusted remote devices, since each device * can have its own number of user secret guesses allowed. Loading
services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java +38 −16 Original line number Diff line number Diff line Loading @@ -16,14 +16,20 @@ package com.android.server.locksettings.recoverablekeystore; import static android.security.recoverablekeystore.KeyStoreRecoveryMetadata.TYPE_LOCKSCREEN; import android.annotation.NonNull; import android.content.Context; import android.security.recoverablekeystore.KeyDerivationParameters; import android.security.recoverablekeystore.KeyEntryRecoveryData; import android.security.recoverablekeystore.KeyStoreRecoveryData; import android.security.recoverablekeystore.KeyStoreRecoveryMetadata; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.widget.LockPatternUtils; import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb; import com.android.server.locksettings.recoverablekeystore.storage.RecoverySnapshotStorage; import java.nio.ByteBuffer; import java.nio.ByteOrder; Loading @@ -36,6 +42,8 @@ import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.SecureRandom; import java.security.UnrecoverableKeyException; import java.util.ArrayList; import java.util.List; import java.util.Map; import javax.crypto.KeyGenerator; Loading @@ -58,28 +66,27 @@ public class KeySyncTask implements Runnable { private final RecoverableKeyStoreDb mRecoverableKeyStoreDb; private final int mUserId; private final RecoverableSnapshotConsumer mSnapshotConsumer; private final int mCredentialType; private final String mCredential; private final PlatformKeyManager.Factory mPlatformKeyManagerFactory; private final VaultKeySupplier mVaultKeySupplier; private final RecoverySnapshotStorage mRecoverySnapshotStorage; public static KeySyncTask newInstance( Context context, RecoverableKeyStoreDb recoverableKeyStoreDb, RecoverySnapshotStorage snapshotStorage, int userId, int credentialType, String credential ) throws NoSuchAlgorithmException, KeyStoreException, InsecureUserException { return new KeySyncTask( recoverableKeyStoreDb, snapshotStorage, userId, credentialType, credential, () -> PlatformKeyManager.getInstance(context, recoverableKeyStoreDb, userId), (salt, recoveryKey, applicationKeys) -> { // TODO: implement sending intent }, () -> { throw new UnsupportedOperationException("Not implemented vault key."); }); Loading @@ -99,19 +106,19 @@ public class KeySyncTask implements Runnable { @VisibleForTesting KeySyncTask( RecoverableKeyStoreDb recoverableKeyStoreDb, RecoverySnapshotStorage snapshotStorage, int userId, int credentialType, String credential, PlatformKeyManager.Factory platformKeyManagerFactory, RecoverableSnapshotConsumer snapshotConsumer, VaultKeySupplier vaultKeySupplier) { mRecoverableKeyStoreDb = recoverableKeyStoreDb; mUserId = userId; mCredentialType = credentialType; mCredential = credential; mPlatformKeyManagerFactory = platformKeyManagerFactory; mSnapshotConsumer = snapshotConsumer; mVaultKeySupplier = vaultKeySupplier; mRecoverySnapshotStorage = snapshotStorage; } @Override Loading Loading @@ -185,7 +192,21 @@ public class KeySyncTask implements Runnable { return; } mSnapshotConsumer.accept(salt, encryptedRecoveryKey, encryptedApplicationKeys); // TODO: why is the secret sent here? I thought it wasn't sent in the raw at all. KeyStoreRecoveryMetadata metadata = new KeyStoreRecoveryMetadata( /*userSecretType=*/ TYPE_LOCKSCREEN, /*lockScreenUiFormat=*/ mCredentialType, /*keyDerivationParameters=*/ KeyDerivationParameters.createSHA256Parameters(salt), /*secret=*/ new byte[0]); ArrayList<KeyStoreRecoveryMetadata> metadataList = new ArrayList<>(); metadataList.add(metadata); // TODO: implement snapshot version mRecoverySnapshotStorage.put(mUserId, new KeyStoreRecoveryData( /*snapshotVersion=*/ 1, /*recoveryMetadata=*/ metadataList, /*applicationKeyBlobs=*/ createApplicationKeyEntries(encryptedApplicationKeys), /*encryptedRecoveryKeyblob=*/ encryptedRecoveryKey)); } private PublicKey getVaultPublicKey() { Loading Loading @@ -290,15 +311,16 @@ public class KeySyncTask implements Runnable { return keyGenerator.generateKey(); } /** * TODO: just replace with the Intent call. I'm not sure exactly what this looks like, hence * this interface, so I can test in the meantime. */ public interface RecoverableSnapshotConsumer { void accept( byte[] salt, byte[] encryptedRecoveryKey, Map<String, byte[]> encryptedApplicationKeys); private static List<KeyEntryRecoveryData> createApplicationKeyEntries( Map<String, byte[]> encryptedApplicationKeys) { ArrayList<KeyEntryRecoveryData> keyEntries = new ArrayList<>(); for (String alias : encryptedApplicationKeys.keySet()) { keyEntries.add( new KeyEntryRecoveryData( alias.getBytes(StandardCharsets.UTF_8), encryptedApplicationKeys.get(alias))); } return keyEntries; } /** Loading
services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java +13 −20 Original line number Diff line number Diff line Loading @@ -34,6 +34,7 @@ import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb; import com.android.server.locksettings.recoverablekeystore.storage.RecoverySessionStorage; import com.android.server.locksettings.recoverablekeystore.storage.RecoverySnapshotStorage; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; Loading Loading @@ -75,6 +76,7 @@ public class RecoverableKeyStoreManager { private final ExecutorService mExecutorService; private final ListenersStorage mListenersStorage; private final RecoverableKeyGenerator mRecoverableKeyGenerator; private final RecoverySnapshotStorage mSnapshotStorage; /** * Returns a new or existing instance. Loading @@ -89,7 +91,8 @@ public class RecoverableKeyStoreManager { db, new RecoverySessionStorage(), Executors.newSingleThreadExecutor(), ListenersStorage.getInstance()); ListenersStorage.getInstance(), new RecoverySnapshotStorage()); } return mInstance; } Loading @@ -100,12 +103,14 @@ public class RecoverableKeyStoreManager { RecoverableKeyStoreDb recoverableKeyStoreDb, RecoverySessionStorage recoverySessionStorage, ExecutorService executorService, ListenersStorage listenersStorage) { ListenersStorage listenersStorage, RecoverySnapshotStorage snapshotStorage) { mContext = context; mDatabase = recoverableKeyStoreDb; mRecoverySessionStorage = recoverySessionStorage; mExecutorService = executorService; mListenersStorage = listenersStorage; mSnapshotStorage = snapshotStorage; try { mRecoverableKeyGenerator = RecoverableKeyGenerator.newInstance(mDatabase); } catch (NoSuchAlgorithmException e) { Loading Loading @@ -143,24 +148,12 @@ public class RecoverableKeyStoreManager { public @NonNull KeyStoreRecoveryData getRecoveryData(@NonNull byte[] account, int userId) throws RemoteException { checkRecoverKeyStorePermission(); final int callingUid = Binder.getCallingUid(); // Recovery agent uid. final int callingUserId = UserHandle.getCallingUserId(); final long callingIdentiy = Binder.clearCallingIdentity(); try { // TODO: Return the latest snapshot for the calling recovery agent. } finally { Binder.restoreCallingIdentity(callingIdentiy); } // KeyStoreRecoveryData without application keys and empty recovery blob. KeyStoreRecoveryData recoveryData = new KeyStoreRecoveryData( /*snapshotVersion=*/ 1, new ArrayList<KeyStoreRecoveryMetadata>(), new ArrayList<KeyEntryRecoveryData>(), /*encryptedRecoveryKeyBlob=*/ new byte[] {}); throw new ServiceSpecificException( RecoverableKeyStoreLoader.UNINITIALIZED_RECOVERY_PUBLIC_KEY); KeyStoreRecoveryData snapshot = mSnapshotStorage.get(UserHandle.getCallingUserId()); if (snapshot == null) { throw new ServiceSpecificException(RecoverableKeyStoreLoader.NO_SNAPSHOT_PENDING_ERROR); } return snapshot; } public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent, int userId) Loading Loading @@ -480,7 +473,7 @@ public class RecoverableKeyStoreManager { // So as not to block the critical path unlocking the phone, defer to another thread. try { mExecutorService.execute(KeySyncTask.newInstance( mContext, mDatabase, userId, storedHashType, credential)); mContext, mDatabase, mSnapshotStorage, userId, storedHashType, credential)); } catch (NoSuchAlgorithmException e) { Log.wtf(TAG, "Should never happen - algorithm unavailable for KeySync", e); } catch (KeyStoreException e) { Loading
services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java +16 −0 Original line number Diff line number Diff line /* * Copyright (C) 2017 The Android Open Source Project * * 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.android.server.locksettings.recoverablekeystore.storage; import android.content.Context; Loading
services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java 0 → 100644 +60 −0 Original line number Diff line number Diff line /* * Copyright (C) 2017 The Android Open Source Project * * 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.android.server.locksettings.recoverablekeystore.storage; import android.annotation.Nullable; import android.security.recoverablekeystore.KeyStoreRecoveryData; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; /** * In-memory storage for recovery snapshots. * * <p>Recovery snapshots are generated after a successful screen unlock. They are only generated if * the recoverable keystore has been mutated since the previous snapshot. This class stores only the * latest snapshot for each user. * * <p>This class is thread-safe. It is used both on the service thread and the * {@link com.android.server.locksettings.recoverablekeystore.KeySyncTask} thread. */ public class RecoverySnapshotStorage { @GuardedBy("this") private final SparseArray<KeyStoreRecoveryData> mSnapshotByUserId = new SparseArray<>(); /** * Sets the latest {@code snapshot} for the user {@code userId}. */ public synchronized void put(int userId, KeyStoreRecoveryData snapshot) { mSnapshotByUserId.put(userId, snapshot); } /** * Returns the latest snapshot for user {@code userId}, or null if none exists. */ @Nullable public synchronized KeyStoreRecoveryData get(int userId) { return mSnapshotByUserId.get(userId); } /** * Removes any (if any) snapshot associated with user {@code userId}. */ public synchronized void remove(int userId) { mSnapshotByUserId.remove(userId); } }