package com.jetpackduba.gitnuro.credentials import com.jetpackduba.gitnuro.extensions.lockUse import kotlinx.coroutines.sync.Mutex import org.eclipse.jgit.util.Base64 import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec import javax.inject.Inject import javax.inject.Singleton private const val KEY_LENGTH = 16 @Singleton class CredentialsCacheRepository @Inject constructor() { private val credentialsCached = mutableListOf() private val credentialsLock = Mutex(false) // having a random key to encrypt the password may help in case of a memory dump attack private val encryptionKey = getRandomKey() fun getCachedHttpCredentials(url: String): CredentialsType.HttpCredentials? { val credentials = credentialsCached.filterIsInstance().firstOrNull { it.url == url } return credentials?.copy(password = credentials.password.cipherDecrypt()) } suspend fun cacheHttpCredentials(credentials: CredentialsType.HttpCredentials) { cacheHttpCredentials(credentials.url, credentials.userName, credentials.password) } suspend fun cacheHttpCredentials(url: String, userName: String, password: String) { val passwordEncrypted = password.cipherEncrypt() credentialsLock.lockUse { val previouslyCached = credentialsCached.any { it is CredentialsType.HttpCredentials && it.url == url } if (!previouslyCached) { val credentials = CredentialsType.HttpCredentials(url, userName, passwordEncrypted) credentialsCached.add(credentials) } } } suspend fun cacheSshCredentials(sshKey: String, password: String) { credentialsLock.lockUse { val previouslyCached = credentialsCached.any { it is CredentialsType.SshCredentials && it.sshKey == sshKey } if (!previouslyCached) { val credentials = CredentialsType.SshCredentials(sshKey, password) credentialsCached.add(credentials) } } } private fun String.cipherEncrypt(): String { val secretKeySpec = SecretKeySpec(encryptionKey.toByteArray(), "AES") val iv = encryptionKey.toByteArray() val ivParameterSpec = IvParameterSpec(iv) val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec) val encryptedValue = cipher.doFinal(this.toByteArray()) return Base64.encodeBytes(encryptedValue) } private fun String.cipherDecrypt(): String { val secretKeySpec = SecretKeySpec(encryptionKey.toByteArray(), "AES") val iv = encryptionKey.toByteArray() val ivParameterSpec = IvParameterSpec(iv) val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) val decodedValue = Base64.decode(this) val decryptedValue = cipher.doFinal(decodedValue) return String(decryptedValue) } private fun getRandomKey(): String { val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9') + "#!$%=?-_.,@ยต*:;+~".toList() return (1..KEY_LENGTH) .map { allowedChars.random() } .joinToString("") } } sealed interface CredentialsType { data class SshCredentials( val sshKey: String, val password: String, ) : CredentialsType data class HttpCredentials( val url: String, val userName: String, val password: String, ) : CredentialsType }