Skip to content

Instantly share code, notes, and snippets.

@remithaunay
Created April 28, 2025 10:14
Show Gist options
  • Select an option

  • Save remithaunay/a5f67ebceba12358c1fcd227e48bf250 to your computer and use it in GitHub Desktop.

Select an option

Save remithaunay/a5f67ebceba12358c1fcd227e48bf250 to your computer and use it in GitHub Desktop.
SharedPreferences with Tink and Android KeyStore
/**
* Securely stores string data in SharedPreferences using Tink's AEAD (Authenticated Encryption with
* Associated Data) primitive, backed by AES-256-GCM encryption and Android KeyStore for key management.
*/
object SecureStorage {
// Constants for SharedPreferences and Tink keyset configuration
private const val TAG = "SecureStorage" // For logging
private const val KEYSET_NAME = "tink_keyset" // Name of the Tink keyset stored in SharedPreferences
private const val PREFS_NAME = "secure_prefs" // Name of the SharedPreferences file
private const val MASTER_KEY_URI = "android-keystore://tink_master_key" // URI for the master key in KeyStore
// Register Tink's AEAD configuration once during class initialization
init {
try {
AeadConfig.register()
} catch (e: GeneralSecurityException) {
Logger.log(
LogCategory.IO,
LogLevel.ERROR,
"Failed to register AeadConfig: ${e.message}",
)
// Note: App can continue, but encryption/decryption will fail later
}
}
// Lazily initialize SharedPreferences for storing encrypted data
private val prefs: SharedPreferences by lazy {
appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
}
// Lazily initialize the AEAD primitive for encryption/decryption
private val aead: Aead? by lazy {
try {
val keysetHandle =
AndroidKeysetManager
.Builder()
.withKeyTemplate(AeadKeyTemplates.AES256_GCM) // Use AES-256-GCM for strong encryption
.withSharedPref(appContext, KEYSET_NAME, PREFS_NAME) // Store keyset in SharedPreferences
.withMasterKeyUri(MASTER_KEY_URI) // Secure keys with Android KeyStore
.build()
.keysetHandle
keysetHandle.getPrimitive(RegistryConfiguration.get(), Aead::class.java)
} catch (e: Exception) {
Logger.log(
LogCategory.IO,
LogLevel.ERROR,
"Failed to initialize AEAD primitive: ${e.message}",
)
null // Return null to indicate failure; methods will handle this
}
}
/**
* Retrieves a string value for the given key, decrypting it if it exists.
*
* @param key The key associated with the encrypted value.
* @return The decrypted string, or null if the key doesn't exist, decryption fails, or initialization failed.
*/
fun getString(key: String): String? {
val aeadInstance =
aead ?: run {
Logger.log(
LogCategory.IO,
LogLevel.ERROR,
"AEAD not initialized; cannot decrypt",
)
return null
}
val encryptedBase64 = prefs.getString(key, null) ?: return null
return try {
val ciphertext = Base64.decode(encryptedBase64, Base64.NO_WRAP)
val decrypted = aeadInstance.decrypt(ciphertext, null) // No associated data used
String(decrypted, StandardCharsets.UTF_8)
} catch (e: Exception) {
Logger.log(
LogCategory.IO,
LogLevel.WARNING,
"Decryption failed for key: $key, error: ${e.message}",
)
null // Return null for tampered data, wrong key, or other failures
}
}
/**
* Stores a string value for the given key, encrypting it before saving. If the value is null,
* the key is removed from storage.
*
* @param key The key to associate with the value.
* @param value The string to encrypt and store, or null to remove the key.
*/
fun setString(
key: String,
value: String?,
) {
val aeadInstance =
aead ?: run {
Logger.log(
LogCategory.IO,
LogLevel.ERROR,
"AEAD not initialized; cannot encrypt",
)
return
}
// Limit input to 8,192 characters (~8 KB) to prevent SharedPreferences performance issues
// Encrypted output (~11 KB after Base64) remains safe for a single entry
if (value != null && value.length > 8192) {
throw IllegalArgumentException(
"Input exceeds 8,192 characters. Use alternative storage for large data.",
)
}
val editor = prefs.edit()
if (value == null) {
editor.remove(key).apply()
return
}
try {
val plaintext = value.toByteArray(StandardCharsets.UTF_8)
val ciphertext = aeadInstance.encrypt(plaintext, null) // No associated data used
val base64 = Base64.encodeToString(ciphertext, Base64.NO_WRAP)
editor.putString(key, base64).apply()
} catch (e: Exception) {
Logger.log(
LogCategory.IO,
LogLevel.ERROR,
"Encryption failed for key: $key, error: ${e.message}",
)
// Don't save if encryption fails to avoid storing plaintext or invalid data
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment