Realm の暗号化 - Java SDK
項目一覧
Overview
Realm を暗号化して、ディスクに保存されたデータをアプリケーション外で読み取れないようにすることができます。 Realm を開くときに64バイトの暗号化キーを指定して、AES- 256 + SHA- 2でディスク上の Realm ファイルを暗号化します。
Realmは標準の AES-256 暗号化 でデータを透過的に暗号化と復号化します 指定された ビット暗号化のキーの最初の256 512ビットを使用します。 Realmは、 ビット暗号化のキーの他の256 512ビットを使用して、 ハッシュベースのメッセージ認証コード(HMAC)を使用して整合性を検証します。
警告
Realm 暗号化キーには、暗号化が脆弱なハッシュを使用しないでください。 最適なセキュリティを実現するには、暗号化キーを生成するのではなく、ランダムに生成することをお勧めします。
Considerations
以下は、Realm を暗号化する際に考慮する必要があるキーへの影響です。
キーの保存と再利用
同じ暗号化キーを RealmConfiguration.Builder.encryptionKey() に渡す 必要 があります邦土を開くたびに。 暗号化された Realm にキーを指定しないか、誤ったキーを指定した場合、Realm SDK はエラーをスローします。
アプリは暗号化のキーを Android キーストアに保存する必要があります 他のアプリがキーを読み取れないようにします。
パフォーマンスへの影響
暗号化された Realm での読み取りと書込みは、暗号化されていない Realm よりも最大 10% 遅くなる可能性があります。
暗号化と Atlas Device Sync
Realm はデバイス上のデータのみを暗号化し、暗号化されていないデータを Atlas データソースに保存します。 Atlas データソースへのアクセスが許可されたユーザーはデータを読み取ることができますが、次の条件は引き続き適用されます。
同期されたデータを読み取るには、ユーザーには適切な読み取り権限が必要です。
Atlas に保存されるデータは、常にボリューム(ディスク)レベルで暗号化されます。
クライアントとサーバー間の転送は常に完全に暗号化されます。
また、カスタマー キー管理を有効にして、クラウドAtlasのキー(例: Amazon Web Services KMS 、 Azure Key Vault 、 Google Cloud Platform KMS )。
アプリケーションの各ユーザーに一意のキーが必要な場合は、OAuth プロバイダーを使用するか、 Realm認証プロバイダーと認証triggerのいずれかを使用して 64 ビットキーを作成し、そのキーをユーザー オブジェクトに保存できます。
複数のプロセスから暗号化された Realm へのアクセス
バージョン 10.14.0 で変更。
Realm Java SDK バージョン10.14.0以降では、 Realm は、複数のプロセスで同じ暗号化された Realm を開くことをサポートしています。
アプリで Realm Java SDK バージョン 10.14.0 またはそれ以前のバージョンが使用されている場合、複数のプロセスから暗号化された Realm を開こうとすると、次のエラーがスローされます。
Encrypted interprocess sharing is currently unsupported.
例
次の手順では、 Android キーストア を使用する推奨方法を説明します Realm を使用した暗号化の場合
Android キーストアを使用して、Android が安全に保存および検索できる非対称 RSA キーを生成します。
注意
Android バージョン M 以降: キーストアのセキュリティ
バージョン M 以降では、キーストアのロックを解除するにはユーザーの API またはフィンガープリントが必要です。
Realm の暗号化に使用できる対称キー(AES)を生成します。
秘密の RSA キーを使用して対称 AES キーを暗号化します。
暗号化された AES キーをファイルシステム(
SharedPreferences
など)に保存します。
暗号化された Realm を使用する必要がある場合:
暗号化された AES キーを取得します。
公開 RSA キーを使用して、暗号化された AES キーを復号化します。
RealmConfiguration
の復号化された AES キーを使用して、暗号化された Realm を開きます。
暗号化のキーの生成と保存
次のコードは、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() }
既存の暗号化のキーへのアクセス
次のコードは、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() }
暗号化された Realm を開く
次のコードは、 encryptionKey()メソッドを使用して暗号化された Realm を開く方法を示しています。
// 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()