Encrypt a Realm - Java SDK
On this page
Overview
You can encrypt your realms to ensure that the data stored to disk can't be read outside of your application. You encrypt the realm file on disk with AES-256 + SHA-2 by supplying a 64-byte encryption key when opening a realm.
Realm transparently encrypts and decrypts data with standard AES-256 encryption using the first 256 bits of the given 512-bit encryption key. Realm uses the other 256 bits of the 512-bit encryption key to validate integrity using a hash-based message authentication code (HMAC).
Warning
Do not use cryptographically-weak hashes for realm encryption keys. For optimal security, we recommend generating random rather than derived encryption keys.
Considerations
The following are key impacts to consider when encrypting a realm.
Storing & Reusing Keys
You must pass the same encryption key to RealmConfiguration.Builder.encryptionKey() each time you open the realm. If you don't provide a key or specify the wrong key for an encrypted realm, the Realm SDK throws an error.
Apps should store the encryption key in the Android KeyStore so that other apps cannot read the key.
Performance Impact
Reads and writes on encrypted realms can be up to 10% slower than unencrypted realms.
Encryption and Atlas Device Sync
You can encrypt a synced realm.
Realm only encrypts the data on the device and stores the data unencrypted in your Atlas data source. Any users with authorized access to the Atlas data source can read the data, but the following still applies:
Users must have the correct read permissions to read the synced data.
Data stored in Atlas is always encrypted at a volume (disk) level.
The transfer between client and server is always fully encrypted.
You can also enable Customer Key Management to encrypt stored Atlas data using your cloud provider's key (e.g. AWS KMS, Azure Key Vault, Google Cloud KMS).
If you need unique keys for each user of your application, you can use an OAuth provider or use one of the Realm authentication providers and an authentication trigger to create a 64-bit key and store that key in a user object.
Accessing an Encrypted Realm from Multiple Processes
Changed in version 10.14.0.
Starting with Realm Java SDK version 10.14.0, Realm supports opening the same encrypted realm in multiple processes.
If your app uses Realm Java SDK version 10.14.0 or earlier, attempting to
open an encrypted realm from multiple processes throws this error:
Encrypted interprocess sharing is currently unsupported.
Example
The following steps describe the recommended way to use the Android KeyStore for encryption with Realm:
Generate an asymmetric RSA key that Android can securely store and retrieve using the Android KeyStore.
Note
Android Version M and Above: Keystore Security
Versions M and above require user PIN or fingerprint to unlock the KeyStore.
Generate a symmetric key (AES) you can use to encrypt the realm.
Encrypt the symmetric AES key using your private RSA key.
Store the encrypted AES key on filesystem (in a
SharedPreferences
, for example).
When you need to use your encrypted realm:
Retrieve your encrypted AES key.
Decrypt your encrypted AES key using the public RSA key.
Use the decrypted AES key in the
RealmConfiguration
to open the encrypted realm.
Tip
See also:
For an end-to-end example of storing and reusing encryption keys, see the store_password example project, which demonstrates the fingerprint API.
Generate and Store an Encryption Key
The following code demonstrates how to securely generate and store an encryption key for a realm:
// Create a key to encrypt a realm and save it securely in the keystore public byte[] getNewKey() { // open a connection to the android keystore KeyStore keyStore; try { keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) { Log.v("EXAMPLE", "Failed to open the keystore."); throw new RuntimeException(e); } // create a securely generated random asymmetric RSA key byte[] realmKey = new byte[Realm.ENCRYPTION_KEY_LENGTH]; new SecureRandom().nextBytes(realmKey); // create a cipher that uses AES encryption -- we'll use this to encrypt our key Cipher cipher; try { cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7); } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { Log.e("EXAMPLE", "Failed to create a cipher."); throw new RuntimeException(e); } // generate secret key KeyGenerator keyGenerator; try { keyGenerator = KeyGenerator.getInstance( KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); } catch (NoSuchAlgorithmException | NoSuchProviderException e) { Log.e("EXAMPLE", "Failed to access the key generator."); throw new RuntimeException(e); } KeyGenParameterSpec keySpec = new KeyGenParameterSpec.Builder( "realm_key", 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) { Log.e("EXAMPLE", "Failed to generate a secret key."); throw new RuntimeException(e); } keyGenerator.generateKey(); // access the generated key in the android keystore, then // use the cipher to create an encrypted version of the key byte[] initializationVector; byte[] encryptedKeyForRealm; try { SecretKey secretKey = (SecretKey) keyStore.getKey("realm_key", null); cipher.init(Cipher.ENCRYPT_MODE, secretKey); encryptedKeyForRealm = cipher.doFinal(realmKey); initializationVector = cipher.getIV(); } catch (InvalidKeyException | UnrecoverableKeyException | NoSuchAlgorithmException | KeyStoreException | BadPaddingException | IllegalBlockSizeException e) { Log.e("EXAMPLE", "Failed encrypting the key with the secret key."); throw new RuntimeException(e); } // keep the encrypted key in shared preferences // to persist it across application runs byte[] initializationVectorAndEncryptedKey = new byte[Integer.BYTES + initializationVector.length + encryptedKeyForRealm.length]; ByteBuffer buffer = ByteBuffer.wrap(initializationVectorAndEncryptedKey); buffer.order(ByteOrder.BIG_ENDIAN); buffer.putInt(initializationVector.length); buffer.put(initializationVector); buffer.put(encryptedKeyForRealm); activity.getSharedPreferences("realm_key", Context.MODE_PRIVATE).edit() .putString("iv_and_encrypted_key", Base64.encodeToString(initializationVectorAndEncryptedKey, Base64.NO_WRAP)) .apply(); return realmKey; // pass to a realm configuration via encryptionKey() }
// Create a key to encrypt a realm and save it securely in the keystore fun getNewKey(): ByteArray { // open a connection to the android keystore val keyStore: KeyStore try { keyStore = KeyStore.getInstance("AndroidKeyStore") keyStore.load(null) } catch (e: Exception) { Log.v("EXAMPLE", "Failed to open the keystore.") throw RuntimeException(e) } // create a securely generated random asymmetric RSA key val realmKey = ByteArray(Realm.ENCRYPTION_KEY_LENGTH) SecureRandom().nextBytes(realmKey) // create a cipher that uses AES encryption -- we'll use this to encrypt our key val cipher: Cipher cipher = try { Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7) } catch (e: Exception) { Log.e("EXAMPLE", "Failed to create a cipher.") throw RuntimeException(e) } // generate secret key val keyGenerator: KeyGenerator keyGenerator = try { KeyGenerator.getInstance( KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore") } catch (e: NoSuchAlgorithmException) { Log.e("EXAMPLE", "Failed to access the key generator.") throw RuntimeException(e) } val keySpec = KeyGenParameterSpec.Builder( "realm_key", KeyProperties.PURPOSE_ENCRYPT or 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 (e: InvalidAlgorithmParameterException) { Log.e("EXAMPLE", "Failed to generate a secret key.") throw RuntimeException(e) } keyGenerator.generateKey() // access the generated key in the android keystore, then // use the cipher to create an encrypted version of the key val initializationVector: ByteArray val encryptedKeyForRealm: ByteArray try { val secretKey = keyStore.getKey("realm_key", null) as SecretKey cipher.init(Cipher.ENCRYPT_MODE, secretKey) encryptedKeyForRealm = cipher.doFinal(realmKey) initializationVector = cipher.iv } catch (e: Exception) { Log.e("EXAMPLE", "Failed encrypting the key with the secret key.") throw RuntimeException(e) } // keep the encrypted key in shared preferences // to persist it across application runs val initializationVectorAndEncryptedKey = ByteArray(Integer.BYTES + initializationVector.size + encryptedKeyForRealm.size) val buffer = ByteBuffer.wrap(initializationVectorAndEncryptedKey) buffer.order(ByteOrder.BIG_ENDIAN) buffer.putInt(initializationVector.size) buffer.put(initializationVector) buffer.put(encryptedKeyForRealm) activity!!.getSharedPreferences("realm_key", Context.MODE_PRIVATE).edit() .putString("iv_and_encrypted_key", Base64.encodeToString(initializationVectorAndEncryptedKey, Base64.NO_WRAP)) .apply() return realmKey // pass to a realm configuration via encryptionKey() }
Access an Existing Encryption Key
The following code demonstrates how to access and decrypt a securely stored encryption key for a realm:
// Access the encrypted key in the keystore, decrypt it with the secret, // and use it to open and read from the realm again public byte[] getExistingKey() { // open a connection to the android keystore KeyStore keyStore; try { keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) { Log.e("EXAMPLE", "Failed to open the keystore."); throw new RuntimeException(e); } // access the encrypted key that's stored in shared preferences byte[] initializationVectorAndEncryptedKey = Base64.decode(activity .getSharedPreferences("realm_key", Context.MODE_PRIVATE) .getString("iv_and_encrypted_key", null), Base64.DEFAULT); ByteBuffer buffer = ByteBuffer.wrap(initializationVectorAndEncryptedKey); buffer.order(ByteOrder.BIG_ENDIAN); // extract the length of the initialization vector from the buffer int initializationVectorLength = buffer.getInt(); // extract the initialization vector based on that length byte[] initializationVector = new byte[initializationVectorLength]; buffer.get(initializationVector); // extract the encrypted key byte[] encryptedKey = new byte[initializationVectorAndEncryptedKey.length - Integer.BYTES - initializationVectorLength]; buffer.get(encryptedKey); // create a cipher that uses AES encryption to decrypt our key Cipher cipher; try { cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7); } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { Log.e("EXAMPLE", "Failed to create cipher."); throw new RuntimeException(e); } // decrypt the encrypted key with the secret key stored in the keystore byte[] decryptedKey; try { final SecretKey secretKey = (SecretKey) keyStore.getKey("realm_key", null); final IvParameterSpec initializationVectorSpec = new IvParameterSpec(initializationVector); cipher.init(Cipher.DECRYPT_MODE, secretKey, initializationVectorSpec); decryptedKey = cipher.doFinal(encryptedKey); } catch (InvalidKeyException e) { Log.e("EXAMPLE", "Failed to decrypt. Invalid key."); throw new RuntimeException(e); } catch (UnrecoverableKeyException | NoSuchAlgorithmException | BadPaddingException | KeyStoreException | IllegalBlockSizeException | InvalidAlgorithmParameterException e) { Log.e("EXAMPLE", "Failed to decrypt the encrypted realm key with the secret key."); throw new RuntimeException(e); } return decryptedKey; // pass to a realm configuration via encryptionKey() }
// Access the encrypted key in the keystore, decrypt it with the secret, // and use it to open and read from the realm again fun getExistingKey(): ByteArray { // open a connection to the android keystore val keyStore: KeyStore try { keyStore = KeyStore.getInstance("AndroidKeyStore") keyStore.load(null) } catch (e: Exception) { Log.e("EXAMPLE", "Failed to open the keystore.") throw RuntimeException(e) } // access the encrypted key that's stored in shared preferences val initializationVectorAndEncryptedKey = Base64.decode(activity ?.getSharedPreferences("realm_key", Context.MODE_PRIVATE) ?.getString("iv_and_encrypted_key", null), Base64.DEFAULT) val buffer = ByteBuffer.wrap(initializationVectorAndEncryptedKey) buffer.order(ByteOrder.BIG_ENDIAN) // extract the length of the initialization vector from the buffer val initializationVectorLength = buffer.int // extract the initialization vector based on that length val initializationVector = ByteArray(initializationVectorLength) buffer[initializationVector] // extract the encrypted key val encryptedKey = ByteArray(initializationVectorAndEncryptedKey.size - Integer.BYTES - initializationVectorLength) buffer[encryptedKey] // create a cipher that uses AES encryption to decrypt our key val cipher: Cipher cipher = try { Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7) } catch (e: Exception) { Log.e("EXAMPLE", "Failed to create cipher.") throw RuntimeException(e) } // decrypt the encrypted key with the secret key stored in the keystore val decryptedKey: ByteArray decryptedKey = try { val secretKey = keyStore.getKey("realm_key", null) as SecretKey val initializationVectorSpec = IvParameterSpec(initializationVector) cipher.init(Cipher.DECRYPT_MODE, secretKey, initializationVectorSpec) cipher.doFinal(encryptedKey) } catch (e: InvalidKeyException) { Log.e("EXAMPLE", "Failed to decrypt. Invalid key.") throw RuntimeException(e) } catch (e: Exception ) { Log.e("EXAMPLE", "Failed to decrypt the encrypted realm key with the secret key.") throw RuntimeException(e) } return decryptedKey // pass to a realm configuration via encryptionKey() }
Open an Encrypted Realm
The following code demonstrates how to open an encrypted realm with the encryptionKey() method:
// use a new encryption key to write and read from a realm byte[] realmKey = getNewKey(); // use the key to configure a realm final SyncConfiguration realmConfig = new SyncConfiguration.Builder(user, PARTITION) .allowQueriesOnUiThread(true) .allowWritesOnUiThread(true) .encryptionKey(realmKey) .build(); // once we've used the key to generate a config, erase it in memory manually Arrays.fill(realmKey, (byte) 0); // open and write and read from the realm Realm encryptedRealm = Realm.getInstance(realmConfig); ObjectId id = new ObjectId(); encryptedRealm.executeTransaction(eR -> { eR.createObject(Frog.class, id); }); Frog frog = encryptedRealm.where(Frog.class).findFirst(); ObjectId written_id = frog.get_id(); Log.v("EXAMPLE", "generated id: " + id + ", written frog id: " + written_id); encryptedRealm.close(); // get the encryption key from the key store a second time byte[] decryptedKey = getExistingKey(); // configure a realm with the key final SyncConfiguration realmConfigDecrypt = new SyncConfiguration.Builder(user, PARTITION) .allowQueriesOnUiThread(true) .allowWritesOnUiThread(true) .encryptionKey(decryptedKey) .build(); // once we've used the key to generate a config, erase it in memory manually Arrays.fill(decryptedKey, (byte) 0); // note: realm is encrypted, this variable just demonstrates that we've // decrypted the contents with the key in memory Realm decryptedRealm = Realm.getInstance(realmConfigDecrypt); Frog frogDecrypt = decryptedRealm.where(Frog.class).findFirst(); Log.v("EXAMPLE", "generated id: " + id + ", decrypted written frog id: " + frogDecrypt.get_id()); decryptedRealm.close();
// use a new encryption key to write and read from a realm val realmKey = getNewKey() // use the key to configure a realm val realmConfig = SyncConfiguration.Builder(user, PARTITION) .allowQueriesOnUiThread(true) .allowWritesOnUiThread(true) .encryptionKey(realmKey) .build() // once we've used the key to generate a config, erase it in memory manually Arrays.fill(realmKey, 0.toByte()) // open and write and read from the realm val encryptedRealm = Realm.getInstance(realmConfig) val id = ObjectId() encryptedRealm.executeTransaction { eR: Realm -> eR.createObject(Frog::class.java, id) } val frog = encryptedRealm.where(Frog::class.java).findFirst() val written_id = frog!!._id Log.v("EXAMPLE", "generated id: " + id + ", written frog id: " + written_id) encryptedRealm.close() // get the encryption key from the key store a second time val decryptedKey = getExistingKey() // configure a realm with the key val realmConfigDecrypt = SyncConfiguration.Builder(user, PARTITION) .allowQueriesOnUiThread(true) .allowWritesOnUiThread(true) .encryptionKey(decryptedKey) .build() // once we've used the key to generate a config, erase it in memory manually Arrays.fill(decryptedKey, 0.toByte()) // note: realm is encrypted, this variable just demonstrates that we've // decrypted the contents with the key in memory val decryptedRealm = Realm.getInstance(realmConfigDecrypt) val frogDecrypt = decryptedRealm.where(Frog::class.java).findFirst() Log.v("EXAMPLE", "generated id: " + id + ", decrypted written frog id: " + frogDecrypt!!._id) decryptedRealm.close()