Skip to content

Rare InvalidKeyException during SharedPreferences read/write using AndroidKeysetManager on Android 15/16 #66

@jonichonpa

Description

@jonichonpa

Describe the bug:

On Android 15 and 16, we have identified a runtime failure related to
Android Keystore when using Tink that only reproduces on a limited subset
of user devices.

On those specific devices, the following exception is thrown:

java.security.InvalidKeyException: Keystore cannot load the key with ID: <master_key_alias>

The keystore alias exists, but the key cannot be loaded or used.
This issue does not occur on most devices and has only been observed
on certain device models used by a subset of users.

The failure has been observed during application-level encrypted read/write operations.

We are not using EncryptedSharedPreferences or Jetpack Security Crypto.

What was the expected behavior?

We expect that a keystore-backed master key managed by AndroidKeysetManager
remains usable across normal application lifecycle events, and that encryption
and decryption via Aead.encrypt() / Aead.decrypt() succeed reliably once the
key has been initialized.

At minimum, we expect well-defined best practices to reduce the likelihood of
the keystore key becoming unusable at runtime.

How can we reproduce the bug?

At this time, the issue has only been reproduced on a limited set of physical
devices owned by some users. We have not been able to reproduce the issue
on all devices of the same Android version, nor in emulator environments.

However, the following code represents the full flow where the exception has been
observed on affected devices. The exact failing call site may differ depending
on the device, but the failure occurs during encrypted read/write operations
after initialization.

private static synchronized void initialize()
        throws GeneralSecurityException, IOException {

    if (initialized) {
        return;
    }

    Context context = Cocos2dxActivity.getContext();
    if (context == null) {
        throw new IOException("Context is not ready");
    }

    TinkConfig.register();

    AndroidKeysetManager keysetManager =
            new AndroidKeysetManager.Builder()
                    .withSharedPref(
                            context,
                            "<keyset_prefs_name>",
                            "<keyset_prefs_key>")
                    .withKeyTemplate(
                            AesGcmKeyManager.aes256GcmTemplate())
                    .withMasterKeyUri(
                            "android-keystore://<master_key_alias>")
                    .build();

    KeysetHandle keysetHandle = keysetManager.getKeysetHandle();
    aead = keysetHandle.getPrimitive(Aead.class);

    sharedPreferences =
            context.getSharedPreferences(
                    "<data_prefs_name>",
                    Context.MODE_PRIVATE);

    initialized = true;
}

public void write(final String key, final String value, long callbackPtr) {
    boolean result = false;
    try {
        writeImpl(key, value);
        result = true;
    } catch (Exception e) {
        // InvalidKeyException is caught here
        logError("write()", e);
    }
    onWrite(result, callbackPtr);
}

public void read(final String key, long callbackPtr) {
    String value = "";
    try {
        value = readImpl(key);
    } catch (Exception e) {
        // InvalidKeyException is caught here
        logError("read()", e);
    }
    onRead(value, callbackPtr);
}

private static void writeImpl(String key, String value)
        throws GeneralSecurityException, IOException {

    initialize();

    byte[] plaintext = value.getBytes(StandardCharsets.UTF_8);
    byte[] aad = key.getBytes(StandardCharsets.UTF_8);

    byte[] ciphertext = aead.encrypt(plaintext, aad);

    sharedPreferences.edit()
            .putString(key, encode(ciphertext))
            .apply();
}

private static String readImpl(String key)
        throws GeneralSecurityException, IOException {

    initialize();

    String encryptedValue = sharedPreferences.getString(key, "");
    if (encryptedValue == null || encryptedValue.isEmpty()) {
        return "";
    }

    byte[] encryptedBytes = decode(encryptedValue);
    byte[] aad = key.getBytes(StandardCharsets.UTF_8);

    byte[] decryptedBytes = aead.decrypt(encryptedBytes, aad);

    return new String(decryptedBytes, StandardCharsets.UTF_8);
}

Do you have any debugging information?

  • Exception observed:
java.security.InvalidKeyException: Keystore cannot load the key with ID: <master_key_alias>
  • The keystore alias exists, but the key cannot be loaded or used.
  • The issue is intermittent and does not reproduce reliably.
  • Based on logs, the exception propagates from writeImpl() / readImpl() and is caught by their public wrapper methods.

What version of Tink are you using?

1.12.0.

Can you tell us more about your development environment?

  • Platform: Android
  • Android versions affected: 15, 16
  • Devices:
    • Samsung Galaxy Z Flip7
    • OPPO A5 5G
    • AQUOS wish3
    • Xperia 10 VI
  • NOT using EncryptedSharedPreferences

Is there anything else you'd like to add?

We are primarily looking for guidance on best practices to prevent or reduce
the likelihood of this issue when using AndroidKeysetManager with
android-keystore:// on recent Android versions.

Any recommendations regarding key lifecycle management, initialization patterns,
or defensive handling strategies would be greatly appreciated.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions