加密 Realm — Java SDK
在此页面上
Overview
您可以对 Realm 进行加密,以确保应用程序外部无法读取存储到磁盘的数据。 您可以通过在域Realm 时提供64字节加密密钥,使用 AES- 256 + SHA- 2加密磁盘上的域文件。
Realm使用标准 AES-256 加密 以透明方式加密和解密数据 使用给定256 位加密密钥的前512 位。 Realm使用 位加密密钥的其他256 位,通过512 基于哈希的消息身份验证代码 (HMAC) 来验证完整性。
警告
不要将弱加密哈希值用于 Realm 加密密钥。为了获得最佳安全性,我们建议生成随机加密密钥,而不是派生加密密钥。
Considerations
以下是加密 Realm 时需要考虑的主要影响。
存储和重用密钥
您必须将相同的加密密钥传递给RealmConfiguration.Builder.encryptionKey() 每次打开域时。 如果您不提供密钥或为加密域指定了错误的密钥, Realm 软件开发工具包(Realm SDK)会抛出错误。
应用程序应将加密密钥存储在 Android KeyStore 中 以便其他应用程序无法读取密钥。
性能影响
加密 Realm 的读取和写入速度可能比未加密 Realm 慢 10%。
加密和 Atlas Device Sync
您可以对同步 Realm 进行加密。
Realm 仅对设备上的数据进行加密,并将未加密的数据存储在 Atlas 数据源中。任何有权访问 Atlas 数据源的用户都可以读取数据,但以下规则仍然适用:
用户必须拥有正确的读取权限才能读取同步数据。
Atlas 中存储的数据始终在卷(磁盘)级别进行加密。
客户端和服务器之间的传输始终是完全加密的。
您还可以启用客户密钥管理,使用云提供商的密钥(例如AWS KMS、Azure Key Vault、Google Cloud KMS)对存储的 Atlas 数据进行加密。
如果您需要为应用程序的每个用户提供唯一的密钥,则可以使用 OAuth 提供程序或使用一个Realm身份验证提供程序和身份验证trigger来创建 64 位密钥并将该密钥存储在用户对象中。
从多个进程访问加密 Realm
在 10.14.0 版本中进行了更改。
从Realm Java SDK版本10.14.0开始, Realm支持在多个进程中打开相同的加密域 。
如果您的应用使用 Realm Java SDK 版本 10.14.0 或更早版本,则尝试从多个进程打开加密 Realm 会引发以下错误:
Encrypted interprocess sharing is currently unsupported.
例子
以下步骤描述了使用 Android KeyStore 的推荐方法 使用Realm进行加密:
生成 Android 可以使用 Android KeyStore 安全地存储和检索的非对称 RSA 密钥。
注意
Android M 及更高版本:密钥库安全性
版本 M 及更高版本需要用户 PIN 或指纹来解锁密钥库。
生成可用于加密 Realm 的对称密钥 (AES)。
使用 RSA 私钥对 AES 对称密钥进行加密。
将加密的 AES 密钥存储在文件系统上(例如,存储在
SharedPreferences
中)。
当您需要使用加密域时:
检索加密的 AES 密钥。
使用 RSA 公钥对加密的 AES 密钥进行解密。
使用
RealmConfiguration
中解密的 AES 密钥打开加密域。
生成并存储加密密钥
以下代码演示了如何安全地生成和存储的加密密钥:
// 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 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() }
打开加密 Realm
以下代码演示了如何使用EncryptionKey()方法打开加密域 :
// 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()