KeyStorePersistence.kt
package de.pflugradts.passbird.adapter.keystore
import de.pflugradts.kotlinextensions.tryCatching
import de.pflugradts.passbird.application.util.SystemOperation
import de.pflugradts.passbird.application.util.withScrambledBytes
import de.pflugradts.passbird.domain.model.shell.MAX_ASCII_VALUE
import de.pflugradts.passbird.domain.model.shell.MIN_ASCII_VALUE
import de.pflugradts.passbird.domain.model.shell.PlainShell
import de.pflugradts.passbird.domain.model.shell.PlainShell.Companion.SECURE_RANDOM
import de.pflugradts.passbird.domain.model.shell.Shell
import de.pflugradts.passbird.domain.model.shell.Shell.Companion.shellOf
import java.nio.file.Path
import java.security.KeyStore
import java.security.KeyStore.PasswordProtection
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.SecretKeySpec
internal const val SECRET_ALIAS = "PwMan3Secret"
private const val ALGORITHM = "AES"
const val KEYSTORE_KEY_BITS = 128
internal class KeyStorePersistence(private val systemOperation: SystemOperation) {
fun loadKey(openKeyStore: () -> KeyStore, password: PlainShell, path: Path) = tryCatching {
val passwordChars = password.toCharArray()
try {
systemOperation.newInputStream(path).use {
val keyStore = openKeyStore()
keyStore.load(it, passwordChars)
val secret = keyStore.getKey(SECRET_ALIAS, passwordChars)
withScrambledBytes(secret.encoded) { shellOf(it) }
}
} finally {
passwordChars.scramble()
password.scramble()
}
}
fun storeKey(openKeyStore: () -> KeyStore, password: PlainShell, path: Path) {
val passwordChars = password.toCharArray()
try {
val keyGenerator = KeyGenerator.getInstance(ALGORITHM)
keyGenerator.init(KEYSTORE_KEY_BITS)
persistKey(openKeyStore, keyGenerator.generateKey(), passwordChars, path)
} finally {
passwordChars.scramble()
password.scramble()
}
}
fun storeExistingKey(openKeyStore: () -> KeyStore, key: Shell, password: PlainShell, path: Path) {
val keyBytes = key.toByteArray()
val passwordChars = password.toCharArray()
try {
persistKey(openKeyStore, SecretKeySpec(keyBytes, ALGORITHM), passwordChars, path)
} finally {
keyBytes.scramble()
key.scramble()
passwordChars.scramble()
password.scramble()
}
}
private fun persistKey(openKeyStore: () -> KeyStore, secretKey: SecretKey, passwordChars: CharArray, path: Path) {
systemOperation.writeToSensitiveFile(path) { outputStream ->
val keyStore = openKeyStore()
keyStore.load(null, null)
keyStore.setEntry(
SECRET_ALIAS,
KeyStore.SecretKeyEntry(secretKey),
PasswordProtection(passwordChars),
)
keyStore.store(outputStream, passwordChars)
}
}
}
private fun CharArray.scramble() = indices.forEach {
this[it] = (PlainShell.SECURE_RANDOM.nextInt(1 + MAX_ASCII_VALUE - MIN_ASCII_VALUE) + MIN_ASCII_VALUE).toChar()
}
private fun ByteArray.scramble() = SECURE_RANDOM.nextBytes(this)