Implemented field-level encryption for the database

This commit is contained in:
grimsi
2024-09-22 19:29:42 +02:00
parent ae56793e6e
commit 3e64cfd30a
5 changed files with 68 additions and 5 deletions
+3
View File
@@ -2,6 +2,9 @@
<configuration default="false" name="GameyfinApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
<option name="ACTIVE_PROFILES" value="dev" />
<option name="ALTERNATIVE_JRE_PATH" value="BUNDLED" />
<envs>
<env name="APP_KEY" value="8ODYedBBEA6qTd2Z/dZiWA==" />
</envs>
<module name="gameyfin.main" />
<option name="SHORTEN_COMMAND_LINE" value="ARGS_FILE" />
<option name="SPRING_BOOT_MAIN_CLASS" value="de.grimsi.gameyfin.GameyfinApplication" />
@@ -1,5 +1,6 @@
package de.grimsi.gameyfin.config.entities
import de.grimsi.gameyfin.core.security.EncryptionConverter
import jakarta.persistence.*
import jakarta.validation.constraints.NotNull
@@ -12,7 +13,7 @@ class ConfigEntry(
val key: String,
@NotNull
@Lob
@Column(name = "`value`")
@Convert(converter = EncryptionConverter::class)
var value: String
)
@@ -0,0 +1,58 @@
package de.grimsi.gameyfin.core.security
import jakarta.persistence.AttributeConverter
import jakarta.persistence.Converter
import java.util.*
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
@Converter
class EncryptionConverter : AttributeConverter<String, String> {
companion object {
private const val ALGORITHM = "AES"
private val SECRET_KEY: SecretKeySpec
init {
val base64Key = System.getenv("APP_KEY")
?: throw IllegalStateException("APP_KEY environment variable is not set or empty")
val decodedKey = Base64.getDecoder().decode(base64Key)
// Ensure the key length is valid for AES (128, 192, or 256 bits)
if (decodedKey.size !in listOf(16, 24, 32)) {
throw IllegalArgumentException("Invalid AES key length. Key must be 128, 192, or 256 bits.")
}
SECRET_KEY = SecretKeySpec(decodedKey, ALGORITHM)
}
}
override fun convertToDatabaseColumn(attribute: String?): String? {
return attribute?.let {
try {
val cipher = Cipher.getInstance(ALGORITHM).apply {
init(Cipher.ENCRYPT_MODE, SECRET_KEY)
}
val encryptedBytes = cipher.doFinal(it.toByteArray())
Base64.getEncoder().encodeToString(encryptedBytes)
} catch (e: Exception) {
throw RuntimeException("Error during encryption", e)
}
}
}
override fun convertToEntityAttribute(dbData: String?): String? {
return dbData?.let {
try {
val cipher = Cipher.getInstance(ALGORITHM).apply {
init(Cipher.DECRYPT_MODE, SECRET_KEY)
}
val decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(it))
String(decryptedBytes)
} catch (e: Exception) {
throw RuntimeException("Error during decryption", e)
}
}
}
}
@@ -1,15 +1,14 @@
package de.grimsi.gameyfin.users.entities
import jakarta.persistence.Entity
import jakarta.persistence.FetchType
import jakarta.persistence.Id
import jakarta.persistence.OneToOne
import de.grimsi.gameyfin.core.security.EncryptionConverter
import jakarta.persistence.*
import org.hibernate.annotations.CreationTimestamp
import java.time.Instant
@Entity
class PasswordResetToken(
@Id
@Convert(converter = EncryptionConverter::class)
val token: String,
@OneToOne(targetEntity = User::class, fetch = FetchType.EAGER)
@@ -1,5 +1,6 @@
package de.grimsi.gameyfin.users.entities
import de.grimsi.gameyfin.core.security.EncryptionConverter
import jakarta.annotation.Nullable
import jakarta.persistence.*
import jakarta.validation.constraints.NotNull
@@ -23,6 +24,7 @@ class User(
@Nullable
@Column(unique = true)
@Convert(converter = EncryptionConverter::class)
var email: String,
var email_confirmed: Boolean = false,