Skip to content
This repository was archived by the owner on Nov 7, 2023. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,30 @@ async function removeUserSession() {
}
```

### Storage options

You can pass a set of **options** as the previous to last parameter of `setItem`, `getItem`, `removeItem` or `clear` functions:

```js
await EncryptedStorage.removeItem('user_session', {
storageName: 'userStorage',
});
```

The following options are supported:

- `keychainAccessibility` (**iOS only**)

Control item availability relative to the lock state of the device. If the attribute ends with the string `ThisDeviceOnly`, the item can be restored to the same device that created a backup, but it isn’t migrated when restoring another device’s backup data. [Read more](https://developer.apple.com/documentation/security/keychain_services/keychain_items/restricting_keychain_item_accessibility?language=objc)

Default value: `kSecAttrAccessibleAfterFirstUnlock`

- `storageName`

A string for identifying a set of storage items. Should not contain path separators. Uses [kSecAttrService](https://developer.apple.com/documentation/security/ksecattrservice?language=objc) on iOS and [fileName](https://developer.android.com/reference/kotlin/androidx/security/crypto/EncryptedSharedPreferences?hl=en#create) on Android.

Default value: App's bundle id

## Note regarding `Keychain` persistence

You'll notice that the iOS `Keychain` is not cleared when your app is uninstalled, this is the expected behaviour. However, if you do want to achieve a different behaviour, you can use the below snippet to clear the `Keychain` on the first launch of your app.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.emeraldsanto.encryptedstorage;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.bridge.ReadableType;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

class MockReadableMap implements ReadableMap {
@Override
public boolean getBoolean(@NonNull String name) {
return false;
}

@Override
public double getDouble(@NonNull String name) {
return 0;
}

@Override
public int getInt(@NonNull String name) {
return 0;
}

@Override
public boolean hasKey(@NonNull String name) {
return name.equals("storageName");
}

@Override
public boolean isNull(@NonNull String name) {
return false;
}

@Nullable
@Override
public String getString(@NonNull String name) {
if (name.equals("storageName")) {
return "mock.storage.name";
}
return null;
}

@Nullable
@Override
public ReadableArray getArray(@NonNull String name) {
return null;
}

@Nullable
@Override
public ReadableMap getMap(@NonNull String name) {
return null;
}

@NonNull
@Override
public Dynamic getDynamic(@NonNull String name) {
return null;
}

@NonNull
@Override
public ReadableType getType(@NonNull String name) {
return null;
}

@NonNull
@Override
public Iterator<Map.Entry<String, Object>> getEntryIterator() {
return null;
}

@NonNull
@Override
public ReadableMapKeySetIterator keySetIterator() {
return null;
}

@NonNull
@Override
public HashMap<String, Object> toHashMap() {
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;

import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableMap;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
Expand All @@ -14,76 +17,77 @@
@RunWith(AndroidJUnit4.class)
public class RNEncryptedStorageModuleUnitTest {
private RNEncryptedStorageModule module;
private final ReadableMap options = new MockReadableMap();

@Before
public void setUp() {
module = new RNEncryptedStorageModule(new ReactApplicationContext(InstrumentationRegistry.getInstrumentation().getTargetContext()));
module.clear(mock(Promise.class));
module.clear(options, mock(Promise.class));
}

@Test
public void shouldGetAndSet() {
Promise promise1 = mock(Promise.class);
module.getItem("test", promise1);
module.getItem("test", options, promise1);
verify(promise1).resolve(null);

Promise promise2 = mock(Promise.class);
module.setItem("test", "asd", promise2);
module.setItem("test", "asd", options, promise2);
verify(promise2).resolve("asd");

Promise promise3 = mock(Promise.class);
module.getItem("test", promise3);
module.getItem("test", options, promise3);
verify(promise3).resolve("asd");
}

@Test
public void shouldRemove() {
Promise promise1 = mock(Promise.class);
module.setItem("test", "asd", promise1);
module.setItem("test", "asd", options, promise1);
verify(promise1).resolve("asd");

Promise promise2 = mock(Promise.class);
module.getItem("test", promise2);
module.getItem("test", options, promise2);
verify(promise2).resolve("asd");

Promise promise3 = mock(Promise.class);
module.removeItem("test", promise3);
module.removeItem("test", options, promise3);
verify(promise3).resolve("test");

Promise promise4 = mock(Promise.class);
module.getItem("test", promise4);
module.getItem("test", options, promise4);
verify(promise4).resolve(null);
}

@Test
public void shouldClear() {
Promise promise1 = mock(Promise.class);
module.setItem("test", "asd", promise1);
module.setItem("test", "asd", options, promise1);
verify(promise1).resolve("asd");

Promise promise2 = mock(Promise.class);
module.getItem("test", promise2);
module.getItem("test", options, promise2);
verify(promise2).resolve("asd");

Promise promise3 = mock(Promise.class);
module.clear(promise3);
module.clear(options, promise3);
verify(promise3).resolve(null);

Promise promise4 = mock(Promise.class);
module.getItem("test", promise4);
module.getItem("test", options, promise4);
verify(promise4).resolve(null);
}

@Test
public void shouldKeepValuesWhenRecreated() {
Promise promise1 = mock(Promise.class);
module.setItem("test", "asd", promise1);
module.setItem("test", "asd", options, promise1);
verify(promise1).resolve("asd");

module = new RNEncryptedStorageModule(new ReactApplicationContext(InstrumentationRegistry.getInstrumentation().getTargetContext()));

Promise promise2 = mock(Promise.class);
module.getItem("test", promise2);
module.getItem("test", options, promise2);
verify(promise2).resolve("asd");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,53 @@
import androidx.security.crypto.MasterKey;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;

import com.facebook.react.bridge.ReadableMap;

public class RNEncryptedStorageModule extends ReactContextBaseJavaModule {

private static final String NATIVE_MODULE_NAME = "RNEncryptedStorage";
private static final String SHARED_PREFERENCES_FILENAME = "RN_ENCRYPTED_STORAGE_SHARED_PREF";

private SharedPreferences sharedPreferences;
private MasterKey masterKey;

private String getStorageName(ReadableMap options) {
String bundleId = this.getReactApplicationContext().getPackageName();
String storageName = options.hasKey("storageName") ?
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are not setting any default value for options, nor checking if is it defined.

This line throws if called without an options, for example:

EncryptedStorage.getItem("myValue")

options.getString("storageName") : bundleId;
return storageName;
}

private void createSharedPreferences(ReadableMap options) {
ReactContext reactContext = this.getReactApplicationContext();
String storageName = this.getStorageName(options);

try {
this.sharedPreferences = EncryptedSharedPreferences.create(
reactContext,
storageName,
this.masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
);
} catch (Exception ex) {
Log.e(NATIVE_MODULE_NAME, "Failed to create encrypted shared preferences! Failing back to standard SharedPreferences", ex);
this.sharedPreferences = reactContext.getSharedPreferences(storageName, Context.MODE_PRIVATE);
}
}

public RNEncryptedStorageModule(ReactApplicationContext context) {
super(context);

try {
MasterKey key = new MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build();

this.sharedPreferences = EncryptedSharedPreferences.create(
context,
RNEncryptedStorageModule.SHARED_PREFERENCES_FILENAME,
key,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
);
}

catch (Exception ex) {
Log.e(NATIVE_MODULE_NAME, "Failed to create encrypted shared preferences! Failing back to standard SharedPreferences", ex);
this.sharedPreferences = context.getSharedPreferences(RNEncryptedStorageModule.SHARED_PREFERENCES_FILENAME, Context.MODE_PRIVATE);
this.masterKey = new MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build();
} catch (Exception ex) {
Log.e(NATIVE_MODULE_NAME, "Failed to create MasterKey", ex);
}
}

Expand All @@ -46,7 +63,9 @@ public String getName() {
}

@ReactMethod
public void setItem(String key, String value, Promise promise) {
public void setItem(String key, String value, ReadableMap options, Promise promise) {
this.createSharedPreferences(options);

if (this.sharedPreferences == null) {
promise.reject(new NullPointerException("Could not initialize SharedPreferences"));
return;
Expand All @@ -66,7 +85,9 @@ public void setItem(String key, String value, Promise promise) {
}

@ReactMethod
public void getItem(String key, Promise promise) {
public void getItem(String key, ReadableMap options, Promise promise) {
this.createSharedPreferences(options);

if (this.sharedPreferences == null) {
promise.reject(new NullPointerException("Could not initialize SharedPreferences"));
return;
Expand All @@ -78,7 +99,9 @@ public void getItem(String key, Promise promise) {
}

@ReactMethod
public void removeItem(String key, Promise promise) {
public void removeItem(String key, ReadableMap options, Promise promise) {
this.createSharedPreferences(options);

if (this.sharedPreferences == null) {
promise.reject(new NullPointerException("Could not initialize SharedPreferences"));
return;
Expand All @@ -98,7 +121,9 @@ public void removeItem(String key, Promise promise) {
}

@ReactMethod
public void clear(Promise promise) {
public void clear(ReadableMap options, Promise promise) {
this.createSharedPreferences(options);

if (this.sharedPreferences == null) {
promise.reject(new NullPointerException("Could not initialize SharedPreferences"));
return;
Expand Down
Loading