Files
GajuMobile-Android/app/src/main/java/swiss/qpq/gajumobile/security/EncryptionService.kt
T
2026-08-22 18:45:28 +09:00

91 lines
3.3 KiB
Kotlin

package swiss.qpq.gajumobile.security
import android.content.Context
import kotlinx.serialization.Serializable
import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
@Serializable
data class EncryptedEnvelope(
val encryptedData: ByteArray,
val encryptedDek: ByteArray,
val dataIv: ByteArray,
val dekIv: ByteArray,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as EncryptedEnvelope
if (!encryptedData.contentEquals(other.encryptedData)) return false
if (!encryptedDek.contentEquals(other.encryptedDek)) return false
if (!dataIv.contentEquals(other.dataIv)) return false
return dekIv.contentEquals(other.dekIv)
}
override fun hashCode(): Int {
var result = encryptedData.contentHashCode()
result = (31 * result) + encryptedDek.contentHashCode()
result = (31 * result) + dataIv.contentHashCode()
result = (31 * result) + dekIv.contentHashCode()
return result
}
}
object EncryptionService {
private const val AES_GCM = "AES/GCM/NoPadding"
private const val GCM_TAG_LENGTH = 128
private const val IV_LENGTH = 12
fun encrypt(context: Context, data: ByteArray): EncryptedEnvelope {
// 1. Generate a random Data Encryption Key (DEK) in software
val dek = generateDek()
// 2. Encrypt the data with the DEK
val dataCipher = Cipher.getInstance(AES_GCM)
val dataIv = ByteArray(IV_LENGTH).apply { SecureRandom().nextBytes(this) }
dataCipher.init(Cipher.ENCRYPT_MODE, dek, GCMParameterSpec(GCM_TAG_LENGTH, dataIv))
val encryptedData = dataCipher.doFinal(data)
// 3. Encrypt the DEK with the Master Key (Hardware-backed)
val masterKey = KeyManager.getMasterKey(context)
val dekCipher = Cipher.getInstance(AES_GCM)
// Let Keystore generate the IV for the Master Key encryption
dekCipher.init(Cipher.ENCRYPT_MODE, masterKey)
val encryptedDek = dekCipher.doFinal(dek.encoded)
val dekIv = dekCipher.iv
return EncryptedEnvelope(
encryptedData = encryptedData,
encryptedDek = encryptedDek,
dataIv = dataIv,
dekIv = dekIv
)
}
fun decrypt(context: Context, envelope: EncryptedEnvelope): ByteArray {
// 1. Decrypt the DEK using the Master Key
val masterKey = KeyManager.getMasterKey(context)
val dekCipher = Cipher.getInstance(AES_GCM)
dekCipher.init(Cipher.DECRYPT_MODE, masterKey, GCMParameterSpec(GCM_TAG_LENGTH, envelope.dekIv))
val decryptedDekBytes = dekCipher.doFinal(envelope.encryptedDek)
val dek = SecretKeySpec(decryptedDekBytes, "AES")
// 2. Decrypt the data using the DEK
val dataCipher = Cipher.getInstance(AES_GCM)
dataCipher.init(Cipher.DECRYPT_MODE, dek, GCMParameterSpec(GCM_TAG_LENGTH, envelope.dataIv))
return dataCipher.doFinal(envelope.encryptedData)
}
private fun generateDek(): SecretKey {
val keyGen = KeyGenerator.getInstance("AES")
keyGen.init(256)
return keyGen.generateKey()
}
}