Skip to content

Commit

Permalink
Merge pull request #109 from modelix/fix-do-not-fall-back-on-a-hard-c…
Browse files Browse the repository at this point in the history
…oded-encryption-key

fix(workspace-manger): do not fall back on a hard-coded encryption key
  • Loading branch information
odzhychko authored Jul 10, 2024
2 parents 34c3afc + 3f9bb4e commit da3b79b
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 78 deletions.
4 changes: 4 additions & 0 deletions commitlint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ module.exports = {
"always",
[
"deps",
"instances-manager",
"workspace-client",
"workspace-job",
"workspace-manager"
],
],
"subject-case": [0, 'never'],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.modelix.workspace.manager

import org.jasypt.util.text.AES256TextEncryptor
import org.modelix.workspaces.Credentials
import org.modelix.workspaces.GitRepository
import org.modelix.workspaces.Workspace

/**
*
*/
class CredentialsEncryption(key: String) {
companion object {
private const val ENCRYPTED_PREFIX = "encrypted:"
}


private val encryptor = AES256TextEncryptor()

init {
encryptor.setPassword(key)
}

fun decrypt(credentials: Credentials): Credentials {
return Credentials(decrypt(credentials.user), decrypt(credentials.password))
}

fun encrypt(credentials: Credentials): Credentials {
return Credentials(encrypt(credentials.user), encrypt(credentials.password))
}

private fun encrypt(input: String): String {
return if (input.startsWith(ENCRYPTED_PREFIX)) {
input
} else {
ENCRYPTED_PREFIX + encryptor.encrypt(input)
}
}

private fun decrypt(input: String): String {
return if (input.startsWith(ENCRYPTED_PREFIX)) {
encryptor.decrypt(input.drop(ENCRYPTED_PREFIX.length))
} else {
input
}
}
}

fun CredentialsEncryption.copyWithEncryptedCredentials(workspace: Workspace): Workspace =
workspace.copy(gitRepositories = workspace.gitRepositories.map(::copyWithEncryptedCredentials))

fun CredentialsEncryption.copyWithDecryptedCredentials(workspace: Workspace): Workspace =
workspace.copy(gitRepositories = workspace.gitRepositories.map(::copyWithDecryptedCredentials))

fun CredentialsEncryption.copyWithEncryptedCredentials(gitRepository: GitRepository): GitRepository =
gitRepository.copy(credentials = gitRepository.credentials?.run(::encrypt))

fun CredentialsEncryption.copyWithDecryptedCredentials(gitRepository: GitRepository): GitRepository =
gitRepository.copy(credentials = gitRepository.credentials?.run(::decrypt))
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import org.eclipse.jgit.transport.http.JDKHttpConnection
import org.eclipse.jgit.transport.http.JDKHttpConnectionFactory
import org.modelix.workspaces.GitRepository
import java.io.File
import java.io.OutputStream
import java.net.*
import java.util.zip.ZipOutputStream

Expand Down Expand Up @@ -64,14 +63,13 @@ class GitRepositoryManager(val config: GitRepository, val workspaceDirectory: Fi
}

private fun <C : GitCommand<T>, T, E : TransportCommand<C, T>> applyCredentials(cmd: E): E {
val encryptedCredentials = config.credentials
if (encryptedCredentials != null) {
val decrypted = encryptedCredentials.decrypt()
cmd.setCredentialsProvider(UsernamePasswordCredentialsProvider(decrypted.user, decrypted.password))
val credentials = config.credentials
if (credentials != null) {
cmd.setCredentialsProvider(UsernamePasswordCredentialsProvider(credentials.user, credentials.password))
cmd.setTransportConfigCallback { transport ->
transport?.setAuthenticator(object : Authenticator() {
override fun getPasswordAuthentication(): PasswordAuthentication {
return PasswordAuthentication(decrypted.user, decrypted.password.toCharArray())
return PasswordAuthentication(credentials.user, credentials.password.toCharArray())
}
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import org.modelix.workspaces.WorkspaceHash
import org.modelix.workspaces.WorkspacePersistence
import java.io.File

class WorkspaceManager {
class WorkspaceManager(private val credentialsEncryption: CredentialsEncryption) {
private val workspacePersistence = WorkspacePersistence()
private val directory: File = run {
// The workspace will contain git repositories. Avoid cloning them into an existing repository.
Expand All @@ -38,7 +38,8 @@ class WorkspaceManager {

@Synchronized
fun update(workspace: Workspace): WorkspaceHash {
val hash = workspacePersistence.update(workspace)
val workspaceWithEncryptedCredentials = credentialsEncryption.copyWithEncryptedCredentials(workspace)
val hash = workspacePersistence.update(workspaceWithEncryptedCredentials)
synchronized(buildJobs) {
buildJobs.removeByWorkspaceId(workspace.id)
FileUtils.deleteQuietly(getDownloadFile(hash))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ import java.util.zip.ZipOutputStream

fun Application.workspaceManagerModule() {

val manager = WorkspaceManager()

val credentialsEncryption = createCredentialEncryption()
val manager = WorkspaceManager(credentialsEncryption)
val maxBodySize = environment.config.property("modelix.maxBodySize").getString()

install(Routing)
Expand Down Expand Up @@ -259,7 +261,8 @@ fun Application.workspaceManagerModule() {
return@intercept
}
val repo = repos[repoIndex]
val repoManager = GitRepositoryManager(repo, manager.getWorkspaceDirectory(workspace))
val gitRepoWitDecryptedCredentials = credentialsEncryption.copyWithDecryptedCredentials(repo)
val repoManager = GitRepositoryManager(gitRepoWitDecryptedCredentials, manager.getWorkspaceDirectory(workspace))
if (!repoManager.repoDirectory.exists()) {
repoManager.updateRepo()
}
Expand Down Expand Up @@ -728,13 +731,7 @@ fun Application.workspaceManagerModule() {
val decryptCredentials = call.request.queryParameters["decryptCredentials"] == "true"
val decrypted = if (decryptCredentials) {
// TODO check permission to read decrypted credentials
workspace.copy(
gitRepositories = workspace.gitRepositories.map {
it.copy(
credentials = it.credentials?.decrypt()
)
}
)
credentialsEncryption.copyWithDecryptedCredentials(workspace)
} else {
workspace
}
Expand All @@ -755,7 +752,8 @@ fun Application.workspaceManagerModule() {
return@get
}

val gitRepoManager = GitRepositoryManager(gitRepo, manager.getWorkspaceDirectory(workspace))
val gitRepoWitDecryptedCredentials = credentialsEncryption.copyWithDecryptedCredentials(gitRepo)
val gitRepoManager = GitRepositoryManager(gitRepoWitDecryptedCredentials, manager.getWorkspaceDirectory(workspace))
gitRepoManager.updateRepo()
call.respondOutputStream(ContentType.Application.Zip) {
ZipOutputStream(this).use { zip ->
Expand Down Expand Up @@ -875,6 +873,15 @@ fun Application.workspaceManagerModule() {
}
}

private fun createCredentialEncryption(): CredentialsEncryption {
// Secrets mounted as files are more secure than environment variables
// because environment variables can more easily leak or be extracted.
// See https://stackoverflow.com/questions/51365355/kubernetes-secrets-volumes-vs-environment-variables
val credentialsEncryptionKeyFile = File("/secrets/workspacesecret/workspace-credentials-key.txt")
val credentialsEncryptionKey = credentialsEncryptionKeyFile.readLines().first()
return CredentialsEncryption(credentialsEncryptionKey)
}

private fun findGitRepo(folder: File): File? {
if (!folder.exists()) return null
if (folder.name == ".git") return folder.parentFile
Expand Down
11 changes: 7 additions & 4 deletions workspace-manager/src/test/kotlin/CredentialsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.modelix.workspace.manager.CredentialsEncryption
import org.modelix.workspaces.Credentials

class CredentialsTest {

private val credentialsEncryption = CredentialsEncryption("aKey")

@Test
fun testEncryption() {
val credentials = Credentials("user", "1A.23bc\$")
val encrypted = credentials.encrypt()
val decrypted = encrypted.decrypt()
val encrypted = credentialsEncryption.encrypt(credentials)
val decrypted = credentialsEncryption.decrypt(encrypted)
Assertions.assertEquals(credentials.password, decrypted.password)
Assertions.assertArrayEquals(credentials.password.toByteArray(), decrypted.password.toByteArray())
}
Expand All @@ -21,10 +24,10 @@ class CredentialsTest {
val credentials = Credentials("user", "1A.23bc\$")
val serialized = Yaml.default.encodeToString(credentials)
val deserialized = Yaml.default.decodeFromString<Credentials>(serialized)
val encrypted = deserialized.encrypt()
val encrypted = credentialsEncryption.encrypt(deserialized)
val serialized2 = Yaml.default.encodeToString(encrypted)
val deserialized2 = Yaml.default.decodeFromString<Credentials>(serialized2)
val decrypted = deserialized2.decrypt()
val decrypted = credentialsEncryption.decrypt(deserialized2)
Assertions.assertEquals(credentials.password, decrypted.password)
Assertions.assertArrayEquals(credentials.password.toByteArray(), decrypted.password.toByteArray())
}
Expand Down
58 changes: 4 additions & 54 deletions workspaces/src/main/kotlin/org/modelix/workspaces/Workspace.kt
Original file line number Diff line number Diff line change
Expand Up @@ -99,65 +99,15 @@ data class Binding(val project: String? = null,
data class GitRepository(val url: String,
val name: String? = null,
val branch: String = "master",
var commitHash: String? = null,
val commitHash: String? = null,
val paths: List<String> = listOf(),
var credentials: Credentials? = null)
val credentials: Credentials? = null)

@Serializable
data class Credentials(val user: String, val password: String) {
fun decrypt(): Credentials {
return Credentials(decrypt(user), decrypt(password))
}

fun encrypt(): Credentials {
return Credentials(encrypt(user), encrypt(password))
}

private fun encrypt(input: String): String {
return if (input.startsWith(ENCRYPTED_PREFIX)) {
input
} else {
ENCRYPTED_PREFIX + credentialsEncryptor.encrypt(input)
}
}

private fun decrypt(input: String): String {
return if (input.startsWith(ENCRYPTED_PREFIX)) {
credentialsEncryptor.decrypt(input.drop(ENCRYPTED_PREFIX.length))
} else {
input
}
}
}
data class Credentials(val user: String, val password: String)

@Serializable
data class MavenRepository(val url: String)

@Serializable
data class SharedInstance(val name: String = "shared", val allowWrite: Boolean = false)

private const val ENCRYPTED_PREFIX = "encrypted:"
private val credentialsEncryptor = run {
val encryptor = AES256TextEncryptor()
val key = try {
val kubernetesSecretFile = File("/secrets/workspacesecret/workspace-credentials-key.txt")
if (kubernetesSecretFile.exists()) {
kubernetesSecretFile.readLines().first()
} else {
println("${kubernetesSecretFile.absolutePath} doesn't exist")
val localSecretFile = File("../kubernetes/secrets/workspace-credentials-key.txt")
if (localSecretFile.exists()) {
localSecretFile.readLines().first()
} else {
val rand = SecureRandom()
val k = (1..200).map { rand.nextInt(10) }.joinToString("") { it.toString() }
localSecretFile.writeText(k, StandardCharsets.UTF_8)
k
}
}
} catch (e: Exception) {
"g5Mu5trXweUQmijZtZrmjCel81O64FyVxbNFFXgOfsaUYdJ5oZ89BIN8TQy20nQskDOHK0fzJ3bnkqZLUeO6Jng2WR2WJY1V8ef86pyWj6HO5c"
}
encryptor.setPassword(key)
encryptor
}
data class SharedInstance(val name: String = "shared", val allowWrite: Boolean = false)
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ val workspaceListResource = KeycloakResourceType("list", setOf(KeycloakScope.ADD
.createInstance("workspace-list")
val workspaceUploadResourceType = KeycloakResourceType("workspace-upload", setOf(KeycloakScope.READ, KeycloakScope.DELETE), createByUser = true)

class WorkspacePersistence() {
class WorkspacePersistence {
private val WORKSPACE_LIST_KEY = "workspaces"
private val modelClient: RestWebModelClient = RestWebModelClient(getModelServerUrl(), authTokenProvider = serviceAccountTokenProvider)

Expand Down Expand Up @@ -105,7 +105,6 @@ class WorkspacePersistence() {
require(mpsVersion == null || mpsVersion.matches(Regex("""20\d\d\.\d"""))) {
"Invalid major MPS version: '$mpsVersion'. Examples for valid values: '2020.3', '2021.1', '2021.2'."
}
workspace.gitRepositories.forEach { it.credentials = it.credentials?.encrypt() }
val id = workspace.id
val json = Json.encodeToString(workspace)
val hash = WorkspaceHash(HashUtil.sha256(json))
Expand Down

0 comments on commit da3b79b

Please sign in to comment.