diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java b/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java index 0216e45d..433a76b7 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java @@ -337,6 +337,19 @@ public class BeaconClient implements AutoCloseable { } } + @JsonTypeName("daemon") + @Value + @Builder + @Jacksonized + @EqualsAndHashCode(callSuper = false) + public static class DaemonInformation extends ClientInformation { + + @Override + public String toDisplayString() { + return "Daemon"; + } + } + @JsonTypeName("gateway") @Value @Builder diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconConfig.java b/beacon/src/main/java/io/xpipe/beacon/BeaconConfig.java index c1c2dcf8..1e1673f6 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconConfig.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconConfig.java @@ -15,7 +15,7 @@ public class BeaconConfig { private static final String ATTACH_DEBUGGER_PROP = "io.xpipe.beacon.attachDebuggerToDaemon"; private static final String EXEC_DEBUG_PROP = "io.xpipe.beacon.printDaemonOutput"; private static final String EXEC_PROCESS_PROP = "io.xpipe.beacon.customDaemonCommand"; - private static final String DAEMON_ARGUMENTS_PROP = "io.xpipe.beacon.daemonArgs"; + public static final String DAEMON_ARGUMENTS_PROP = "io.xpipe.beacon.daemonArgs"; private static final String LOCAL_PROXY_PROP = "io.xpipe.beacon.localProxy"; public static boolean localProxy() { diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconJacksonModule.java b/beacon/src/main/java/io/xpipe/beacon/BeaconJacksonModule.java index 313a5aa9..ea02cf22 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconJacksonModule.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconJacksonModule.java @@ -10,6 +10,7 @@ public class BeaconJacksonModule extends SimpleModule { context.registerSubtypes( new NamedType(BeaconClient.ApiClientInformation.class), new NamedType(BeaconClient.CliClientInformation.class), + new NamedType(BeaconClient.DaemonInformation.class), new NamedType(BeaconClient.ReachableCheckInformation.class) ); } diff --git a/beacon/src/main/java/io/xpipe/beacon/SecretProviderImpl.java b/beacon/src/main/java/io/xpipe/beacon/SecretProviderImpl.java new file mode 100644 index 00000000..9554f7f8 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/SecretProviderImpl.java @@ -0,0 +1,73 @@ +package io.xpipe.beacon; + +import io.xpipe.core.util.SecretProvider; +import lombok.SneakyThrows; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.Random; + +public class SecretProviderImpl extends SecretProvider { + + private static final String ENCRYPT_ALGO = "AES/GCM/NoPadding"; + private static final int TAG_LENGTH_BIT = 128; + private static final int IV_LENGTH_BYTE = 12; + private static final int AES_KEY_BIT = 128; + private static final byte[] IV = getFixedNonce(IV_LENGTH_BYTE); + + private static byte[] getFixedNonce(int numBytes) { + byte[] nonce = new byte[numBytes]; + new SecureRandom(new byte[] {1, -28, 123}).nextBytes(nonce); + return nonce; + } + + + private static SecretKey getAESKey(int keysize) throws NoSuchAlgorithmException, InvalidKeySpecException { + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + var salt = new byte[16]; + new Random(keysize).nextBytes(salt); + KeySpec spec = new PBEKeySpec(new char[] {'X', 'P', 'E' << 1}, salt, 65536, keysize); + SecretKey secret = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES"); + return secret; + } + + @Override + @SneakyThrows + public byte[] encrypt(byte[] c) { + SecretKey secretKey = getAESKey(AES_KEY_BIT); + Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(TAG_LENGTH_BIT, IV)); + var bytes = cipher.doFinal(c); + bytes = ByteBuffer.allocate(IV.length + bytes.length) + .order(ByteOrder.LITTLE_ENDIAN) + .put(IV) + .put(bytes) + .array(); + return bytes; + } + + @Override + @SneakyThrows + public byte[] decrypt(byte[] c) { + ByteBuffer bb = ByteBuffer.wrap(c).order(ByteOrder.LITTLE_ENDIAN); + byte[] iv = new byte[IV_LENGTH_BYTE]; + bb.get(iv); + byte[] cipherText = new byte[bb.remaining()]; + bb.get(cipherText); + + SecretKey secretKey = getAESKey(AES_KEY_BIT); + Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO); + cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(TAG_LENGTH_BIT, iv)); + return cipher.doFinal(cipherText); + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/FocusExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/FocusExchange.java new file mode 100644 index 00000000..40c23a7d --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/FocusExchange.java @@ -0,0 +1,27 @@ +package io.xpipe.beacon.exchange; + +import io.xpipe.beacon.RequestMessage; +import io.xpipe.beacon.ResponseMessage; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +public class FocusExchange implements MessageExchange { + + @Override + public String getId() { + return "focus"; + } + + @Jacksonized + @Builder + @Value + public static class Request implements RequestMessage { + } + + @Jacksonized + @Builder + @Value + public static class Response implements ResponseMessage { + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/OpenExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/OpenExchange.java new file mode 100644 index 00000000..6eecc77b --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/OpenExchange.java @@ -0,0 +1,31 @@ +package io.xpipe.beacon.exchange; + +import io.xpipe.beacon.RequestMessage; +import io.xpipe.beacon.ResponseMessage; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.util.List; + +public class OpenExchange implements MessageExchange { + + @Override + public String getId() { + return "open"; + } + + @Jacksonized + @Builder + @Value + public static class Request implements RequestMessage { + @NonNull List arguments; + } + + @Jacksonized + @Builder + @Value + public static class Response implements ResponseMessage { + } +} diff --git a/beacon/src/main/java/module-info.java b/beacon/src/main/java/module-info.java index 6b7d5efe..6a9780d2 100644 --- a/beacon/src/main/java/module-info.java +++ b/beacon/src/main/java/module-info.java @@ -1,6 +1,7 @@ import com.fasterxml.jackson.databind.Module; import io.xpipe.beacon.BeaconJacksonModule; import io.xpipe.beacon.BeaconProxyImpl; +import io.xpipe.beacon.SecretProviderImpl; import io.xpipe.beacon.exchange.*; import io.xpipe.beacon.exchange.api.QueryRawDataExchange; import io.xpipe.beacon.exchange.api.QueryTableDataExchange; @@ -8,6 +9,7 @@ import io.xpipe.beacon.exchange.api.QueryTextDataExchange; import io.xpipe.beacon.exchange.cli.*; import io.xpipe.core.util.ProxyFunction; import io.xpipe.core.util.ProxyProvider; +import io.xpipe.core.util.SecretProvider; module io.xpipe.beacon { exports io.xpipe.beacon; @@ -33,6 +35,7 @@ module io.xpipe.beacon { uses ProxyFunction; provides ProxyProvider with BeaconProxyImpl; + provides SecretProvider with SecretProviderImpl; provides Module with BeaconJacksonModule; provides io.xpipe.beacon.exchange.MessageExchange with ForwardExchange, @@ -49,6 +52,8 @@ module io.xpipe.beacon { ProxyFunctionExchange, QueryStoreExchange, StatusExchange, + FocusExchange, + OpenExchange, StopExchange, RenameStoreExchange, RemoveStoreExchange, diff --git a/core/src/main/java/io/xpipe/core/util/JacksonMapper.java b/core/src/main/java/io/xpipe/core/util/JacksonMapper.java index ac76952a..e031f153 100644 --- a/core/src/main/java/io/xpipe/core/util/JacksonMapper.java +++ b/core/src/main/java/io/xpipe/core/util/JacksonMapper.java @@ -1,6 +1,7 @@ package io.xpipe.core.util; import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; @@ -18,6 +19,12 @@ public class JacksonMapper { private static ObjectMapper INSTANCE = new ObjectMapper(); private static boolean init = false; + public static T parse(String s, Class c) throws JsonProcessingException { + var mapper = getDefault(); + var tree = mapper.readTree(s); + return mapper.treeToValue(tree, c); + } + static { ObjectMapper objectMapper = BASE; objectMapper.enable(SerializationFeature.INDENT_OUTPUT); diff --git a/core/src/main/java/io/xpipe/core/util/SecretProvider.java b/core/src/main/java/io/xpipe/core/util/SecretProvider.java new file mode 100644 index 00000000..3c62fcaa --- /dev/null +++ b/core/src/main/java/io/xpipe/core/util/SecretProvider.java @@ -0,0 +1,16 @@ +package io.xpipe.core.util; + +import java.util.ServiceLoader; + +public abstract class SecretProvider { + + private static final SecretProvider INSTANCE = ServiceLoader.load(ModuleLayer.boot(), SecretProvider.class).findFirst().orElseThrow(); + + public abstract byte[] encrypt(byte[] c); + + public abstract byte[] decrypt(byte[] c); + + public static SecretProvider get() { + return INSTANCE; + } +} diff --git a/core/src/main/java/io/xpipe/core/util/SecretValue.java b/core/src/main/java/io/xpipe/core/util/SecretValue.java index ab697ce4..0d2d5db8 100644 --- a/core/src/main/java/io/xpipe/core/util/SecretValue.java +++ b/core/src/main/java/io/xpipe/core/util/SecretValue.java @@ -5,6 +5,7 @@ import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import java.nio.ByteBuffer; +import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Base64; @@ -16,24 +17,32 @@ public class SecretValue { String value; - public static SecretValue create(String s) { + public static SecretValue encrypt(char[] c) { + if (c == null) { + return null; + } + + var utf8 = StandardCharsets.UTF_8.encode(CharBuffer.wrap(c)); + var bytes = new byte[utf8.limit()]; + utf8.get(bytes); + Arrays.fill(c, (char) 0); + bytes = SecretProvider.get().encrypt(bytes); + var base64 = Base64.getEncoder().encodeToString(bytes); + return new SecretValue(base64.replace("/", "-")); + } + + public static SecretValue encrypt(String s) { if (s == null) { return null; } - if (s.length() < 2) { - return new SecretValue(s); - } - - return new SecretValue(Base64.getEncoder().encodeToString(s.getBytes(StandardCharsets.UTF_8))); + return encrypt(s.toCharArray()); } - public void withSecretValue(Consumer chars) { - var bytes = Base64.getDecoder().decode(value); - var buffer = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(bytes)); - var array = buffer.array(); - chars.accept(array); - Arrays.fill(array, (char) 0); + public void withSecretValue(Consumer con) { + var chars = decryptChars(); + con.accept(chars); + Arrays.fill(chars, (char) 0); } @Override @@ -45,14 +54,28 @@ public class SecretValue { return value; } - public String getSecretValue() { - if (value.length() < 2) { - return value; - } + public char[] decryptChars() { try { - return new String(Base64.getDecoder().decode(value), StandardCharsets.UTF_8); - } catch (Exception exception) { - return ""; + var bytes = Base64.getDecoder().decode(value.replace("-", "/")); + bytes = SecretProvider.get().decrypt(bytes); + var charBuffer = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(bytes)); + var chars = new char[charBuffer.limit()]; + charBuffer.get(chars); + return chars; + } catch (Exception ex) { + return new char[0]; } } + + public String decrypt() { + return new String(decryptChars()); + } + + public static SecretValue ofSecret(String s) { + return new SecretValue(s); + } + + public String getSecretValue() { + return decrypt(); + } } diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index 8f90f276..506da871 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -28,6 +28,7 @@ open module io.xpipe.core { uses io.xpipe.core.util.ProxyProvider; uses io.xpipe.core.util.ProxyManagerProvider; uses io.xpipe.core.util.DataStateProvider; + uses io.xpipe.core.util.SecretProvider; provides WriteMode with WriteMode.Replace, WriteMode.Append, WriteMode.Prepend; provides com.fasterxml.jackson.databind.Module with diff --git a/extension/src/main/java/io/xpipe/extension/DataStoreProvider.java b/extension/src/main/java/io/xpipe/extension/DataStoreProvider.java index 9c9bcae1..e97f18a9 100644 --- a/extension/src/main/java/io/xpipe/extension/DataStoreProvider.java +++ b/extension/src/main/java/io/xpipe/extension/DataStoreProvider.java @@ -50,6 +50,10 @@ public interface DataStoreProvider { default void storageInit() throws Exception { } + default boolean isShareable() { + return false; + } + String queryInformationString(DataStore store, int length) throws Exception; public String toSummaryString(DataStore store, int length); diff --git a/extension/src/main/java/io/xpipe/extension/fxcomps/impl/SecretFieldComp.java b/extension/src/main/java/io/xpipe/extension/fxcomps/impl/SecretFieldComp.java index f271a9d6..1fcbc4eb 100644 --- a/extension/src/main/java/io/xpipe/extension/fxcomps/impl/SecretFieldComp.java +++ b/extension/src/main/java/io/xpipe/extension/fxcomps/impl/SecretFieldComp.java @@ -22,7 +22,7 @@ public class SecretFieldComp extends Comp> { var text = new PasswordField(); text.setText(value.getValue() != null ? value.getValue().getSecretValue() : null); text.textProperty().addListener((c, o, n) -> { - value.setValue(n != null && n.length() > 0 ? SecretValue.create(n) : null); + value.setValue(n != null && n.length() > 0 ? SecretValue.encrypt(n) : null); }); value.addListener((c, o, n) -> { PlatformThread.runLaterIfNeeded(() -> {