mirror of
https://github.com/wgtunnel/desktop.git
synced 2026-06-02 00:29:09 +02:00
fix: IPC key bug on systems of various languages
Instead if passing the user to look up their home dir, client now passes full IPC key patch for daemon read.
This commit is contained in:
@@ -13,9 +13,8 @@ fun Project.registerConveyorTask(
|
||||
val outputDir = layout.buildDirectory.dir("conveyor/$subDir")
|
||||
outputs.dir(outputDir)
|
||||
|
||||
(System.getenv("CONVEYOR_SIGNING_KEY") ?: LocalProperties.get("conveyor.signing-key"))?.let {
|
||||
environment("CONVEYOR_SIGNING_KEY", it)
|
||||
}
|
||||
(System.getenv("CONVEYOR_SIGNING_KEY") ?: LocalProperties.get("conveyor.signing-key"))
|
||||
?.let { environment("CONVEYOR_SIGNING_KEY", it) }
|
||||
|
||||
(System.getenv("CONVEYOR_PAT") ?: LocalProperties.get("github.pat"))?.let {
|
||||
environment("CONVEYOR_PAT", it)
|
||||
|
||||
+1
-3
@@ -87,10 +87,8 @@ val serviceModule = module {
|
||||
if (path == Routes.DAEMON_BASE) return@intercept
|
||||
|
||||
val secret = IPC.getIPCSecret()
|
||||
val user = System.getProperty("user.name")
|
||||
val timestamp = System.currentTimeMillis() / 1000
|
||||
|
||||
// extract body string without destroying the payload
|
||||
val bodyString =
|
||||
when (payload) {
|
||||
is TextContent -> payload.text
|
||||
@@ -100,9 +98,9 @@ val serviceModule = module {
|
||||
|
||||
val signature = HmacProtector.generateSignature(secret, timestamp, bodyString)
|
||||
|
||||
context.header(Headers.HMAC_USER, user)
|
||||
context.header(Headers.HMAC_TIMESTAMP, timestamp.toString())
|
||||
context.header(Headers.HMAC_SIGNATURE, signature)
|
||||
context.header(Headers.HMAC_KEY_PATH, IPC.getIpcKeyPath())
|
||||
|
||||
proceedWith(payload)
|
||||
}
|
||||
|
||||
+47
-11
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.daemon.plugin
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.zaneschepke.wireguardautotunnel.core.crypto.HmacProtector
|
||||
import com.zaneschepke.wireguardautotunnel.core.helper.PermissionsHelper
|
||||
import com.zaneschepke.wireguardautotunnel.core.ipc.Headers
|
||||
import com.zaneschepke.wireguardautotunnel.core.ipc.IPC
|
||||
import com.zaneschepke.wireguardautotunnel.core.ipc.Routes
|
||||
@@ -9,6 +10,7 @@ import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import java.nio.file.Paths
|
||||
|
||||
val hmacShieldPlugin =
|
||||
createApplicationPlugin("HmacShield") {
|
||||
@@ -28,21 +30,55 @@ val hmacShieldPlugin =
|
||||
return@onCall
|
||||
}
|
||||
|
||||
val userHint = call.request.headers[Headers.HMAC_USER]
|
||||
val timestamp = call.request.headers[Headers.HMAC_TIMESTAMP]?.toLong() ?: 0L
|
||||
val signature = call.request.headers[Headers.HMAC_SIGNATURE]
|
||||
val keyPathStr =
|
||||
call.request.headers[Headers.HMAC_KEY_PATH]
|
||||
?: return@onCall call.respond(
|
||||
HttpStatusCode.Unauthorized,
|
||||
"Missing IPC key path",
|
||||
)
|
||||
|
||||
if (userHint == null || signature == null) {
|
||||
Logger.w { "Daemon: Rejecting request - Missing Headers" }
|
||||
return@onCall call.respond(HttpStatusCode.Unauthorized, "Identity headers missing")
|
||||
val keyFile =
|
||||
try {
|
||||
Paths.get(keyPathStr).normalize().toAbsolutePath().toFile()
|
||||
} catch (e: Exception) {
|
||||
Logger.w { "Daemon: Invalid path format: $keyPathStr" }
|
||||
return@onCall call.respond(HttpStatusCode.Unauthorized, "Invalid key path")
|
||||
}
|
||||
|
||||
if (keyFile.name != IPC.KEY_FILE || keyFile.parentFile?.name != IPC.USER_FOLDER) {
|
||||
Logger.w {
|
||||
"Daemon: Path does not match expected structure: ${keyFile.absolutePath}"
|
||||
}
|
||||
return@onCall call.respond(
|
||||
HttpStatusCode.Unauthorized,
|
||||
"Invalid key path structure",
|
||||
)
|
||||
}
|
||||
if (!keyFile.isFile) {
|
||||
Logger.w { "Daemon: Key file does not exist: ${keyFile.absolutePath}" }
|
||||
return@onCall call.respond(HttpStatusCode.Unauthorized, "Key file not found")
|
||||
}
|
||||
|
||||
// ensure it is user only owned, as we expect
|
||||
if (!PermissionsHelper.isOwnerOnly(keyFile.toPath())) {
|
||||
Logger.e { "Daemon: Key file permissions are not 0600: ${keyFile.absolutePath}" }
|
||||
return@onCall call.respond(
|
||||
HttpStatusCode.Unauthorized,
|
||||
"Invalid key file permissions",
|
||||
)
|
||||
}
|
||||
|
||||
val secret =
|
||||
IPC.resolveKeyForUser(userHint)
|
||||
?: return@onCall call.respond(
|
||||
HttpStatusCode.Unauthorized,
|
||||
"User not recognized",
|
||||
)
|
||||
keyFile.readText().trim().takeIf { it.isNotBlank() }
|
||||
?: return@onCall call.respond(HttpStatusCode.Unauthorized, "Empty key file")
|
||||
|
||||
val timestamp = call.request.headers[Headers.HMAC_TIMESTAMP]?.toLong() ?: 0L
|
||||
val signature = call.request.headers[Headers.HMAC_SIGNATURE]
|
||||
|
||||
if (signature == null) {
|
||||
Logger.w { "Daemon: Missing HMAC signature" }
|
||||
return@onCall call.respond(HttpStatusCode.Unauthorized, "Missing signature")
|
||||
}
|
||||
|
||||
val bodyText = call.receiveText()
|
||||
|
||||
|
||||
+71
@@ -277,6 +277,77 @@ object PermissionsHelper {
|
||||
}
|
||||
}
|
||||
|
||||
fun isOwnerOnly(path: Path): Boolean {
|
||||
if (!Files.exists(path) || !Files.isRegularFile(path)) {
|
||||
Logger.w { "isOwnerOnly check failed basic validation: $path" }
|
||||
return false
|
||||
}
|
||||
|
||||
return try {
|
||||
if (SystemUtils.IS_OS_WINDOWS) {
|
||||
isOwnerOnlyWindows(path)
|
||||
} else {
|
||||
isOwnerOnlyPosix(path)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.e(e) { "Failed to verify owner-only permissions for: $path" }
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun isOwnerOnlyPosix(path: Path): Boolean {
|
||||
return try {
|
||||
val perms = Files.getPosixFilePermissions(path)
|
||||
// Exact match to what setOwnerOnly applies
|
||||
perms == PosixFilePermissions.fromString(OWNER_ONLY_PRIVATE_FILE)
|
||||
} catch (e: Exception) {
|
||||
Logger.w(e) { "POSIX permission read failed for $path" }
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun isOwnerOnlyWindows(path: Path): Boolean {
|
||||
return try {
|
||||
val process = ProcessBuilder(ICACLS, path.toString()).start()
|
||||
val output = process.inputStream.bufferedReader().readText().trim()
|
||||
val exitCode = process.waitFor()
|
||||
|
||||
if (exitCode != 0) {
|
||||
Logger.w { "icacls failed (exit $exitCode) while checking $path" }
|
||||
return false
|
||||
}
|
||||
|
||||
val currentUser = System.getProperty("user.name").lowercase()
|
||||
|
||||
// Owner must have full control
|
||||
val ownerHasFullControl =
|
||||
output.contains("$currentUser:(F)", ignoreCase = true) ||
|
||||
output.contains("$currentUser:(M)", ignoreCase = true)
|
||||
|
||||
// Block dangerous groups
|
||||
val dangerousGroups = listOf("everyone", "users", "authenticated users", "s-1-5-32-545")
|
||||
val hasDangerousWrite =
|
||||
dangerousGroups.any { group ->
|
||||
output.contains(group, ignoreCase = true) &&
|
||||
(output.contains("$group:(F)", ignoreCase = true) ||
|
||||
output.contains("$group:(M)", ignoreCase = true) ||
|
||||
output.contains("$group:(W)", ignoreCase = true))
|
||||
}
|
||||
|
||||
if (!ownerHasFullControl) {
|
||||
Logger.w { "IPC key owner does not have full control: $path" }
|
||||
}
|
||||
if (hasDangerousWrite) {
|
||||
Logger.w { "Dangerous group write access found on IPC key: $path" }
|
||||
}
|
||||
|
||||
ownerHasFullControl && !hasDangerousWrite
|
||||
} catch (e: Exception) {
|
||||
Logger.w(e) { "Windows ACL check failed for $path" }
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun logWindowsACLs(path: String) {
|
||||
runCatching {
|
||||
val output =
|
||||
|
||||
@@ -4,4 +4,5 @@ object Headers {
|
||||
const val HMAC_USER = "X-HMAC-User"
|
||||
const val HMAC_TIMESTAMP = "X-HMAC-Timestamp"
|
||||
const val HMAC_SIGNATURE = "X-HMAC-Signature"
|
||||
const val HMAC_KEY_PATH = "X-Ipc-Key-Path"
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.ipc
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.zaneschepke.wireguardautotunnel.core.crypto.Crypto
|
||||
import com.zaneschepke.wireguardautotunnel.core.helper.PermissionsHelper
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import org.apache.commons.lang3.SystemUtils
|
||||
|
||||
object IPC {
|
||||
|
||||
@@ -14,28 +10,6 @@ object IPC {
|
||||
const val USER_FOLDER = ".wgtunnel"
|
||||
const val SOCKET_FILE_NAME = "daemon.sock"
|
||||
|
||||
fun resolveKeyForUser(user: String): String? {
|
||||
if (!user.matches(Regex("^[a-zA-Z0-9._-]+$"))) {
|
||||
Logger.w { "Invalid username format: $user" }
|
||||
return null
|
||||
}
|
||||
|
||||
return try {
|
||||
val userHome = getUserHome(user)
|
||||
val keyPath = Paths.get(userHome, USER_FOLDER, KEY_FILE)
|
||||
|
||||
if (Files.exists(keyPath)) {
|
||||
keyPath.toFile().readText().trim().takeIf { it.isNotBlank() }
|
||||
} else {
|
||||
Logger.w { "IPC key not found for user: $user → $keyPath" }
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.e(e) { "Failed to resolve IPC key for user: $user" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// should be called by client ONLY
|
||||
fun getIPCSecret(): String {
|
||||
val ipcFile = File(System.getProperty("user.home"), "${IPC.USER_FOLDER}/${IPC.KEY_FILE}")
|
||||
@@ -52,11 +26,16 @@ object IPC {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUserHome(user: String): String {
|
||||
return when {
|
||||
SystemUtils.IS_OS_WINDOWS -> "C:\\Users\\$user"
|
||||
SystemUtils.IS_OS_MAC_OSX -> "/Users/$user"
|
||||
else -> "/home/$user"
|
||||
fun getIpcKeyPath(): String {
|
||||
val dir = File(System.getProperty("user.home"), USER_FOLDER)
|
||||
if (!dir.exists()) dir.mkdirs()
|
||||
val keyFile = File(dir, KEY_FILE)
|
||||
|
||||
if (!keyFile.exists()) {
|
||||
val secret = Crypto.generateRandomBase64(32)
|
||||
keyFile.writeText(secret)
|
||||
PermissionsHelper.setOwnerOnly(keyFile.toPath())
|
||||
}
|
||||
return keyFile.canonicalPath
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user