How AES-256 Encryption Protects Learning Data and User Dictionary in JapaneseKeyboard

The JapaneseKeyboard app uses AES-256-GCM encryption via the Android Keystore to ensure that learning data and user dictionary entries remain unreadable to unauthorized apps or attackers with physical device access.

The kazumaproject/japanesekeyboard repository implements a defense-in-depth strategy for sensitive user-generated content. By applying AES-256 encryption to all learning statistics and custom dictionary words before they reach the Room database, the app ensures that only encrypted byte arrays ever touch persistent storage. This protection is orchestrated by the CryptoManager singleton, which leverages hardware-backed key storage and authenticated encryption to prevent both unauthorized disclosure and tampering.

What Data Gets Protected

The encryption layer guards two distinct categories of user information stored in the app's Room database:

  • Learning data – Statistical records of words the user has typed, including frequency counters and usage patterns (managed via LearnEntity in LearnDao.kt).
  • User dictionary – Custom word entries and shortcuts defined by the user for personalized typing suggestions.

Both data types are converted to JSON byte arrays and encrypted via CryptoManager before insertion, ensuring that the underlying SQLite database contains only opaque binary blobs.

How AES-256-GCM Encryption Works

The implementation relies on AES-256 in Galois/Counter Mode (GCM), providing both confidentiality and integrity verification. The CryptoManager object in core/src/main/java/com/kazumaproject/core/domain/cryptoManager/CryptoManager.kt handles all cryptographic operations.

Hardware-Backed Key Generation

The 256-bit AES key never exists in the app's memory as extractable material. Instead, it is generated inside the Android Keystore under the alias user_dictionary_key:

// From CryptoManager.kt lines 21-48
val keyGenerator = KeyGenerator.getInstance(
    KeyProperties.KEY_ALGORITHM_AES,
    PROVIDER
)
val keyGenParameterSpec = KeyGenParameterSpec.Builder(
    KEY_ALIAS,
    KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
    .setKeySize(256)
    .setUserAuthenticationRequired(false)
    .build()

keyGenerator.init(keyGenParameterSpec)
keyGenerator.generateKey()

This configuration ensures the key is bound to the device hardware and protected by the device's credential lock, making extraction infeasible even with root access.

The Encryption Process

When persisting learning data or dictionary entries, the encrypt function performs the following steps (lines 55-61 in CryptoManager.kt):

  1. Initializes a Cipher instance with the transformation "AES/GCM/NoPadding".
  2. Generates a unique 12-byte Initialization Vector (IV) for each encryption operation.
  3. Encrypts the plaintext ByteArray (e.g., JSON representation of LearnEntity).
  4. Prepends the IV to the ciphertext, storing the concatenation as IV || ciphertext.
fun encrypt(plainText: ByteArray): ByteArray {
    val cipher = Cipher.getInstance(TRANSFORMATION)
    cipher.init(Cipher.ENCRYPT_MODE, getKey())
    val iv = cipher.iv // 12 bytes
    val cipherText = cipher.doFinal(plainText)
    return iv + cipherText // IV prepended for storage
}

The Decryption Process

Retrieval reverses the operation via the decrypt function (lines 68-76):

  1. Extracts the first 12 bytes as the IV.
  2. Uses the remaining bytes as the ciphertext.
  3. Initializes a GCMParameterSpec with the IV and authentication tag length (128 bits).
  4. Decrypts using the same Keystore key, verifying integrity via GCM's authentication tag.
fun decrypt(encryptedData: ByteArray): ByteArray {
    val cipher = Cipher.getInstance(TRANSFORMATION)
    val iv = encryptedData.sliceArray(0 until 12)
    val cipherText = encryptedData.sliceArray(12 until encryptedData.size)
    val spec = GCMParameterSpec(128, iv)
    cipher.init(Cipher.DECRYPT_MODE, getKey(), spec)
    return cipher.doFinal(cipherText)
}

Integration with the Room Database

The encryption layer is transparent to the database schema. In LearnRepository.kt and LearnDao.kt, data flows through CryptoManager at the boundary between application logic and persistent storage:

  • Write path: LearnRepository converts LearnEntity to JSON bytes, calls CryptoManager.encrypt(), and passes the resulting ByteArray to the DAO for insertion.
  • Read path: LearnDao returns the encrypted ByteArray from the Room database, which LearnRepository decrypts via CryptoManager.decrypt() before deserializing back to a LearnEntity object.

This architecture ensures that only unencrypted data lives in memory during active use, while the Room database (LearnEntity table and user dictionary table) contains exclusively encrypted blobs.

Security Benefits of AES-256-GCM

The specific choice of AES-256-GCM delivers three critical security properties for mobile keyboard data:

  1. Confidentiality via Hardware-Backed Keys: The 256-bit key never leaves the Android Keystore. Even if an attacker extracts the database file, they cannot decrypt it without the hardware-protected key.
  2. Integrity via Authentication Tags: GCM mode generates a 128-bit authentication tag during encryption. The Cipher.doFinal() operation during decryption will throw an exception if any bit of the ciphertext has been tampered with, preventing data manipulation attacks.
  3. Unique Ciphertext per Entry: By generating a random 12-byte IV for every encryption operation, identical dictionary entries or learning records produce completely different ciphertexts. This prevents pattern analysis and blocks replay attacks.

Summary

  • AES-256-GCM encryption in CryptoManager.kt protects both learning data and user dictionary entries before they reach the Room database.
  • The 256-bit AES key is generated inside the Android Keystore under the alias user_dictionary_key, ensuring it cannot be extracted from the device.
  • Each encryption operation uses a unique 12-byte IV and produces authenticated ciphertext with a 128-bit GCM tag, guaranteeing both confidentiality and integrity.
  • LearnRepository and LearnDao handle the encrypt/decrypt operations at the data layer boundary, ensuring only encrypted bytes persist in LearnEntity and dictionary tables.

Frequently Asked Questions

What is the difference between AES-256 and AES-2S6 mentioned in the documentation?

AES-2S6 is simply an internal shorthand or typographical reference to AES-256 used within the JapaneseKeyboard project documentation. The implementation in CryptoManager.kt explicitly uses KeyProperties.KEY_SIZE_256 and the standard AES-256-GCM algorithm. The "2S6" notation appears to be a project-specific naming convention rather than a distinct cryptographic standard.

Where is the encryption key stored?

The encryption key is stored in the Android Keystore system under the alias user_dictionary_key. As defined in CryptoManager.kt lines 21-48, the key is generated with KeyGenParameterSpec and never exists in the application's memory as extractable bytes. The Keystore binds the key to the device hardware, making it inaccessible even to apps with root privileges.

Can encrypted data be recovered if the user forgets their device password?

No, the encrypted data cannot be recovered if the device credentials are lost or reset. Because the AES-256 key is hardware-bound to the Android Keystore and protected by the device's credential lock, a factory reset or credential change effectively destroys the key material. Without the original Keystore entry, the encrypted blobs in the Room database remain permanently undecipherable.

Does AES-256-GCM impact keyboard performance?

The performance impact is negligible for typical keyboard usage patterns. The CryptoManager operations occur on background threads within the LearnRepository and LearnDao layers, and AES-GCM is highly optimized in modern Android devices via hardware acceleration (ARM Cryptography Extensions). The overhead of encrypting/decrypting small JSON payloads (learning entries and dictionary words) is imperceptible to the user during normal typing sessions.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →