diff --git a/examples/StoreEncryptionPassword/.gitignore b/examples/StoreEncryptionPassword/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/examples/StoreEncryptionPassword/.gitignore @@ -0,0 +1 @@ +/build diff --git a/examples/StoreEncryptionPassword/build.gradle b/examples/StoreEncryptionPassword/build.gradle new file mode 100644 index 0000000000..f620101080 --- /dev/null +++ b/examples/StoreEncryptionPassword/build.gradle @@ -0,0 +1,30 @@ +apply plugin: 'com.android.application' +apply plugin: 'realm-android' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.3" + + defaultConfig { + applicationId "realm.io.storeencryptionpassword" + minSdkVersion 23 + targetSdkVersion 23 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + testCompile 'junit:junit:4.12' + //noinspection GradleDependency + compile 'com.android.support:appcompat-v7:23.1.1' + //noinspection GradleDependency + compile 'com.android.support:design:23.1.1' +} diff --git a/examples/StoreEncryptionPassword/lint.xml b/examples/StoreEncryptionPassword/lint.xml new file mode 100644 index 0000000000..a1b63af0fd --- /dev/null +++ b/examples/StoreEncryptionPassword/lint.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/examples/StoreEncryptionPassword/proguard-rules.pro b/examples/StoreEncryptionPassword/proguard-rules.pro new file mode 100644 index 0000000000..740907a636 --- /dev/null +++ b/examples/StoreEncryptionPassword/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/Nabil/Library/Android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/examples/StoreEncryptionPassword/src/androidTest/java/realm/io/storeencryptionpassword/ApplicationTest.java b/examples/StoreEncryptionPassword/src/androidTest/java/realm/io/storeencryptionpassword/ApplicationTest.java new file mode 100644 index 0000000000..d8b6c593e0 --- /dev/null +++ b/examples/StoreEncryptionPassword/src/androidTest/java/realm/io/storeencryptionpassword/ApplicationTest.java @@ -0,0 +1,13 @@ +package realm.io.storeencryptionpassword; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/examples/StoreEncryptionPassword/src/main/AndroidManifest.xml b/examples/StoreEncryptionPassword/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..7074c0c425 --- /dev/null +++ b/examples/StoreEncryptionPassword/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + diff --git a/examples/StoreEncryptionPassword/src/main/java/realm/io/storeencryptionpassword/MainActivity.java b/examples/StoreEncryptionPassword/src/main/java/realm/io/storeencryptionpassword/MainActivity.java new file mode 100644 index 0000000000..2a5e87c890 --- /dev/null +++ b/examples/StoreEncryptionPassword/src/main/java/realm/io/storeencryptionpassword/MainActivity.java @@ -0,0 +1,119 @@ +package realm.io.storeencryptionpassword; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.View; +import android.widget.Button; +import android.widget.Toast; + +import java.util.Arrays; + +import io.realm.Realm; +import io.realm.RealmConfiguration; + +public class MainActivity extends AppCompatActivity { + public static final int REQ_UNLOCK = 1; + + private Button mBtnUnlock; + private Button mBtnLock; + private View mBtnOpen; + + private final Store store = new Store(this); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + mBtnUnlock = (Button) findViewById(R.id.btnUnLock); + mBtnLock = (Button) findViewById(R.id.btnLock); + + //TODO use this one RealmConfiguration realmConfig = new RealmConfiguration.Builder(this).encryptionKey(realmKey).build(); + RealmConfiguration realmConfig = new RealmConfiguration.Builder(MainActivity.this).build(); + Realm.setDefaultConfiguration(realmConfig); + + mBtnUnlock.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + store.unlockKeyStore(REQ_UNLOCK); + } + }); + + mBtnLock.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mBtnLock.setEnabled(false); + Toast.makeText(MainActivity.this, "Locking ...", Toast.LENGTH_SHORT).show(); + Realm.setDefaultConfiguration(new RealmConfiguration.Builder(MainActivity.this).build()); + mBtnUnlock.setEnabled(true); + } + }); + + //noinspection ConstantConditions + mBtnOpen = findViewById(R.id.btnOpenList); + mBtnOpen.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + goToList(); + } + }); + + mBtnOpen.setEnabled(store.getEncryptedRealmKey() != null && store.containsEncryptionKey()); + + mBtnUnlock.setEnabled(true); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case REQ_UNLOCK: + if (store.onUnlockKeyStoreResult(resultCode, data)) { + onKeystoreUnlocked(); + } + break; + default: + super.onActivityResult(requestCode, resultCode, data); + break; + } + } + + private void onKeystoreUnlocked() { + byte[] encryptedRealmKey = store.getEncryptedRealmKey(); + if (encryptedRealmKey == null || !store.containsEncryptionKey()) { + final byte[] realmKey = store.generateKeyForRealm(); + store.generateKeyInKeystore(); + encryptedRealmKey = store.encryptAndSaveKeyForRealm(realmKey); + + final RealmConfiguration realmConfig = new RealmConfiguration.Builder(MainActivity.this) + .encryptionKey(realmKey) + .build(); + Arrays.fill(realmKey, (byte) 0); + + // create encrypted Realm + Realm.deleteRealm(realmConfig); + Realm.getInstance(realmConfig).close(); + + mBtnOpen.setEnabled(true); + } + + Toast.makeText(MainActivity.this, "Unlocking ...", Toast.LENGTH_SHORT).show(); + mBtnUnlock.setEnabled(false); + + final byte[] realmKey = store.decryptKeyForRealm(encryptedRealmKey); + + RealmConfiguration realmConfig = new RealmConfiguration.Builder(MainActivity.this).encryptionKey(realmKey).build(); + Realm.setDefaultConfiguration(realmConfig); + + mBtnLock.setEnabled(true); + } + + private void goToList () { + //start Todo list + Intent intent = new Intent(MainActivity.this, SecretTodoList.class); + startActivity(intent); + } +} diff --git a/examples/StoreEncryptionPassword/src/main/java/realm/io/storeencryptionpassword/MyAdapter.java b/examples/StoreEncryptionPassword/src/main/java/realm/io/storeencryptionpassword/MyAdapter.java new file mode 100644 index 0000000000..dcfa1211f1 --- /dev/null +++ b/examples/StoreEncryptionPassword/src/main/java/realm/io/storeencryptionpassword/MyAdapter.java @@ -0,0 +1,43 @@ +package realm.io.storeencryptionpassword; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListAdapter; +import android.widget.TextView; + +import io.realm.OrderedRealmCollection; +import io.realm.RealmBaseAdapter; +import realm.io.storeencryptionpassword.model.TodoItem; + +class MyAdapter extends RealmBaseAdapter implements ListAdapter { + + private static class ViewHolder { + TextView name; + } + + public MyAdapter(Context context, OrderedRealmCollection realmResults) { + super(context, realmResults, true); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + ViewHolder viewHolder; + if (convertView == null) { + convertView = inflater.inflate(android.R.layout.simple_list_item_1, parent, false); + viewHolder = new ViewHolder(); + viewHolder.name = (TextView) convertView.findViewById(android.R.id.text1); + convertView.setTag(viewHolder); + } else { + viewHolder = (ViewHolder) convertView.getTag(); + } + + TodoItem item = adapterData.get(position); + viewHolder.name.setText(item.getName()); + return convertView; + } + + public OrderedRealmCollection getAdapterData() { + return adapterData; + } +} diff --git a/examples/StoreEncryptionPassword/src/main/java/realm/io/storeencryptionpassword/SecretTodoList.java b/examples/StoreEncryptionPassword/src/main/java/realm/io/storeencryptionpassword/SecretTodoList.java new file mode 100644 index 0000000000..8d4c58010e --- /dev/null +++ b/examples/StoreEncryptionPassword/src/main/java/realm/io/storeencryptionpassword/SecretTodoList.java @@ -0,0 +1,79 @@ +package realm.io.storeencryptionpassword; + +import android.app.ListActivity; +import android.content.Intent; +import android.os.Bundle; +import android.os.Message; +import android.support.design.widget.FloatingActionButton; +import android.support.design.widget.Snackbar; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Toast; + +import java.util.Random; + +import io.realm.Realm; +import io.realm.RealmResults; +import realm.io.storeencryptionpassword.model.TodoItem; + +public class SecretTodoList extends ListActivity { + + private Realm realm; + private Random random; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + random = new Random(); + + realm = getRealm(); + if (realm == null) { + finish(); + return; + } + RealmResults todos = realm.where(TodoItem.class).findAll(); + setContentView(R.layout.secret_todo); + final MyAdapter adapter = new MyAdapter(this, todos); + getListView().setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { + @Override + public boolean onItemLongClick(AdapterView adapterView, View view, int i, long l) { + TodoItem timeStamp = adapter.getAdapterData().get(i); + realm.beginTransaction(); + timeStamp.deleteFromRealm(); + realm.commitTransaction(); + return true; + } + }); + + FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); + fab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + realm.beginTransaction(); + TodoItem todoItem = realm.createObject(TodoItem.class); + todoItem.setName("Item " + random.nextInt()); + realm.commitTransaction(); + } + }); + + setListAdapter(adapter); + } + + private Realm getRealm() { + try { + return Realm.getDefaultInstance(); + } catch (IllegalArgumentException e) { + Toast.makeText(this, "Please unlock Realm first.", Toast.LENGTH_SHORT).show(); + return null; + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (realm != null) { + realm.close(); + } + } +} diff --git a/examples/StoreEncryptionPassword/src/main/java/realm/io/storeencryptionpassword/SharedPrefUtils.java b/examples/StoreEncryptionPassword/src/main/java/realm/io/storeencryptionpassword/SharedPrefUtils.java new file mode 100644 index 0000000000..c2114fc504 --- /dev/null +++ b/examples/StoreEncryptionPassword/src/main/java/realm/io/storeencryptionpassword/SharedPrefUtils.java @@ -0,0 +1,45 @@ +package realm.io.storeencryptionpassword; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Base64; + +public final class SharedPrefUtils { + private static final String PREF_NAME = "realm_key"; + private static final String KEY = "iv_and_encrypted_key"; + + public static void save(Context context, byte[] ivAndEncryptedKey) { + getPreference(context).edit() + .putString(KEY, encode(ivAndEncryptedKey)) + .apply(); + } + + public static byte[] load(Context context) { + final SharedPreferences pref = getPreference(context); + + final String ivAndEncryptedKey = pref.getString(KEY, null); + if (ivAndEncryptedKey == null) { + return null; + } + + return decode(ivAndEncryptedKey); + } + + private static String encode(byte[] data) { + if (data == null) { + return null; + } + return Base64.encodeToString(data, Base64.NO_WRAP); + } + + private static byte[] decode(String encodedData) { + if (encodedData == null) { + return null; + } + return Base64.decode(encodedData, Base64.DEFAULT); + } + + private static SharedPreferences getPreference(Context context) { + return context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + } +} diff --git a/examples/StoreEncryptionPassword/src/main/java/realm/io/storeencryptionpassword/Store.java b/examples/StoreEncryptionPassword/src/main/java/realm/io/storeencryptionpassword/Store.java new file mode 100644 index 0000000000..3117ed7155 --- /dev/null +++ b/examples/StoreEncryptionPassword/src/main/java/realm/io/storeencryptionpassword/Store.java @@ -0,0 +1,187 @@ +package realm.io.storeencryptionpassword; + +import android.app.Activity; +import android.app.KeyguardManager; +import android.content.Context; +import android.content.Intent; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; + +import io.realm.RealmConfiguration; + +public class Store { + private static final String KEYSTORE_PROVIDER_NAME = "AndroidKeyStore"; + private static final String KEY_ALIAS = "realm_key"; + private static final String TRANSFORMATION = KeyProperties.KEY_ALGORITHM_AES + + "/" + KeyProperties.BLOCK_MODE_CBC + + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7; + private static final int AUTH_VALID_DURATION_IN_SECOND = 30; + + private static final ByteOrder ORDER_FOR_ENCRYPTED_DATA = ByteOrder.BIG_ENDIAN; + + private final Activity context; + private final SecureRandom rng = new SecureRandom(); + private final KeyStore keyStore = prepareKeyStore(); + + public Store(Activity context) { + this.context = context; + } + + public boolean containsEncryptionKey() { + try { + return keyStore.containsAlias(KEY_ALIAS); + } catch (KeyStoreException e) { + throw new RuntimeException(e); + } + } + + public void unlockKeyStore(int requestCode) { + final Intent intent = getKeyguardManager().createConfirmDeviceCredentialIntent("Android Keystore System", + "unlock keystore to decrypt Realm database."); + context.startActivityForResult(intent, requestCode); + } + + public boolean onUnlockKeyStoreResult(int result, Intent data) { + return result == Activity.RESULT_OK; + } + + public byte[] generateKeyForRealm() { + final byte[] keyForRealm = new byte[RealmConfiguration.KEY_LENGTH]; + rng.nextBytes(keyForRealm); + return keyForRealm; + } + + public void generateKeyInKeystore() { + final KeyGenerator keyGenerator; + try { + keyGenerator = KeyGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_AES, + KEYSTORE_PROVIDER_NAME); + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { + throw new RuntimeException(e); + } + + final KeyGenParameterSpec keySpec = new KeyGenParameterSpec.Builder( + KEY_ALIAS, + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_CBC) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) + .setUserAuthenticationRequired(true) + .setUserAuthenticationValidityDurationSeconds( + AUTH_VALID_DURATION_IN_SECOND) + .build(); + try { + keyGenerator.init(keySpec); + } catch (InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); + } + keyGenerator.generateKey(); + + } + + public byte[] encryptAndSaveKeyForRealm(byte[] keyForRealm) { + final KeyStore ks = prepareKeyStore(); + final Cipher cipher = prepareCipher(); + + final byte[] iv; + final byte[] encryptedKeyForRealm; + try { + final SecretKey key = (SecretKey) ks.getKey(KEY_ALIAS, null); + cipher.init(Cipher.ENCRYPT_MODE, key); + + encryptedKeyForRealm = cipher.doFinal(keyForRealm); + iv = cipher.getIV(); + } catch (InvalidKeyException | UnrecoverableKeyException | NoSuchAlgorithmException + | KeyStoreException | BadPaddingException | IllegalBlockSizeException e) { + throw new RuntimeException("key for encryption is invalid", e); + } + final byte[] ivAndEncryptedKey = new byte[Integer.BYTES + iv.length + encryptedKeyForRealm.length]; + + final ByteBuffer buffer = ByteBuffer.wrap(ivAndEncryptedKey); + buffer.order(ORDER_FOR_ENCRYPTED_DATA); + buffer.putInt(iv.length); + buffer.put(iv); + buffer.put(encryptedKeyForRealm); + + SharedPrefUtils.save(context, ivAndEncryptedKey); + + return ivAndEncryptedKey; + } + + public byte[] decryptKeyForRealm(byte[] ivAndEncryptedKey) { + final Cipher cipher = prepareCipher(); + final KeyStore keyStore = prepareKeyStore(); + + final ByteBuffer buffer = ByteBuffer.wrap(ivAndEncryptedKey); + buffer.order(ORDER_FOR_ENCRYPTED_DATA); + + final int ivLength = buffer.getInt(); + final byte[] iv = new byte[ivLength]; + final byte[] encryptedKey = new byte[ivAndEncryptedKey.length - Integer.BYTES - ivLength]; + + buffer.get(iv); + buffer.get(encryptedKey); + + try { + final SecretKey key = (SecretKey) keyStore.getKey(KEY_ALIAS, null); + final IvParameterSpec ivSpec = new IvParameterSpec(iv); + cipher.init(Cipher.DECRYPT_MODE, key, ivSpec); + + return cipher.doFinal(encryptedKey); + } catch (InvalidKeyException e) { + throw new RuntimeException("key is invalid."); + } catch (UnrecoverableKeyException | NoSuchAlgorithmException | BadPaddingException + | KeyStoreException | IllegalBlockSizeException | InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); + } + } + + public byte[] getEncryptedRealmKey() { + return SharedPrefUtils.load(context); + } + + private KeyStore prepareKeyStore() { + try { + KeyStore ks = KeyStore.getInstance(KEYSTORE_PROVIDER_NAME); + ks.load(null); + return ks; + } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) { + throw new RuntimeException(e); + } + } + + private Cipher prepareCipher() { + final Cipher cipher; + try { + cipher = Cipher.getInstance(TRANSFORMATION); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new RuntimeException(e); + } + return cipher; + } + + private KeyguardManager getKeyguardManager() { + return (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); + } +} diff --git a/examples/StoreEncryptionPassword/src/main/java/realm/io/storeencryptionpassword/model/TodoItem.java b/examples/StoreEncryptionPassword/src/main/java/realm/io/storeencryptionpassword/model/TodoItem.java new file mode 100644 index 0000000000..5bc86a86da --- /dev/null +++ b/examples/StoreEncryptionPassword/src/main/java/realm/io/storeencryptionpassword/model/TodoItem.java @@ -0,0 +1,25 @@ +package realm.io.storeencryptionpassword.model; +import io.realm.RealmObject; +/** + * Created by Nabil on 12/04/2016. + */ +public class TodoItem extends RealmObject { + private String name; + private boolean isDone; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isDone() { + return isDone; + } + + public void setIsDone(boolean isDone) { + this.isDone = isDone; + } +} diff --git a/examples/StoreEncryptionPassword/src/main/res/layout/activity_main.xml b/examples/StoreEncryptionPassword/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000000..1063c5204d --- /dev/null +++ b/examples/StoreEncryptionPassword/src/main/res/layout/activity_main.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/examples/StoreEncryptionPassword/src/main/res/layout/content_main.xml b/examples/StoreEncryptionPassword/src/main/res/layout/content_main.xml new file mode 100644 index 0000000000..ab44a26018 --- /dev/null +++ b/examples/StoreEncryptionPassword/src/main/res/layout/content_main.xml @@ -0,0 +1,40 @@ + + + +