mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 18:30:20 +01:00
Add an encrypted key-value store.
SignalStore is backed by SQLCipher and is intended to be used instead of TextSecurePreferences moving forward.
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
package org.thoughtcrime.securesms.keyvalue;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class KeyValueDataSet implements KeyValueReader {
|
||||
private final Map<String, Object> values = new HashMap<>();
|
||||
private final Map<String, Class> types = new HashMap<>();
|
||||
|
||||
public void putBlob(@NonNull String key, byte[] value) {
|
||||
values.put(key, value);
|
||||
types.put(key, byte[].class);
|
||||
}
|
||||
|
||||
public void putBoolean(@NonNull String key, boolean value) {
|
||||
values.put(key, value);
|
||||
types.put(key, Boolean.class);
|
||||
}
|
||||
|
||||
public void putFloat(@NonNull String key, float value) {
|
||||
values.put(key, value);
|
||||
types.put(key, Float.class);
|
||||
}
|
||||
|
||||
public void putInteger(@NonNull String key, int value) {
|
||||
values.put(key, value);
|
||||
types.put(key, Integer.class);
|
||||
}
|
||||
|
||||
public void putLong(@NonNull String key, long value) {
|
||||
values.put(key, value);
|
||||
types.put(key, Long.class);
|
||||
}
|
||||
|
||||
public void putString(@NonNull String key, String value) {
|
||||
values.put(key, value);
|
||||
types.put(key, String.class);
|
||||
}
|
||||
|
||||
void putAll(@NonNull KeyValueDataSet other) {
|
||||
values.putAll(other.values);
|
||||
types.putAll(other.types);
|
||||
}
|
||||
|
||||
void removeAll(@NonNull Collection<String> removes) {
|
||||
for (String remove : removes) {
|
||||
values.remove(remove);
|
||||
types.remove(remove);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getBlob(@NonNull String key, byte[] defaultValue) {
|
||||
if (containsKey(key)) {
|
||||
return readValueAsType(key, byte[].class, true);
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getBoolean(@NonNull String key, boolean defaultValue) {
|
||||
if (containsKey(key)) {
|
||||
return readValueAsType(key, Boolean.class, false);
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getFloat(@NonNull String key, float defaultValue) {
|
||||
if (containsKey(key)) {
|
||||
return readValueAsType(key, Float.class, false);
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInteger(@NonNull String key, int defaultValue) {
|
||||
if (containsKey(key)) {
|
||||
return readValueAsType(key, Integer.class, false);
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLong(@NonNull String key, long defaultValue) {
|
||||
if (containsKey(key)) {
|
||||
return readValueAsType(key, Long.class, false);
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(@NonNull String key, String defaultValue) {
|
||||
if (containsKey(key)) {
|
||||
return readValueAsType(key, String.class, true);
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull Map<String, Object> getValues() {
|
||||
return values;
|
||||
}
|
||||
|
||||
public Class getType(@NonNull String key) {
|
||||
return types.get(key);
|
||||
}
|
||||
|
||||
public boolean containsKey(@NonNull String key) {
|
||||
return values.containsKey(key);
|
||||
}
|
||||
|
||||
private <E> E readValueAsType(@NonNull String key, Class<E> type, boolean nullable) {
|
||||
Object value = values.get(key);
|
||||
if ((value == null && nullable) || (value != null && value.getClass() == type)) {
|
||||
return type.cast(value);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Type mismatch!");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.thoughtcrime.securesms.keyvalue;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
interface KeyValueReader {
|
||||
byte[] getBlob(@NonNull String key, byte[] defaultValue);
|
||||
boolean getBoolean(@NonNull String key, boolean defaultValue);
|
||||
float getFloat(@NonNull String key, float defaultValue);
|
||||
int getInteger(@NonNull String key, int defaultValue);
|
||||
long getLong(@NonNull String key, long defaultValue);
|
||||
String getString(@NonNull String key, String defaultValue);
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package org.thoughtcrime.securesms.keyvalue;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.KeyValueDatabase;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.logging.SignalUncaughtExceptionHandler;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
/**
|
||||
* An replacement for {@link android.content.SharedPreferences} that stores key-value pairs in our
|
||||
* encrypted database.
|
||||
*
|
||||
* Implemented as a write-through cache that is safe to read and write to on the main thread.
|
||||
*
|
||||
* Writes are enqueued on a separate executor, but writes are finished up in
|
||||
* {@link SignalUncaughtExceptionHandler}, meaning all write should finish barring a native crash
|
||||
* or the system killing us unexpectedly (i.e. a force-stop).
|
||||
*/
|
||||
public final class KeyValueStore implements KeyValueReader {
|
||||
|
||||
private static final String TAG = Log.tag(KeyValueStore.class);
|
||||
|
||||
private final ExecutorService executor;
|
||||
private final KeyValueDatabase database;
|
||||
|
||||
private KeyValueDataSet dataSet;
|
||||
|
||||
public KeyValueStore(@NonNull Context context) {
|
||||
this.executor = SignalExecutors.newCachedSingleThreadExecutor("signal-KeyValueStore");
|
||||
this.database = DatabaseFactory.getKeyValueDatabase(context);
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
@Override
|
||||
public synchronized byte[] getBlob(@NonNull String key, byte[] defaultValue) {
|
||||
initializeIfNecessary();
|
||||
return dataSet.getBlob(key, defaultValue);
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
@Override
|
||||
public synchronized boolean getBoolean(@NonNull String key, boolean defaultValue) {
|
||||
initializeIfNecessary();
|
||||
return dataSet.getBoolean(key, defaultValue);
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
@Override
|
||||
public synchronized float getFloat(@NonNull String key, float defaultValue) {
|
||||
initializeIfNecessary();
|
||||
return dataSet.getFloat(key, defaultValue);
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
@Override
|
||||
public synchronized int getInteger(@NonNull String key, int defaultValue) {
|
||||
initializeIfNecessary();
|
||||
return dataSet.getInteger(key, defaultValue);
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
@Override
|
||||
public synchronized long getLong(@NonNull String key, long defaultValue) {
|
||||
initializeIfNecessary();
|
||||
return dataSet.getLong(key, defaultValue);
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
@Override
|
||||
public synchronized String getString(@NonNull String key, String defaultValue) {
|
||||
initializeIfNecessary();
|
||||
return dataSet.getString(key, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A writer that allows writing and removing multiple entries in a single atomic
|
||||
* transaction.
|
||||
*/
|
||||
@AnyThread
|
||||
@NonNull Writer beginWrite() {
|
||||
return new Writer();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A reader that lets you read from an immutable snapshot of the store, ensuring that data
|
||||
* is consistent between reads. If you're only reading a single value, it is more
|
||||
* efficient to use the various get* methods instead.
|
||||
*/
|
||||
@AnyThread
|
||||
synchronized @NonNull KeyValueReader beginRead() {
|
||||
initializeIfNecessary();
|
||||
|
||||
KeyValueDataSet copy = new KeyValueDataSet();
|
||||
copy.putAll(dataSet);
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that any pending writes (such as those made via {@link Writer#apply()}) are finished.
|
||||
*/
|
||||
@AnyThread
|
||||
synchronized void blockUntilAllWritesFinished() {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
executor.execute(latch::countDown);
|
||||
|
||||
try {
|
||||
latch.await();
|
||||
} catch (InterruptedException e) {
|
||||
Log.w(TAG, "Failed to wait for all writes.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private synchronized void write(@NonNull KeyValueDataSet newDataSet, @NonNull Collection<String> removes) {
|
||||
initializeIfNecessary();
|
||||
|
||||
dataSet.putAll(newDataSet);
|
||||
dataSet.removeAll(removes);
|
||||
|
||||
executor.execute(() -> database.writeDataSet(newDataSet, removes));
|
||||
}
|
||||
|
||||
private void initializeIfNecessary() {
|
||||
if (dataSet != null) return;
|
||||
this.dataSet = database.getDataSet();
|
||||
}
|
||||
|
||||
class Writer {
|
||||
private final KeyValueDataSet dataSet = new KeyValueDataSet();
|
||||
private final Set<String> removes = new HashSet<>();
|
||||
|
||||
@NonNull Writer putBlob(@NonNull String key, @Nullable byte[] value) {
|
||||
dataSet.putBlob(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull Writer putBoolean(@NonNull String key, boolean value) {
|
||||
dataSet.putBoolean(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull Writer putFloat(@NonNull String key, float value) {
|
||||
dataSet.putFloat(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull Writer putInteger(@NonNull String key, int value) {
|
||||
dataSet.putInteger(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull Writer putLong(@NonNull String key, long value) {
|
||||
dataSet.putLong(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull Writer putString(@NonNull String key, String value) {
|
||||
dataSet.putString(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull Writer remove(@NonNull String key) {
|
||||
removes.add(key);
|
||||
return this;
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
void apply() {
|
||||
for (String key : removes) {
|
||||
if (dataSet.containsKey(key)) {
|
||||
throw new IllegalStateException("Tried to remove a key while also setting it!");
|
||||
}
|
||||
}
|
||||
|
||||
write(dataSet, removes);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
void commit() {
|
||||
apply();
|
||||
blockUntilAllWritesFinished();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.thoughtcrime.securesms.keyvalue;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.SignalUncaughtExceptionHandler;
|
||||
|
||||
/**
|
||||
* Simple, encrypted key-value store.
|
||||
*/
|
||||
public final class SignalStore {
|
||||
|
||||
private SignalStore() {}
|
||||
|
||||
/**
|
||||
* Ensures any pending writes are finished. Only intended to be called by
|
||||
* {@link SignalUncaughtExceptionHandler}.
|
||||
*/
|
||||
public static void blockUntilAllWritesFinished() {
|
||||
getStore().blockUntilAllWritesFinished();
|
||||
}
|
||||
|
||||
private static @NonNull KeyValueStore getStore() {
|
||||
return ApplicationDependencies.getKeyValueStore();
|
||||
}
|
||||
|
||||
private static void putBlob(@NonNull String key, byte[] value) {
|
||||
getStore().beginWrite().putBlob(key, value).apply();
|
||||
}
|
||||
|
||||
private static void putBoolean(@NonNull String key, boolean value) {
|
||||
getStore().beginWrite().putBoolean(key, value).apply();
|
||||
}
|
||||
|
||||
private static void putFloat(@NonNull String key, float value) {
|
||||
getStore().beginWrite().putFloat(key, value).apply();
|
||||
}
|
||||
|
||||
private static void putInteger(@NonNull String key, int value) {
|
||||
getStore().beginWrite().putInteger(key, value).apply();
|
||||
}
|
||||
|
||||
private static void putLong(@NonNull String key, long value) {
|
||||
getStore().beginWrite().putLong(key, value).apply();
|
||||
}
|
||||
|
||||
private static void putString(@NonNull String key, String value) {
|
||||
getStore().beginWrite().putString(key, value).apply();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user