archive-97f2350/app/src/main/java/uk/orllewin/sianel/data/Crypto.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package uk.orllewin.sianel.data
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
/**
* AES-256-GCM encryption backed by a non-exportable AndroidKeyStore key.
*
* Replaces the deprecated Jetpack Security (`EncryptedSharedPreferences`) library:
* we keep the key in the Keystore and store only `base64(iv || ciphertext)` in a
* plain SharedPreferences. No Tink dependency, no R8 keep-rule fragility.
*/
object Crypto {
private const val KEYSTORE = "AndroidKeyStore"
private const val KEY_ALIAS = "sianel_prefs_key"
private const val TRANSFORMATION = "AES/GCM/NoPadding"
private const val IV_LENGTH = 12 // GCM standard nonce size
private const val TAG_LENGTH_BITS = 128
private fun secretKey(): SecretKey {
val ks = KeyStore.getInstance(KEYSTORE).apply { load(null) }
(ks.getEntry(KEY_ALIAS, null) as? KeyStore.SecretKeyEntry)?.let { return it.secretKey }
return KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE).apply {
init(
KeyGenParameterSpec.Builder(
KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.build()
)
}.generateKey()
}
fun encrypt(plaintext: String): String {
val cipher = Cipher.getInstance(TRANSFORMATION).apply {
init(Cipher.ENCRYPT_MODE, secretKey())
}
val iv = cipher.iv
val ciphertext = cipher.doFinal(plaintext.toByteArray(Charsets.UTF_8))
return Base64.encodeToString(iv + ciphertext, Base64.NO_WRAP)
}
/** Returns null if the blob is corrupt or was encrypted under a since-invalidated key. */
fun decrypt(stored: String): String? = runCatching {
val combined = Base64.decode(stored, Base64.NO_WRAP)
val iv = combined.copyOfRange(0, IV_LENGTH)
val ciphertext = combined.copyOfRange(IV_LENGTH, combined.size)
val cipher = Cipher.getInstance(TRANSFORMATION).apply {
init(Cipher.DECRYPT_MODE, secretKey(), GCMParameterSpec(TAG_LENGTH_BITS, iv))
}
String(cipher.doFinal(ciphertext), Charsets.UTF_8)
}.getOrNull()
}