Phase 2: AES-256-GCM vault backed by Android Keystore
Browse files
app/src/main/java/io/github/abrarul/modularai/security/Vault.kt
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package io.github.abrarul.modularai.security
|
| 2 |
+
|
| 3 |
+
import android.security.keystore.KeyGenParameterSpec
|
| 4 |
+
import android.security.keystore.KeyProperties
|
| 5 |
+
import android.util.Base64
|
| 6 |
+
import java.security.KeyStore
|
| 7 |
+
import javax.crypto.Cipher
|
| 8 |
+
import javax.crypto.KeyGenerator
|
| 9 |
+
import javax.crypto.SecretKey
|
| 10 |
+
import javax.crypto.spec.GCMParameterSpec
|
| 11 |
+
|
| 12 |
+
/** AES-256-GCM encryption; the key never leaves the Android Keystore. */
|
| 13 |
+
object Vault {
|
| 14 |
+
private const val ALIAS = "modularai_vault_key"
|
| 15 |
+
private const val PROVIDER = "AndroidKeyStore"
|
| 16 |
+
private const val TRANSFORM = "AES/GCM/NoPadding"
|
| 17 |
+
|
| 18 |
+
private fun key(): SecretKey {
|
| 19 |
+
val ks = KeyStore.getInstance(PROVIDER).apply { load(null) }
|
| 20 |
+
(ks.getKey(ALIAS, null) as? SecretKey)?.let { return it }
|
| 21 |
+
val gen = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, PROVIDER)
|
| 22 |
+
gen.init(
|
| 23 |
+
KeyGenParameterSpec.Builder(
|
| 24 |
+
ALIAS, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
|
| 25 |
+
)
|
| 26 |
+
.setKeySize(256)
|
| 27 |
+
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
| 28 |
+
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
| 29 |
+
.build()
|
| 30 |
+
)
|
| 31 |
+
return gen.generateKey()
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
/** Returns base64(iv || ciphertext+tag). */
|
| 35 |
+
fun encrypt(plain: String): String {
|
| 36 |
+
val c = Cipher.getInstance(TRANSFORM)
|
| 37 |
+
c.init(Cipher.ENCRYPT_MODE, key())
|
| 38 |
+
val out = c.iv + c.doFinal(plain.toByteArray(Charsets.UTF_8))
|
| 39 |
+
return Base64.encodeToString(out, Base64.NO_WRAP)
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
fun decrypt(blob: String): String {
|
| 43 |
+
val raw = Base64.decode(blob, Base64.NO_WRAP)
|
| 44 |
+
val iv = raw.copyOfRange(0, 12)
|
| 45 |
+
val c = Cipher.getInstance(TRANSFORM)
|
| 46 |
+
c.init(Cipher.DECRYPT_MODE, key(), GCMParameterSpec(128, iv))
|
| 47 |
+
return String(c.doFinal(raw, 12, raw.size - 12), Charsets.UTF_8)
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
/** Deletes the key: every encrypted record becomes permanently unreadable. */
|
| 51 |
+
fun destroy() {
|
| 52 |
+
KeyStore.getInstance(PROVIDER).apply { load(null) }.deleteEntry(ALIAS)
|
| 53 |
+
}
|
| 54 |
+
}
|