Rework encryption

This commit is contained in:
crschnick
2025-01-07 10:10:12 +00:00
parent c3165b806c
commit 1090c8d0d6
23 changed files with 300 additions and 156 deletions

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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))

View File

@@ -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);
}

View File

@@ -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];
}

View File

@@ -391,9 +391,7 @@ public class StandardStorage extends DataStorage {
}
deleteLeftovers();
if (dispose) {
dataStorageUserHandler.save();
}
dataStorageUserHandler.save();
dataStorageSyncHandler.afterStorageSave();
if (dispose) {
disposed = true;

View File

@@ -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);
}
}

View 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;
}
}
}

View File

@@ -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);
}

View File

@@ -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());

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -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,

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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)

View File

@@ -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());

View File

@@ -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();

View File

@@ -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();
},

View File

@@ -54,7 +54,7 @@ public class IncusContainerStore
install.checkComplete();
Validators.nonNull(containerName);
if (identity != null) {
identity.checkComplete(false, false, false);
identity.checkComplete(false);
}
}

View File

@@ -52,7 +52,7 @@ public class LxdContainerStore
cmd.checkComplete();
Validators.nonNull(containerName);
if (identity != null) {
identity.checkComplete(false, false, false);
identity.checkComplete(false);
}
}