|
| 1 | +package com.alphahealth.monitor.security |
| 2 | + |
| 3 | +import android.content.Context |
| 4 | +import android.content.SharedPreferences |
| 5 | +import android.security.keystore.KeyGenParameterSpec |
| 6 | +import android.security.keystore.KeyProperties |
| 7 | +import android.util.Base64 |
| 8 | +import androidx.biometric.BiometricPrompt |
| 9 | +import androidx.core.content.ContextCompat |
| 10 | +import androidx.security.crypto.EncryptedSharedPreferences |
| 11 | +import androidx.security.crypto.MasterKey |
| 12 | +import java.security.KeyStore |
| 13 | +import javax.crypto.Cipher |
| 14 | +import javax.crypto.KeyGenerator |
| 15 | +import javax.crypto.SecretKey |
| 16 | +import javax.crypto.spec.GCMParameterSpec |
| 17 | + |
| 18 | +class SecurityOrchestrator(private val context: Context) { |
| 19 | + |
| 20 | + private val KEY_ALIAS = "NeuralPulseClinicalKey" |
| 21 | + private val ANDROID_KEYSTORE = "AndroidKeyStore" |
| 22 | + private val TRANSFORMATION = "AES/GCM/NoPadding" |
| 23 | + |
| 24 | + init { |
| 25 | + generateSecretKeyIfNeeded() |
| 26 | + } |
| 27 | + |
| 28 | + private fun generateSecretKeyIfNeeded() { |
| 29 | + val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) } |
| 30 | + if (!keyStore.containsAlias(KEY_ALIAS)) { |
| 31 | + val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE) |
| 32 | + val parameterSpec = KeyGenParameterSpec.Builder( |
| 33 | + KEY_ALIAS, |
| 34 | + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT |
| 35 | + ).setBlockModes(KeyProperties.BLOCK_MODE_GCM) |
| 36 | + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) |
| 37 | + .setKeySize(256) |
| 38 | + .build() |
| 39 | + keyGenerator.init(parameterSpec) |
| 40 | + keyGenerator.generateKey() |
| 41 | + } |
| 42 | + } |
| 43 | + |
| 44 | + private fun getSecretKey(): SecretKey { |
| 45 | + val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) } |
| 46 | + return keyStore.getKey(KEY_ALIAS, null) as SecretKey |
| 47 | + } |
| 48 | + |
| 49 | + /** |
| 50 | + * Encrypts clinical data string using AES-GCM encryption. |
| 51 | + */ |
| 52 | + fun encryptData(plainText: String): String { |
| 53 | + val cipher = Cipher.getInstance(TRANSFORMATION) |
| 54 | + cipher.init(Cipher.ENCRYPT_MODE, getSecretKey()) |
| 55 | + val encryptionIv = cipher.iv |
| 56 | + val cipherText = cipher.doFinal(plainText.toByteArray(Charsets.UTF_8)) |
| 57 | + |
| 58 | + // Combine IV and cipherText to store/transmit together |
| 59 | + val combined = ByteArray(encryptionIv.size + cipherText.size) |
| 60 | + System.arraycopy(encryptionIv, 0, combined, 0, encryptionIv.size) |
| 61 | + System.arraycopy(cipherText, 0, combined, encryptionIv.size, cipherText.size) |
| 62 | + |
| 63 | + return Base64.encodeToString(combined, Base64.DEFAULT) |
| 64 | + } |
| 65 | + |
| 66 | + /** |
| 67 | + * Decrypts GCM-encrypted clinical data using Android KeyStore secret keys. |
| 68 | + */ |
| 69 | + fun decryptData(encryptedBase64: String): String { |
| 70 | + val combined = Base64.decode(encryptedBase64, Base64.DEFAULT) |
| 71 | + val ivSize = 12 // Standard AES-GCM IV length is 12 bytes |
| 72 | + val cipherTextSize = combined.size - ivSize |
| 73 | + |
| 74 | + val iv = ByteArray(ivSize) |
| 75 | + val cipherText = ByteArray(cipherTextSize) |
| 76 | + System.arraycopy(combined, 0, iv, 0, ivSize) |
| 77 | + System.arraycopy(combined, ivSize, cipherText, 0, cipherTextSize) |
| 78 | + |
| 79 | + val cipher = Cipher.getInstance(TRANSFORMATION) |
| 80 | + val spec = GCMParameterSpec(128, iv) |
| 81 | + cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec) |
| 82 | + |
| 83 | + val decrypted = cipher.doFinal(cipherText) |
| 84 | + return String(decrypted, Charsets.UTF_8) |
| 85 | + } |
| 86 | + |
| 87 | + /** |
| 88 | + * Obtains secure EncryptedSharedPreferences to store configuration properties like API tokens. |
| 89 | + */ |
| 90 | + fun getSecurePreferences(): SharedPreferences { |
| 91 | + val masterKey = MasterKey.Builder(context) |
| 92 | + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) |
| 93 | + .build() |
| 94 | + |
| 95 | + return EncryptedSharedPreferences.create( |
| 96 | + context, |
| 97 | + "neuralpulse_secure_prefs", |
| 98 | + masterKey, |
| 99 | + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, |
| 100 | + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM |
| 101 | + ) |
| 102 | + } |
| 103 | + |
| 104 | + /** |
| 105 | + * Launches the biometric prompt to authenticate the user before sensitive actions (e.g. data export). |
| 106 | + */ |
| 107 | + fun showBiometricAuthentication( |
| 108 | + activity: androidx.fragment.app.FragmentActivity, |
| 109 | + title: String, |
| 110 | + subtitle: String, |
| 111 | + onSuccess: () -> Unit, |
| 112 | + onFailure: (String) -> Unit |
| 113 | + ) { |
| 114 | + val executor = ContextCompat.getMainExecutor(context) |
| 115 | + val biometricPrompt = BiometricPrompt(activity, executor, |
| 116 | + object : BiometricPrompt.AuthenticationCallback() { |
| 117 | + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { |
| 118 | + super.onAuthenticationSucceeded(result) |
| 119 | + onSuccess() |
| 120 | + } |
| 121 | + |
| 122 | + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { |
| 123 | + super.onAuthenticationError(errorCode, errString) |
| 124 | + onFailure(errString.toString()) |
| 125 | + } |
| 126 | + |
| 127 | + override fun onAuthenticationFailed() { |
| 128 | + super.onAuthenticationFailed() |
| 129 | + onFailure("Biometric authentication failed.") |
| 130 | + } |
| 131 | + }) |
| 132 | + |
| 133 | + val promptInfo = BiometricPrompt.PromptInfo.Builder() |
| 134 | + .setTitle(title) |
| 135 | + .setSubtitle(subtitle) |
| 136 | + .setNegativeButtonText("Cancel") |
| 137 | + .build() |
| 138 | + |
| 139 | + biometricPrompt.authenticate(promptInfo) |
| 140 | + } |
| 141 | +} |
0 commit comments