realm / realm-java

Realm is a mobile database: a replacement for SQLite & ORMs
http://realm.io
Apache License 2.0
11.47k stars 1.75k forks source link

Realm file decryption fails randomly #3974

Closed carlbenson closed 7 years ago

carlbenson commented 7 years ago

We get these crash reports from some of our customers of our app

java.lang.RuntimeException: Unable to start activity ComponentInfo{com.getdreams/com.dreams.LauncherActivity}: io.realm.exceptions.RealmFileException: Unable to open a realm at path '/data/data/com.getdreams/files/default.realm': Realm file decryption failed. in /Users/cm/Realm/realm-java/realm/realm-library/src/main/cpp/io_realm_internal_SharedRealm.cpp line 92 Kind: ACCESS_ERROR.
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2411)
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2474)
    at android.app.ActivityThread.access$800(ActivityThread.java:144)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1359)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at android.os.Looper.loop(Looper.java:155)
    at android.app.ActivityThread.main(ActivityThread.java:5696)
    at java.lang.reflect.Method.invoke(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:372)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1028)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:823)
Caused by: io.realm.exceptions.RealmFileException: Unable to open a realm at path '/data/data/com.getdreams/files/default.realm': Realm file decryption failed. in /Users/cm/Realm/realm-java/realm/realm-library/src/main/cpp/io_realm_internal_SharedRealm.cpp line 92 Kind: ACCESS_ERROR.
    at io.realm.internal.SharedRealm.nativeGetSharedRealm(Native Method)
    at io.realm.internal.SharedRealm.a(SharedRealm.java:205)
    at io.realm.internal.SharedRealm.a(SharedRealm.java:182)
    at io.realm.be.a(RealmCache.java:124)
    at io.realm.Realm.n(Realm.java:209)
    at com.dreams.LauncherActivity.onCreate(LauncherActivity.kt:28)
    at android.app.Activity.performCreate(Activity.java:5958)
    at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1129)
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2364)
    ... 10 more

Our configuration is very simple

Realm.init(application)
RealmConfiguration.Builder().apply {
    schemaVersion(21)
    migration(DreamsRealmMigration())
    if (!BuildConfig.DEBUG) {
        encryptionKey(getKeyPair(application, "dreams").public.encoded.copyOfRange(0, 64))
    }
}.build()

Version of Realm and tooling

Realm version(s): 2.2.1

Realm sync feature enabled: no

Android Studio version: 2.2.2

Which Android version and device: Android versions 4.4, 5.0 and 5.1, running on HTC One (m7), Sony Xperia X (F5121), Sony, Xperia Z (C6603),LG G3, Samsung Galaxy Grand Max

kneth commented 7 years ago

@carlbenson How reproducible is it? And just a trivial question: you can sure that getKeyPair() always return the same key?

carlbenson commented 7 years ago

I cannot reproduce it, it seems to be device dependent? The keys are taken from an Android Key Store entry and created if not present so it should stay the same. Cannot think of a reason for a Android Key Store entry to change for an already installed app.

nhachicha commented 7 years ago

Hi @carlbenson Are you generating an AES from an asymmetric key? How are you initialising the KeyStore?

is it possible to see the code where you obtain the key. the Android KeyStore have some subtle differences sometimes given the version of Android.

carlbenson commented 7 years ago

@nhachicha here is the code that initialises and fetches keys from the key store

fun getKeyPair(context: Context, entryKey: String): KeyPair {
    val keyStore = KeyStore.getInstance("AndroidKeyStore")
    keyStore.load(null)
    val entry = keyStore.getEntry(entryKey, null)
    if (entry is KeyStore.PrivateKeyEntry) {
        return KeyPair(entry.certificate.publicKey, entry.privateKey)
    } else {
        val keyGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore")
        keyGenerator.initialize(getAlgorithmParameterSpec(context, entryKey))
        return keyGenerator.generateKeyPair()
    }
}

private fun getAlgorithmParameterSpec(context: Context, entryKey: String): AlgorithmParameterSpec {
    return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
        val notBefore = Calendar.getInstance()
        val notAfter = Calendar.getInstance()
        notAfter.add(1, Calendar.YEAR)

        KeyPairGeneratorSpec.Builder(context)
                .setAlias(entryKey)
                .setKeyType(KeyProperties.KEY_ALGORITHM_RSA)
                .setKeySize(2048)
                .setSubject(X500Principal(String.format("CN=Dreams, OU=%s, C=SE", context.packageName)))
                .setSerialNumber(BigInteger.ONE)
                .setStartDate(notBefore.time)
                .setEndDate(notAfter.time)
                .build()
    } else {
        KeyGenParameterSpec.Builder(entryKey, KeyProperties.PURPOSE_DECRYPT or
                KeyProperties.PURPOSE_ENCRYPT or
                KeyProperties.PURPOSE_SIGN or
                KeyProperties.PURPOSE_VERIFY)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
                .setBlockModes(KeyProperties.BLOCK_MODE_ECB)
                .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
                .setKeySize(2048)
                .setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
                .build()
    }
}
nhachicha commented 7 years ago

AFAICT you're using the key only for encryption/decryption setSignaturePaddings is used for signing/verification. try setting instead .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)

Can you try with this configuration and see if it's reproducible on the mentioned devices?

Android >= M

Android >= 18

carlbenson commented 7 years ago

I am setting encryption paddings, 4 lines above setting signature paddings. I cannot change configurations and invalid thousands of user's sessions and ask them to login in again. Like I said, it only happens randomly in our user base, and I cannot reproduce it. Also, it only seems to happen on API < 23 devices.

Also, for realm, I'm only using the 512 first bits of the generated key (which is 2048 bites and used in backend communication where it works perfectly).

Qubitium commented 7 years ago

@carlbenson android.keystore is unreliable. if the data is not in your app/data/data folder, u are at the mercy of android fragmentation. https://code.google.com/p/android/issues/detail?id=61989

carlbenson commented 7 years ago

@diegomontoya yes this is what I have been fearing too, but it is very unclear from the Android docs if this is the intended behaviour or a bug. I think the solution will be for us to invalid the user session, remove their key from the keystore, create a new one and require a new login (will create a new realm file).