From b50f1246d3f0103e91b35df271ed3bde9dc8acc4 Mon Sep 17 00:00:00 2001 From: crschnick Date: Thu, 8 Jan 2026 17:33:33 +0000 Subject: [PATCH] Properly handle mstsc localhost certs [stage] --- .../java/io/xpipe/app/rdp/MstscRdpClient.java | 85 +++++++++++++++++-- .../java/io/xpipe/app/util/Deobfuscator.java | 5 ++ .../io/xpipe/app/util/WindowsRegistry.java | 58 +++++++++++++ version | 2 +- 4 files changed, 144 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/io/xpipe/app/rdp/MstscRdpClient.java b/app/src/main/java/io/xpipe/app/rdp/MstscRdpClient.java index bd01be0e3..21516eef5 100644 --- a/app/src/main/java/io/xpipe/app/rdp/MstscRdpClient.java +++ b/app/src/main/java/io/xpipe/app/rdp/MstscRdpClient.java @@ -1,11 +1,13 @@ package io.xpipe.app.rdp; +import io.xpipe.app.core.AppCache; import io.xpipe.app.platform.OptionsBuilder; import io.xpipe.app.prefs.ExternalApplicationType; import io.xpipe.app.process.CommandBuilder; import io.xpipe.app.process.LocalShell; +import io.xpipe.app.util.GlobalTimer; import io.xpipe.app.util.RdpConfig; -import io.xpipe.app.util.ThreadHelper; +import io.xpipe.app.util.WindowsRegistry; import io.xpipe.core.SecretValue; import javafx.beans.property.Property; @@ -17,7 +19,10 @@ import lombok.Value; import lombok.extern.jackson.Jacksonized; import org.apache.commons.io.FileUtils; +import java.time.Duration; import java.util.Map; +import java.util.Optional; +import java.util.UUID; @JsonTypeName("mstsc") @Value @@ -25,6 +30,16 @@ import java.util.Map; @Builder public class MstscRdpClient implements ExternalApplicationType.PathApplication, ExternalRdpClient { + @Value + @Jacksonized + @Builder + public static class RegistryCache { + String usernameHint; + byte[] certHash; + } + + private static int launchCounter = 0; + @SuppressWarnings("unused") static OptionsBuilder createOptions(Property property) { var smartSizing = new SimpleObjectProperty<>(property.getValue().isSmartSizing()); @@ -43,13 +58,28 @@ public class MstscRdpClient implements ExternalApplicationType.PathApplication, @Override public void launch(RdpLaunchConfig configuration) throws Exception { var adaptedRdpConfig = getAdaptedConfig(configuration); + + prepareLocalhostRegistryCache(configuration); + var file = writeRdpConfigFile(configuration.getTitle(), adaptedRdpConfig); LocalShell.getShell() - .executeSimpleCommand(CommandBuilder.of().add(getExecutable()).addFile(file.toString())); - ThreadHelper.runFailableAsync(() -> { - ThreadHelper.sleep(1000); + .command(CommandBuilder.of().add(getExecutable()).addFile(file.toString())).execute(); + + GlobalTimer.delay(() -> { FileUtils.deleteQuietly(file.toFile()); - }); + }, Duration.ofSeconds(1)); + + var localhost = configuration.getConfig().get("full address").orElseThrow().getValue().startsWith("localhost"); + if (localhost) { + var counter = ++launchCounter; + GlobalTimer.delay(() -> { + if (counter != launchCounter) { + return; + } + + saveLocalhostRegistryCache(configuration.getStoreId()); + }, Duration.ofSeconds(15)); + } } @Override @@ -79,6 +109,51 @@ public class MstscRdpClient implements ExternalApplicationType.PathApplication, return adapted; } + private void saveLocalhostRegistryCache(UUID entry) { + var ex = WindowsRegistry.local().keyExists(WindowsRegistry.HKEY_CURRENT_USER, "Software\\Microsoft\\Terminal Server Client\\Servers\\localhost"); + if (!ex) { + return; + } + + var user = WindowsRegistry.local().readStringValueIfPresent(WindowsRegistry.HKEY_CURRENT_USER, + "Software\\Microsoft\\Terminal Server Client\\Servers\\localhost", "UsernameHint").orElse(null); + var cert = WindowsRegistry.local().readBinaryValueIfPresent(WindowsRegistry.HKEY_CURRENT_USER, + "Software\\Microsoft\\Terminal Server Client\\Servers\\localhost", "CertHash").orElse(null); + if (user == null && cert == null) { + return; + } + + AppCache.update("rdp-" + entry, RegistryCache.builder().usernameHint(user).certHash(cert).build()); + } + + private Optional getLocalhostRegistryCache(UUID entry) { + RegistryCache found = AppCache.getNonNull("rdp-" + entry, RegistryCache.class, () -> null); + return Optional.ofNullable(found); + } + + private void prepareLocalhostRegistryCache(RdpLaunchConfig configuration) { + WindowsRegistry.local().deleteKey(WindowsRegistry.HKEY_CURRENT_USER, + "Software\\Microsoft\\Terminal Server Client\\Servers\\localhost"); + + var localhost = configuration.getConfig().get("full address").orElseThrow().getValue().startsWith("localhost"); + if (localhost) { + var found = getLocalhostRegistryCache(configuration.getStoreId()); + if (found.isPresent()) { + var user = found.get().getUsernameHint(); + if (user != null) { + WindowsRegistry.local().setStringValue(WindowsRegistry.HKEY_CURRENT_USER, + "Software\\Microsoft\\Terminal Server Client\\Servers\\localhost", "UsernameHint", user); + } + + var cert = found.get().getCertHash(); + if (cert != null) { + WindowsRegistry.local().setBinaryValue(WindowsRegistry.HKEY_CURRENT_USER, + "Software\\Microsoft\\Terminal Server Client\\Servers\\localhost", "CertHash", cert); + } + } + } + } + private String encrypt(SecretValue password) throws Exception { var ps = LocalShell.getLocalPowershell().orElseThrow(); var cmd = ps.command(CommandBuilder.of() diff --git a/app/src/main/java/io/xpipe/app/util/Deobfuscator.java b/app/src/main/java/io/xpipe/app/util/Deobfuscator.java index ac874df48..f5a59f7ee 100644 --- a/app/src/main/java/io/xpipe/app/util/Deobfuscator.java +++ b/app/src/main/java/io/xpipe/app/util/Deobfuscator.java @@ -1,6 +1,7 @@ package io.xpipe.app.util; import io.xpipe.app.core.AppNames; +import io.xpipe.app.core.AppProperties; import io.xpipe.app.ext.ProcessControlProvider; import io.xpipe.app.process.ShellDialect; import io.xpipe.app.process.ShellDialects; @@ -46,6 +47,10 @@ public class Deobfuscator { } private static boolean canDeobfuscate() { + if (AppProperties.get().isDevelopmentEnvironment()) { + return false; + } + // We probably can't run .bat scripts in this case if (OsType.ofLocal() == OsType.WINDOWS && ProcessControlProvider.get().getEffectiveLocalDialect() != ShellDialects.CMD) { return false; diff --git a/app/src/main/java/io/xpipe/app/util/WindowsRegistry.java b/app/src/main/java/io/xpipe/app/util/WindowsRegistry.java index c06802a4c..de4ebd1e5 100644 --- a/app/src/main/java/io/xpipe/app/util/WindowsRegistry.java +++ b/app/src/main/java/io/xpipe/app/util/WindowsRegistry.java @@ -89,6 +89,64 @@ public abstract class WindowsRegistry { } } + public void setStringValue(int hkey, String key, String valueName, String value) { + if (!isLibrarySupported()) { + return; + } + + try { + Advapi32Util.registryCreateKey(hkey(hkey), key); + Advapi32Util.registrySetStringValue(hkey(hkey), key, valueName, value); + } catch (Win32Exception ignored) {} + } + + public void setBinaryValue(int hkey, String key, String valueName, byte[] value) { + if (!isLibrarySupported()) { + return; + } + + try { + Advapi32Util.registryCreateKey(hkey(hkey), key); + Advapi32Util.registrySetBinaryValue(hkey(hkey), key, valueName, value); + } catch (Win32Exception ignored) {} + } + + public void deleteKey(int hkey, String key) { + if (!isLibrarySupported()) { + return; + } + + try { + Advapi32Util.registryDeleteKey(hkey(hkey), key); + } catch (Win32Exception ignored) {} + } + + public void deleteValue(int hkey, String key, String valueName) { + if (!isLibrarySupported()) { + return; + } + + try { + Advapi32Util.registryDeleteValue(hkey(hkey), key, valueName); + } catch (Win32Exception ignored) {} + } + + public Optional readBinaryValueIfPresent(int hkey, String key, String valueName) { + if (!isLibrarySupported()) { + return Optional.empty(); + } + + try { + if (!Advapi32Util.registryValueExists(hkey(hkey), key, valueName)) { + return Optional.empty(); + } + + return Optional.of(Advapi32Util.registryGetBinaryValue(hkey(hkey), key, valueName)); + } catch (Win32Exception ignored) { + return Optional.empty(); + } + } + @Override public boolean keyExists(int hkey, String key) { if (!isLibrarySupported()) { diff --git a/version b/version index 106736cde..f66d7a9b8 100644 --- a/version +++ b/version @@ -1 +1 @@ -20.3-6 +20.3-7