mirror of
https://github.com/xpipe-io/xpipe.git
synced 2026-04-22 23:49:09 -04:00
Rework encryption
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
package io.xpipe.app.ext;
|
||||
|
||||
import com.fasterxml.jackson.databind.JavaType;
|
||||
import io.xpipe.app.comp.Comp;
|
||||
|
||||
import javafx.beans.property.Property;
|
||||
|
||||
public interface PrefsHandler {
|
||||
|
||||
<T> void addSetting(String id, Class<T> c, Property<T> property, Comp<?> comp, boolean requiresRestart);
|
||||
<T> void addSetting(String id, JavaType t, Property<T> property, Comp<?> comp, boolean requiresRestart);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package io.xpipe.app.prefs;
|
||||
|
||||
import com.fasterxml.jackson.databind.JavaType;
|
||||
import com.fasterxml.jackson.databind.type.SimpleType;
|
||||
import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.core.*;
|
||||
import io.xpipe.app.core.mode.OperationMode;
|
||||
@@ -551,8 +554,7 @@ public class AppPrefs {
|
||||
private <T> T loadValue(AppPrefsStorageHandler handler, Mapping value) {
|
||||
T def = (T) value.getProperty().getValue();
|
||||
Property<T> property = (Property<T>) value.getProperty();
|
||||
Class<T> clazz = (Class<T>) value.getValueClass();
|
||||
var val = handler.loadObject(value.getKey(), clazz, def);
|
||||
var val = handler.loadObject(value.getKey(), value.getValueType(), def);
|
||||
property.setValue(val);
|
||||
return val;
|
||||
}
|
||||
@@ -607,28 +609,47 @@ public class AppPrefs {
|
||||
|
||||
String key;
|
||||
Property<?> property;
|
||||
Class<?> valueClass;
|
||||
JavaType valueType;
|
||||
boolean vaultSpecific;
|
||||
boolean requiresRestart;
|
||||
String licenseFeatureId;
|
||||
|
||||
public Mapping(
|
||||
String key, Property<?> property, Class<?> valueClass, boolean vaultSpecific, boolean requiresRestart) {
|
||||
String key, Property<?> property, Class<?> valueType, boolean vaultSpecific, boolean requiresRestart) {
|
||||
this.key = key;
|
||||
this.property = property;
|
||||
this.valueClass = valueClass;
|
||||
this.valueType = SimpleType.constructUnsafe(valueType);
|
||||
this.vaultSpecific = vaultSpecific;
|
||||
this.requiresRestart = requiresRestart;
|
||||
this.licenseFeatureId = null;
|
||||
}
|
||||
|
||||
public Mapping(
|
||||
String key, Property<?> property, JavaType valueType, boolean vaultSpecific, boolean requiresRestart) {
|
||||
this.key = key;
|
||||
this.property = property;
|
||||
this.valueType = valueType;
|
||||
this.vaultSpecific = vaultSpecific;
|
||||
this.requiresRestart = requiresRestart;
|
||||
this.licenseFeatureId = null;
|
||||
}
|
||||
|
||||
|
||||
public static class MappingBuilder {
|
||||
|
||||
MappingBuilder valueClass(Class<?> clazz) {
|
||||
this.valueType(TypeFactory.defaultInstance().constructType(clazz));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
private class PrefsHandlerImpl implements PrefsHandler {
|
||||
|
||||
@Override
|
||||
public <T> void addSetting(String id, Class<T> c, Property<T> property, Comp<?> comp, boolean requiresRestart) {
|
||||
var m = new Mapping(id, property, c, false, requiresRestart);
|
||||
public <T> void addSetting(String id, JavaType t, Property<T> property, Comp<?> comp, boolean requiresRestart) {
|
||||
var m = new Mapping(id, property, t, false, requiresRestart);
|
||||
customEntries.put(m, comp);
|
||||
mapping.add(m);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.xpipe.app.prefs;
|
||||
|
||||
import com.fasterxml.jackson.databind.JavaType;
|
||||
import io.xpipe.app.ext.PrefsChoiceValue;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
@@ -16,6 +17,7 @@ import org.apache.commons.io.FileUtils;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static io.xpipe.app.ext.PrefsChoiceValue.getAll;
|
||||
import static io.xpipe.app.ext.PrefsChoiceValue.getSupported;
|
||||
@@ -77,7 +79,7 @@ public class AppPrefsStorageHandler {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T loadObject(String id, Class<T> type, T defaultObject) {
|
||||
public <T> T loadObject(String id, JavaType type, T defaultObject) {
|
||||
var tree = getContent(id);
|
||||
if (tree == null) {
|
||||
TrackEvent.withDebug("Preferences value not found")
|
||||
@@ -87,10 +89,10 @@ public class AppPrefsStorageHandler {
|
||||
return defaultObject;
|
||||
}
|
||||
|
||||
if (PrefsChoiceValue.class.isAssignableFrom(type)) {
|
||||
var all = getAll(type);
|
||||
if (PrefsChoiceValue.class.isAssignableFrom(type.getRawClass())) {
|
||||
List<T> all = (List<T>) getAll(type.getRawClass());
|
||||
if (all != null) {
|
||||
Class<PrefsChoiceValue> cast = (Class<PrefsChoiceValue>) type;
|
||||
Class<PrefsChoiceValue> cast = (Class<PrefsChoiceValue>) type.getRawClass();
|
||||
var in = tree.asText();
|
||||
var found = all.stream()
|
||||
.filter(t -> ((PrefsChoiceValue) t).getId().equalsIgnoreCase(in))
|
||||
|
||||
@@ -45,7 +45,7 @@ public class DataStorageNode {
|
||||
}
|
||||
|
||||
try {
|
||||
var secret = JacksonMapper.getDefault().treeToValue(node, DataStorageSecret.class);
|
||||
var secret = DataStorageSecret.deserialize(node);
|
||||
if (secret == null) {
|
||||
return new DataStorageNode(node, false, true, false);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
package io.xpipe.app.storage;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.util.EncryptionToken;
|
||||
import io.xpipe.app.util.PasswordLockSecretValue;
|
||||
import io.xpipe.app.util.VaultKeySecretValue;
|
||||
import io.xpipe.core.util.EncryptedSecretValue;
|
||||
import io.xpipe.core.util.JacksonMapper;
|
||||
import io.xpipe.core.util.SecretValue;
|
||||
|
||||
@@ -10,11 +16,50 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.NonFinal;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
@Value
|
||||
public class DataStorageSecret {
|
||||
|
||||
public static DataStorageSecret deserialize(JsonNode tree) throws IOException {
|
||||
if (!tree.isObject()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var legacy = JacksonMapper.getDefault().treeToValue(tree, EncryptedSecretValue.class);
|
||||
if (legacy != null) {
|
||||
// Don't cache legacy node
|
||||
return new DataStorageSecret(EncryptionToken.ofVaultKey(), null, legacy.inPlace());
|
||||
}
|
||||
|
||||
var obj = (ObjectNode) tree;
|
||||
if (!obj.has("secret")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var secretTree = obj.required("secret");
|
||||
var secret = JacksonMapper.getDefault().treeToValue(secretTree, SecretValue.class);
|
||||
if (secret == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var hadLock = AppPrefs.get().getLockCrypt().get() != null
|
||||
&& !AppPrefs.get().getLockCrypt().get().isEmpty();
|
||||
var tokenNode = obj.get("encryptedToken");
|
||||
var token =
|
||||
tokenNode != null ? JacksonMapper.getDefault().treeToValue(tokenNode, EncryptionToken.class) : null;
|
||||
if (token == null) {
|
||||
var userToken = hadLock;
|
||||
if (userToken && DataStorageUserHandler.getInstance().getActiveUser() == null) {
|
||||
return null;
|
||||
}
|
||||
token = userToken ? EncryptionToken.ofUser() : EncryptionToken.ofVaultKey();
|
||||
}
|
||||
|
||||
return new DataStorageSecret(token, secretTree, secret);
|
||||
}
|
||||
|
||||
public static DataStorageSecret ofCurrentSecret(SecretValue internalSecret) {
|
||||
var handler = DataStorageUserHandler.getInstance();
|
||||
return new DataStorageSecret(
|
||||
@@ -41,7 +86,7 @@ public class DataStorageSecret {
|
||||
this.internalSecret = internalSecret;
|
||||
}
|
||||
|
||||
public boolean requiresRewrite() {
|
||||
public boolean requiresRewrite(boolean allowUserSecretKey) {
|
||||
var isVault = encryptedToken.isVault();
|
||||
var isUser = encryptedToken.isUser();
|
||||
var userHandler = DataStorageUserHandler.getInstance();
|
||||
@@ -53,12 +98,17 @@ public class DataStorageSecret {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We don't want to use the new user key
|
||||
if (!allowUserSecretKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
var hasUserKey = userHandler.getActiveUser() != null;
|
||||
// Switch from vault to user
|
||||
if (hasUserKey && isVault) {
|
||||
if (hasUserKey && isVault && allowUserSecretKey) {
|
||||
return true;
|
||||
}
|
||||
// Switch from user to vault
|
||||
@@ -69,9 +119,9 @@ public class DataStorageSecret {
|
||||
return false;
|
||||
}
|
||||
|
||||
public JsonNode rewrite() {
|
||||
public JsonNode rewrite(boolean allowUserSecretKey) {
|
||||
var handler = DataStorageUserHandler.getInstance();
|
||||
if (handler != null && handler.getActiveUser() != null && encryptedToken.isUser()) {
|
||||
if (handler != null && handler.getActiveUser() != null && allowUserSecretKey) {
|
||||
var val = new PasswordLockSecretValue(getSecret());
|
||||
originalNode = JacksonMapper.getDefault().valueToTree(val);
|
||||
encryptedToken = EncryptionToken.ofUser();
|
||||
@@ -84,6 +134,24 @@ public class DataStorageSecret {
|
||||
return originalNode;
|
||||
}
|
||||
|
||||
public void serialize(JsonGenerator jgen, boolean allowUserSecretKey) throws IOException {
|
||||
var mapper = JacksonMapper.getDefault();
|
||||
var tree = JsonNodeFactory.instance.objectNode();
|
||||
tree.set("encryptedToken", mapper.valueToTree(getEncryptedToken()));
|
||||
|
||||
// Preserve same output if not changed
|
||||
if (getOriginalNode() != null && !requiresRewrite(allowUserSecretKey)) {
|
||||
tree.set("secret", getOriginalNode());
|
||||
jgen.writeTree(tree);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reencrypt
|
||||
var val = rewrite(allowUserSecretKey);
|
||||
tree.set("secret", val);
|
||||
jgen.writeTree(tree);
|
||||
}
|
||||
|
||||
public char[] getSecret() {
|
||||
return internalSecret != null ? internalSecret.getSecret() : new char[0];
|
||||
}
|
||||
|
||||
@@ -391,9 +391,7 @@ public class StandardStorage extends DataStorage {
|
||||
}
|
||||
|
||||
deleteLeftovers();
|
||||
if (dispose) {
|
||||
dataStorageUserHandler.save();
|
||||
}
|
||||
dataStorageUserHandler.save();
|
||||
dataStorageSyncHandler.afterStorageSave();
|
||||
if (dispose) {
|
||||
disposed = true;
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
package io.xpipe.app.util;
|
||||
|
||||
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
|
||||
import com.fasterxml.jackson.databind.type.SimpleType;
|
||||
import io.xpipe.app.ext.LocalStore;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.storage.*;
|
||||
import io.xpipe.app.terminal.ExternalTerminalType;
|
||||
import io.xpipe.core.util.EncryptedSecretValue;
|
||||
import io.xpipe.core.util.JacksonMapper;
|
||||
import io.xpipe.core.util.SecretValue;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.*;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import java.io.CharArrayReader;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -29,10 +28,10 @@ public class AppJacksonModule extends SimpleModule {
|
||||
addDeserializer(DataStoreEntryRef.class, new DataStoreEntryRefDeserializer());
|
||||
addSerializer(ContextualFileReference.class, new LocalFileReferenceSerializer());
|
||||
addDeserializer(ContextualFileReference.class, new LocalFileReferenceDeserializer());
|
||||
addSerializer(DataStorageSecret.class, new DataStoreSecretSerializer());
|
||||
addDeserializer(DataStorageSecret.class, new DataStoreSecretDeserializer());
|
||||
addSerializer(ExternalTerminalType.class, new ExternalTerminalTypeSerializer());
|
||||
addDeserializer(ExternalTerminalType.class, new ExternalTerminalTypeDeserializer());
|
||||
addSerializer(EncryptedValue.class, new EncryptedValueSerializer());
|
||||
addDeserializer(EncryptedValue.class, new EncryptedValueDeserializer());
|
||||
|
||||
context.addSerializers(_serializers);
|
||||
context.addDeserializers(_deserializers);
|
||||
@@ -76,69 +75,49 @@ public class AppJacksonModule extends SimpleModule {
|
||||
}
|
||||
}
|
||||
|
||||
public static class DataStoreSecretSerializer extends JsonSerializer<DataStorageSecret> {
|
||||
@SuppressWarnings("all")
|
||||
public static class EncryptedValueSerializer extends JsonSerializer<EncryptedValue> {
|
||||
|
||||
@Override
|
||||
public void serialize(DataStorageSecret value, JsonGenerator jgen, SerializerProvider provider)
|
||||
public void serialize(EncryptedValue value, JsonGenerator jgen, SerializerProvider provider)
|
||||
throws IOException {
|
||||
var mapper = JacksonMapper.getDefault();
|
||||
var tree = JsonNodeFactory.instance.objectNode();
|
||||
tree.set("encryptedToken", mapper.valueToTree(value.getEncryptedToken()));
|
||||
|
||||
// Preserve same output if not changed
|
||||
if (value.getOriginalNode() != null && !value.requiresRewrite()) {
|
||||
tree.set("secret", value.getOriginalNode());
|
||||
jgen.writeTree(tree);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reencrypt
|
||||
var val = value.rewrite();
|
||||
tree.set("secret", val);
|
||||
jgen.writeTree(tree);
|
||||
value.getSecret().serialize(jgen, value.allowUserSecretKey());
|
||||
}
|
||||
}
|
||||
|
||||
public static class DataStoreSecretDeserializer extends JsonDeserializer<DataStorageSecret> {
|
||||
@SuppressWarnings("all")
|
||||
public static class EncryptedValueDeserializer extends JsonDeserializer<EncryptedValue> implements ContextualDeserializer {
|
||||
|
||||
private boolean useCurrentSecretKey;
|
||||
private Class<?> type;
|
||||
|
||||
@Override
|
||||
public DataStorageSecret deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
var tree = JacksonMapper.getDefault().readTree(p);
|
||||
if (!tree.isObject()) {
|
||||
return null;
|
||||
}
|
||||
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
|
||||
JavaType wrapperType = property.getType();
|
||||
JavaType valueType = wrapperType.containedType(0);
|
||||
var deserializer = new EncryptedValueDeserializer();
|
||||
var useCurrentSecretKey = !wrapperType.equals(SimpleType.constructUnsafe(EncryptedValue.VaultKey.class));
|
||||
deserializer.useCurrentSecretKey = useCurrentSecretKey;
|
||||
deserializer.type = valueType.getRawClass();
|
||||
return deserializer;
|
||||
}
|
||||
|
||||
var legacy = JacksonMapper.getDefault().treeToValue(tree, EncryptedSecretValue.class);
|
||||
if (legacy != null) {
|
||||
// Don't cache legacy node
|
||||
return new DataStorageSecret(EncryptionToken.ofVaultKey(), null, legacy.inPlace());
|
||||
}
|
||||
|
||||
var obj = (ObjectNode) tree;
|
||||
if (!obj.has("secret")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var secretTree = obj.required("secret");
|
||||
var secret = JacksonMapper.getDefault().treeToValue(secretTree, SecretValue.class);
|
||||
@Override
|
||||
public EncryptedValue<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
Object value;
|
||||
var secret = DataStorageSecret.deserialize(JacksonMapper.getDefault().readTree(p));
|
||||
if (secret == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var hadLock = AppPrefs.get().getLockCrypt().get() != null
|
||||
&& !AppPrefs.get().getLockCrypt().get().isEmpty();
|
||||
var tokenNode = obj.get("encryptedToken");
|
||||
var token =
|
||||
tokenNode != null ? JacksonMapper.getDefault().treeToValue(tokenNode, EncryptionToken.class) : null;
|
||||
if (token == null) {
|
||||
var migrateToUser = hadLock;
|
||||
if (migrateToUser && DataStorageUserHandler.getInstance().getActiveUser() == null) {
|
||||
var raw = JacksonMapper.getDefault().readValue(p, type);
|
||||
if (raw != null) {
|
||||
value = raw;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
token = migrateToUser ? EncryptionToken.ofUser() : EncryptionToken.ofVaultKey();
|
||||
} else {
|
||||
value = JacksonMapper.getDefault().readValue(new CharArrayReader(secret.getSecret()), type);
|
||||
}
|
||||
|
||||
return new DataStorageSecret(token, secretTree, secret);
|
||||
var perUser = useCurrentSecretKey || secret.getEncryptedToken().isUser();
|
||||
return perUser ? new EncryptedValue.CurrentKey<>(value, secret) : new EncryptedValue.VaultKey<>(value, secret);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
68
app/src/main/java/io/xpipe/app/util/EncryptedValue.java
Normal file
68
app/src/main/java/io/xpipe/app/util/EncryptedValue.java
Normal file
@@ -0,0 +1,68 @@
|
||||
package io.xpipe.app.util;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import io.xpipe.app.storage.DataStorageSecret;
|
||||
import io.xpipe.app.storage.DataStorageUserHandler;
|
||||
import io.xpipe.core.util.JacksonMapper;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = EncryptedValue.VaultKey.class),
|
||||
@JsonSubTypes.Type(value = EncryptedValue.CurrentKey.class),
|
||||
})
|
||||
public abstract class EncryptedValue<T> {
|
||||
|
||||
private final T value;
|
||||
private final DataStorageSecret secret;
|
||||
|
||||
public abstract boolean allowUserSecretKey();
|
||||
|
||||
@JsonTypeName("current")
|
||||
public static class CurrentKey<T> extends EncryptedValue<T> {
|
||||
|
||||
public CurrentKey(T value, DataStorageSecret secret) {
|
||||
super(value, secret);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static <T> CurrentKey<T> of(T value) {
|
||||
var handler = DataStorageUserHandler.getInstance();
|
||||
var s = JacksonMapper.getDefault().writeValueAsString(value);
|
||||
var secret = new VaultKeySecretValue(s.toCharArray());
|
||||
return new CurrentKey<>(value, DataStorageSecret.ofSecret(secret,
|
||||
handler.getActiveUser() != null ? EncryptionToken.ofUser() : EncryptionToken.ofVaultKey()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowUserSecretKey() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonTypeName("vault")
|
||||
public static class VaultKey<T> extends EncryptedValue<T> {
|
||||
|
||||
public VaultKey(T value, DataStorageSecret secret) {
|
||||
super(value, secret);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static <T> VaultKey<T> of(T value) {
|
||||
var s = JacksonMapper.getDefault().writeValueAsString(value);
|
||||
var secret = new VaultKeySecretValue(s.toCharArray());
|
||||
return new VaultKey<>(value, DataStorageSecret.ofSecret(secret, EncryptionToken.ofVaultKey()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowUserSecretKey() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,9 +52,9 @@ public interface SecretRetrievalStrategy {
|
||||
@Jacksonized
|
||||
class InPlace implements SecretRetrievalStrategy {
|
||||
|
||||
DataStorageSecret value;
|
||||
InPlaceSecretValue value;
|
||||
|
||||
public InPlace(DataStorageSecret value) {
|
||||
public InPlace(InPlaceSecretValue value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ public interface SecretRetrievalStrategy {
|
||||
@Override
|
||||
public SecretQueryResult query(String prompt) {
|
||||
return new SecretQueryResult(
|
||||
value != null ? value.getInternalSecret() : InPlaceSecretValue.of(""),
|
||||
value,
|
||||
SecretQueryState.NORMAL);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,11 +22,11 @@ import java.util.List;
|
||||
|
||||
public class SecretRetrievalStrategyHelper {
|
||||
|
||||
private static OptionsBuilder inPlace(Property<SecretRetrievalStrategy.InPlace> p, boolean allowUserSecretKey) {
|
||||
private static OptionsBuilder inPlace(Property<SecretRetrievalStrategy.InPlace> p) {
|
||||
var original = p.getValue() != null ? p.getValue().getValue() : null;
|
||||
var secretProperty = new SimpleObjectProperty<>(
|
||||
p.getValue() != null && p.getValue().getValue() != null
|
||||
? p.getValue().getValue().getInternalSecret().inPlace()
|
||||
? p.getValue().getValue()
|
||||
: null);
|
||||
return new OptionsBuilder()
|
||||
.addComp(new SecretFieldComp(secretProperty, true), secretProperty)
|
||||
@@ -38,10 +38,7 @@ public class SecretRetrievalStrategyHelper {
|
||||
newSecret != null ? newSecret.getSecret() : new char[0],
|
||||
original != null ? original.getSecret() : new char[0]);
|
||||
var val = changed
|
||||
? (allowUserSecretKey
|
||||
? DataStorageSecret.ofCurrentSecret(secretProperty.getValue())
|
||||
: DataStorageSecret.ofSecret(
|
||||
secretProperty.getValue(), EncryptionToken.ofVaultKey()))
|
||||
? secretProperty.getValue()
|
||||
: original;
|
||||
return new SecretRetrievalStrategy.InPlace(val);
|
||||
},
|
||||
@@ -91,7 +88,7 @@ public class SecretRetrievalStrategyHelper {
|
||||
}
|
||||
|
||||
public static OptionsBuilder comp(
|
||||
Property<SecretRetrievalStrategy> s, boolean allowNone, boolean allowUserSecretKey) {
|
||||
Property<SecretRetrievalStrategy> s, boolean allowNone) {
|
||||
SecretRetrievalStrategy strat = s.getValue();
|
||||
var inPlace = new SimpleObjectProperty<>(strat instanceof SecretRetrievalStrategy.InPlace i ? i : null);
|
||||
var passwordManager =
|
||||
@@ -102,7 +99,7 @@ public class SecretRetrievalStrategyHelper {
|
||||
if (allowNone) {
|
||||
map.put(AppI18n.observable("app.none"), new OptionsBuilder());
|
||||
}
|
||||
map.put(AppI18n.observable("app.password"), inPlace(inPlace, allowUserSecretKey));
|
||||
map.put(AppI18n.observable("app.password"), inPlace(inPlace));
|
||||
map.put(AppI18n.observable("app.passwordManager"), passwordManager(passwordManager));
|
||||
map.put(AppI18n.observable("app.customCommand"), customCommand(customCommand));
|
||||
map.put(AppI18n.observable("app.prompt"), new OptionsBuilder());
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.xpipe.ext.base.identity;
|
||||
|
||||
import io.xpipe.app.ext.ShellStore;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.EncryptedValue;
|
||||
import io.xpipe.app.util.OptionsBuilder;
|
||||
import io.xpipe.app.util.SecretRetrievalStrategyHelper;
|
||||
|
||||
@@ -58,10 +59,10 @@ public class IdentityChoice {
|
||||
var options = new OptionsBuilder()
|
||||
.nameAndDescription(userChoiceTranslationKey)
|
||||
.addComp(new IdentitySelectComp(ref, user, pass, identityStrategy, allowCustomUserInput), user)
|
||||
.nonNullIf(inPlaceSelected.and(new SimpleBooleanProperty(requireUserInput)))
|
||||
.nonNullIf(inPlaceSelected)
|
||||
.nameAndDescription(passwordChoiceTranslationKey)
|
||||
.sub(SecretRetrievalStrategyHelper.comp(pass, true, true), pass)
|
||||
.nonNullIf(inPlaceSelected.and(new SimpleBooleanProperty(requirePassword)))
|
||||
.sub(SecretRetrievalStrategyHelper.comp(pass, true), pass)
|
||||
.nonNullIf(inPlaceSelected)
|
||||
.disable(refSelected)
|
||||
.hide(refSelected)
|
||||
.name("keyAuthentication")
|
||||
@@ -72,8 +73,7 @@ public class IdentityChoice {
|
||||
gateway != null ? gateway : new SimpleObjectProperty<>(),
|
||||
identityStrategy,
|
||||
null,
|
||||
false,
|
||||
true),
|
||||
false),
|
||||
identityStrategy)
|
||||
.nonNullIf(inPlaceSelected.and(new SimpleBooleanProperty(keyInput)))
|
||||
.disable(refSelected)
|
||||
@@ -89,8 +89,8 @@ public class IdentityChoice {
|
||||
return IdentityValue.InPlace.builder()
|
||||
.identityStore(LocalIdentityStore.builder()
|
||||
.username(user.get())
|
||||
.password(pass.get())
|
||||
.sshIdentity(identityStrategy.get())
|
||||
.password(EncryptedValue.CurrentKey.of(pass.get()))
|
||||
.sshIdentity(EncryptedValue.CurrentKey.of(identityStrategy.get()))
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -71,8 +71,6 @@ public class IdentityMigrationDeserializer extends DelegatingDeserializer {
|
||||
identityStore.put("type", "localIdentity");
|
||||
if (user != null && user.isTextual()) {
|
||||
identityStore.set("username", user);
|
||||
} else {
|
||||
int a = 0;
|
||||
}
|
||||
identityStore.set("password", password);
|
||||
identityStore.set("sshIdentity", identity);
|
||||
|
||||
@@ -15,6 +15,7 @@ import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.EncryptedValue;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
import io.xpipe.app.util.SecretRetrievalStrategy;
|
||||
|
||||
@@ -63,13 +64,13 @@ public class IdentitySelectComp extends Comp<CompStructure<HBox>> {
|
||||
var id = canSync
|
||||
? SyncedIdentityStore.builder()
|
||||
.username(inPlaceUser.getValue())
|
||||
.password(password.getValue())
|
||||
.sshIdentity(identityStrategy.getValue())
|
||||
.password(EncryptedValue.VaultKey.of(password.getValue()))
|
||||
.sshIdentity(EncryptedValue.VaultKey.of(identityStrategy.getValue()))
|
||||
.build()
|
||||
: LocalIdentityStore.builder()
|
||||
.username(inPlaceUser.getValue())
|
||||
.password(password.getValue())
|
||||
.sshIdentity(identityStrategy.getValue())
|
||||
.password(EncryptedValue.CurrentKey.of(password.getValue()))
|
||||
.sshIdentity(EncryptedValue.CurrentKey.of(identityStrategy.getValue()))
|
||||
.build();
|
||||
StoreCreationComp.showCreation(
|
||||
id,
|
||||
|
||||
@@ -16,16 +16,16 @@ import lombok.experimental.SuperBuilder;
|
||||
public abstract class IdentityStore implements SelfReferentialStore, DataStore {
|
||||
|
||||
String username;
|
||||
SecretRetrievalStrategy password;
|
||||
SshIdentityStrategy sshIdentity;
|
||||
|
||||
public abstract SecretRetrievalStrategy getPassword();
|
||||
|
||||
public abstract SshIdentityStrategy getSshIdentity();
|
||||
|
||||
@Override
|
||||
public void checkComplete() throws Throwable {
|
||||
if (password != null) {
|
||||
password.checkComplete();
|
||||
}
|
||||
if (sshIdentity != null) {
|
||||
sshIdentity.checkComplete();
|
||||
}
|
||||
Validators.nonNull(getPassword());
|
||||
Validators.nonNull(getSshIdentity());
|
||||
getPassword().checkComplete();
|
||||
getSshIdentity().checkComplete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ public interface IdentityValue {
|
||||
return new InPlace(identityStore);
|
||||
}
|
||||
|
||||
void checkComplete(boolean requireUser, boolean requirePassword, boolean requireKey) throws Throwable;
|
||||
void checkComplete(boolean requireUser) throws Throwable;
|
||||
|
||||
IdentityStore unwrap();
|
||||
|
||||
@@ -36,24 +36,12 @@ public interface IdentityValue {
|
||||
LocalIdentityStore identityStore;
|
||||
|
||||
@Override
|
||||
public void checkComplete(boolean requireUser, boolean requirePassword, boolean requireKey) throws Throwable {
|
||||
public void checkComplete(boolean requireUser) throws Throwable {
|
||||
Validators.nonNull(identityStore);
|
||||
identityStore.checkComplete();
|
||||
if (requireUser) {
|
||||
Validators.nonNull(identityStore.getUsername());
|
||||
}
|
||||
if (requirePassword) {
|
||||
Validators.nonNull(identityStore.getPassword());
|
||||
}
|
||||
if (identityStore.getPassword() != null) {
|
||||
identityStore.getPassword().checkComplete();
|
||||
}
|
||||
if (requireKey) {
|
||||
Validators.nonNull(identityStore.getSshIdentity());
|
||||
}
|
||||
if (identityStore.getSshIdentity() != null) {
|
||||
identityStore.getSshIdentity().checkComplete();
|
||||
}
|
||||
identityStore.checkComplete();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -76,25 +64,13 @@ public interface IdentityValue {
|
||||
DataStoreEntryRef<IdentityStore> ref;
|
||||
|
||||
@Override
|
||||
public void checkComplete(boolean requireUser, boolean requirePassword, boolean requireKey) throws Throwable {
|
||||
public void checkComplete(boolean requireUser) throws Throwable {
|
||||
Validators.nonNull(ref);
|
||||
Validators.isType(ref, IdentityStore.class);
|
||||
ref.getStore().checkComplete();
|
||||
if (requireUser) {
|
||||
Validators.nonNull(ref.getStore().getUsername());
|
||||
}
|
||||
if (requirePassword) {
|
||||
Validators.nonNull(ref.getStore().getPassword());
|
||||
}
|
||||
if (ref.getStore().getPassword() != null) {
|
||||
ref.getStore().getPassword().checkComplete();
|
||||
}
|
||||
if (requireKey) {
|
||||
Validators.nonNull(ref.getStore().getSshIdentity());
|
||||
}
|
||||
if (ref.getStore().getSshIdentity() != null) {
|
||||
ref.getStore().getSshIdentity().checkComplete();
|
||||
}
|
||||
ref.getStore().checkComplete();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -7,6 +7,7 @@ import io.xpipe.app.ext.DataStoreCreationCategory;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
|
||||
import io.xpipe.app.util.EncryptedValue;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import lombok.Value;
|
||||
@@ -58,8 +59,9 @@ public class LocalIdentityConvertAction implements ActionProvider {
|
||||
var st = ref.getStore();
|
||||
var synced = SyncedIdentityStore.builder()
|
||||
.username(st.getUsername())
|
||||
.password(st.getPassword())
|
||||
.sshIdentity(st.getSshIdentity())
|
||||
.password(EncryptedValue.VaultKey.of(st.getPassword()))
|
||||
.sshIdentity(EncryptedValue.VaultKey.of(st.getSshIdentity()))
|
||||
.perUser(false)
|
||||
.build();
|
||||
StoreCreationComp.showCreation(synced, DataStoreCreationCategory.IDENTITY, dataStoreEntry -> {}, true);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package io.xpipe.ext.base.identity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import io.xpipe.app.util.EncryptedValue;
|
||||
import io.xpipe.app.util.SecretRetrievalStrategy;
|
||||
import io.xpipe.app.util.Validators;
|
||||
import io.xpipe.core.util.ValidationException;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import lombok.Value;
|
||||
@@ -13,4 +17,18 @@ import lombok.extern.jackson.Jacksonized;
|
||||
@Value
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class LocalIdentityStore extends IdentityStore {}
|
||||
public class LocalIdentityStore extends IdentityStore {
|
||||
|
||||
EncryptedValue.CurrentKey<SecretRetrievalStrategy> password;
|
||||
EncryptedValue.CurrentKey<SshIdentityStrategy> sshIdentity;
|
||||
|
||||
@Override
|
||||
public SecretRetrievalStrategy getPassword() {
|
||||
return password != null ? password.getValue() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SshIdentityStrategy getSshIdentity() {
|
||||
return sshIdentity != null ? sshIdentity.getValue() : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import io.xpipe.app.comp.store.StoreEntryWrapper;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.ext.GuiDialog;
|
||||
import io.xpipe.app.storage.*;
|
||||
import io.xpipe.app.util.EncryptedValue;
|
||||
import io.xpipe.app.util.OptionsBuilder;
|
||||
import io.xpipe.app.util.SecretRetrievalStrategyHelper;
|
||||
import io.xpipe.core.store.DataStore;
|
||||
@@ -39,19 +40,19 @@ public class LocalIdentityStoreProvider extends IdentityStoreProvider {
|
||||
.addString(user)
|
||||
.name("passwordAuthentication")
|
||||
.description("passwordAuthenticationDescription")
|
||||
.sub(SecretRetrievalStrategyHelper.comp(pass, true, true), pass)
|
||||
.sub(SecretRetrievalStrategyHelper.comp(pass, true), pass)
|
||||
.name("keyAuthentication")
|
||||
.description("keyAuthenticationDescription")
|
||||
.longDescription("base:sshKey")
|
||||
.sub(
|
||||
SshIdentityStrategyHelper.identity(new SimpleObjectProperty<>(), identity, null, false, true),
|
||||
SshIdentityStrategyHelper.identity(new SimpleObjectProperty<>(), identity, null, false),
|
||||
identity)
|
||||
.bind(
|
||||
() -> {
|
||||
return LocalIdentityStore.builder()
|
||||
.username(user.get())
|
||||
.password(pass.get())
|
||||
.sshIdentity(identity.get())
|
||||
.password(EncryptedValue.CurrentKey.of(pass.get()))
|
||||
.sshIdentity(EncryptedValue.CurrentKey.of(identity.get()))
|
||||
.build();
|
||||
},
|
||||
store)
|
||||
|
||||
@@ -49,8 +49,7 @@ public class SshIdentityStrategyHelper {
|
||||
Property<DataStoreEntryRef<ShellStore>> proxy,
|
||||
Property<SshIdentityStrategy.File> fileProperty,
|
||||
Predicate<Path> perUserFile,
|
||||
boolean allowSync,
|
||||
boolean allowUserSecretKey) {
|
||||
boolean allowSync) {
|
||||
var keyPath = new SimpleStringProperty(
|
||||
fileProperty.getValue() != null && fileProperty.getValue().getFile() != null
|
||||
? fileProperty.getValue().getFile().toAbsoluteFilePath(null)
|
||||
@@ -77,7 +76,7 @@ public class SshIdentityStrategyHelper {
|
||||
.name("keyPassword")
|
||||
.description("sshConfigHost.identityPassphraseDescription")
|
||||
.sub(
|
||||
SecretRetrievalStrategyHelper.comp(keyPasswordProperty, true, allowUserSecretKey),
|
||||
SecretRetrievalStrategyHelper.comp(keyPasswordProperty, true),
|
||||
keyPasswordProperty)
|
||||
.nonNull()
|
||||
.bind(
|
||||
@@ -92,8 +91,7 @@ public class SshIdentityStrategyHelper {
|
||||
Property<DataStoreEntryRef<ShellStore>> proxy,
|
||||
Property<SshIdentityStrategy> strategyProperty,
|
||||
Predicate<Path> perUserFile,
|
||||
boolean allowSync,
|
||||
boolean allowUserSecretKey) {
|
||||
boolean allowSync) {
|
||||
SshIdentityStrategy strat = strategyProperty.getValue();
|
||||
var file = new SimpleObjectProperty<>(
|
||||
strat instanceof SshIdentityStrategy.File f
|
||||
@@ -110,7 +108,7 @@ public class SshIdentityStrategyHelper {
|
||||
map.put(AppI18n.observable("base.none"), new OptionsBuilder());
|
||||
map.put(
|
||||
AppI18n.observable("base.keyFile"),
|
||||
fileIdentity(proxy, file, perUserFile, allowSync, allowUserSecretKey));
|
||||
fileIdentity(proxy, file, perUserFile, allowSync));
|
||||
map.put(AppI18n.observable("base.sshAgent"), agent(agent));
|
||||
map.put(AppI18n.observable("base.pageant"), new OptionsBuilder());
|
||||
map.put(gpgFeature.suffixObservable("base.gpgAgent"), new OptionsBuilder());
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package io.xpipe.ext.base.identity;
|
||||
|
||||
import io.xpipe.app.ext.UserScopeStore;
|
||||
import io.xpipe.app.util.EncryptedValue;
|
||||
import io.xpipe.app.util.SecretRetrievalStrategy;
|
||||
import io.xpipe.app.util.Validators;
|
||||
import io.xpipe.core.util.ValidationException;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
@@ -18,8 +21,20 @@ import lombok.extern.jackson.Jacksonized;
|
||||
@ToString(callSuper = true)
|
||||
public class SyncedIdentityStore extends IdentityStore implements UserScopeStore {
|
||||
|
||||
EncryptedValue<SecretRetrievalStrategy> password;
|
||||
EncryptedValue<SshIdentityStrategy> sshIdentity;
|
||||
boolean perUser;
|
||||
|
||||
@Override
|
||||
public SecretRetrievalStrategy getPassword() {
|
||||
return password != null ? password.getValue() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SshIdentityStrategy getSshIdentity() {
|
||||
return sshIdentity != null ? sshIdentity.getValue() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkComplete() throws Throwable {
|
||||
super.checkComplete();
|
||||
|
||||
@@ -5,6 +5,7 @@ import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.ext.DataStoreCreationCategory;
|
||||
import io.xpipe.app.ext.GuiDialog;
|
||||
import io.xpipe.app.storage.*;
|
||||
import io.xpipe.app.util.EncryptedValue;
|
||||
import io.xpipe.app.util.OptionsBuilder;
|
||||
import io.xpipe.app.util.SecretRetrievalStrategyHelper;
|
||||
import io.xpipe.app.util.Validator;
|
||||
@@ -59,13 +60,13 @@ public class SyncedIdentityStoreProvider extends IdentityStoreProvider {
|
||||
.addString(user)
|
||||
.name("passwordAuthentication")
|
||||
.description("passwordAuthenticationDescription")
|
||||
.sub(SecretRetrievalStrategyHelper.comp(pass, true, true), pass)
|
||||
.sub(SecretRetrievalStrategyHelper.comp(pass, true), pass)
|
||||
.name("keyAuthentication")
|
||||
.description("keyAuthenticationDescription")
|
||||
.longDescription("base:sshKey")
|
||||
.sub(
|
||||
SshIdentityStrategyHelper.identity(
|
||||
new SimpleObjectProperty<>(), identity, path -> perUser.get(), true, true),
|
||||
new SimpleObjectProperty<>(), identity, path -> perUser.get(), true),
|
||||
identity)
|
||||
.check(val -> Validator.create(val, AppI18n.observable("keyNotSynced"), identity, i -> {
|
||||
var wrong = i instanceof SshIdentityStrategy.File f
|
||||
@@ -83,8 +84,8 @@ public class SyncedIdentityStoreProvider extends IdentityStoreProvider {
|
||||
() -> {
|
||||
return SyncedIdentityStore.builder()
|
||||
.username(user.get())
|
||||
.password(pass.get())
|
||||
.sshIdentity(identity.get())
|
||||
.password(perUser.get() ? EncryptedValue.CurrentKey.of(pass.get()) : EncryptedValue.VaultKey.of(pass.get()))
|
||||
.sshIdentity(perUser.get() ? EncryptedValue.CurrentKey.of(identity.get()) : EncryptedValue.VaultKey.of(identity.get()))
|
||||
.perUser(perUser.get())
|
||||
.build();
|
||||
},
|
||||
|
||||
@@ -54,7 +54,7 @@ public class IncusContainerStore
|
||||
install.checkComplete();
|
||||
Validators.nonNull(containerName);
|
||||
if (identity != null) {
|
||||
identity.checkComplete(false, false, false);
|
||||
identity.checkComplete(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ public class LxdContainerStore
|
||||
cmd.checkComplete();
|
||||
Validators.nonNull(containerName);
|
||||
if (identity != null) {
|
||||
identity.checkComplete(false, false, false);
|
||||
identity.checkComplete(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user