Properly handle mstsc localhost certs [stage]

This commit is contained in:
crschnick
2026-01-08 17:33:33 +00:00
parent 1678a10b53
commit b50f1246d3
4 changed files with 144 additions and 6 deletions
@@ -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<MstscRdpClient> 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<RegistryCache> 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()
@@ -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;
@@ -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<byte[]> 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()) {
+1 -1
View File
@@ -1 +1 @@
20.3-6
20.3-7