diff --git a/app/src/main/java/io/xpipe/app/pwman/KeeperPasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/KeeperPasswordManager.java index 0f4aa2c71..3f3b5984b 100644 --- a/app/src/main/java/io/xpipe/app/pwman/KeeperPasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/KeeperPasswordManager.java @@ -1,11 +1,9 @@ package io.xpipe.app.pwman; -import io.xpipe.app.comp.base.ButtonComp; -import io.xpipe.app.comp.base.ListBoxViewComp; +import com.fasterxml.jackson.annotation.JsonIgnore; import io.xpipe.app.core.AppI18n; import io.xpipe.app.ext.ProcessControlProvider; import io.xpipe.app.issue.ErrorEventFactory; -import io.xpipe.app.platform.DerivedObservableList; import io.xpipe.app.platform.OptionsBuilder; import io.xpipe.app.process.*; import io.xpipe.app.secret.SecretManager; @@ -13,26 +11,22 @@ import io.xpipe.app.secret.SecretPromptStrategy; import io.xpipe.app.secret.SecretQueryState; import io.xpipe.app.terminal.TerminalLaunch; import io.xpipe.app.util.AskpassAlert; -import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.*; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; -import javafx.application.Platform; -import javafx.beans.binding.Bindings; import javafx.beans.property.Property; import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.collections.FXCollections; +import javafx.beans.property.SimpleStringProperty; import lombok.Builder; import lombok.Getter; import lombok.ToString; import lombok.extern.jackson.Jacksonized; -import java.nio.charset.StandardCharsets; -import java.time.Instant; +import java.time.Duration; import java.util.List; +import java.util.Random; import java.util.UUID; @JsonTypeName("keeper") @@ -45,6 +39,10 @@ public class KeeperPasswordManager implements PasswordManager { private static final UUID KEEPER_PASSWORD_ID = UUID.randomUUID(); private static ShellControl SHELL; private final Boolean mfa; + private final String totpDuration; + + @JsonIgnore + private boolean hasCompletedRequestInSession; private static synchronized ShellControl getOrStartShell() throws Exception { if (SHELL == null) { @@ -60,13 +58,18 @@ public class KeeperPasswordManager implements PasswordManager { @SuppressWarnings("unused") public static OptionsBuilder createOptions(Property p) { - var mfa = new SimpleObjectProperty<>(p.getValue().getMfa()); + var mfa = new SimpleBooleanProperty(p.getValue().getMfa() != null ? p.getValue().getMfa() : false); + var duration = new SimpleStringProperty(p.getValue().getTotpDuration()); return new OptionsBuilder() .nameAndDescription("keeperUseMfa") .addToggle(mfa) + .name("keeperTotpDuration") + .description(AppI18n.observable("keeperTotpDurationDescription", "login | 12_hours | 24_hours | 30_days | forever")) + .addString(duration) + .hide(mfa.not()) .bind( () -> { - return KeeperPasswordManager.builder().mfa(mfa.get()).build(); + return KeeperPasswordManager.builder().mfa(mfa.get()).totpDuration(duration.get()).build(); }, p); } @@ -122,22 +125,33 @@ public class KeeperPasswordManager implements PasswordManager { .add("--format", "json", "--unmask") .add("--password") .addLiteral(r.getSecretValue()); - FilePath file = null; + FilePath file = sc.getSystemTemporaryDirectory().join("keeper" + Math.abs(new Random().nextInt()) + ".txt"); CommandBuilder fullB; if (mfa != null && mfa) { - var totp = AskpassAlert.queryRaw("Enter Keeper 2FA Code", null, true); - if (totp.getState() != SecretQueryState.NORMAL) { - return null; - } - - var input = """ + var index = getTotpDurationIndex(); + if (hasCompletedRequestInSession && index > 0) { + var input = """ 1 - %s - """.formatted(totp.getSecret().getSecretValue()); - file = sc.getSystemTemporaryDirectory().join("keeper.txt"); - sc.view().writeTextFile(file, input); - fullB = CommandBuilder.of().add(sc.getShellDialect() == ShellDialects.CMD ? "type" : "cat").addFile(file).add("|").add(b); + + """; + sc.view().writeTextFile(file, input); + fullB = CommandBuilder.of().add(sc.getShellDialect() == ShellDialects.CMD ? "type" : "cat").addFile(file).add("|").add(b); + } else { + var totp = AskpassAlert.queryRaw("Enter Keeper 2FA Code", null, true); + if (totp.getState() != SecretQueryState.NORMAL) { + return null; + } + + var input = """ + + 1%s + %s + + """.formatted(index != -1 ? "\n" + getTotpDurationValues().get(index) : "", totp.getSecret().getSecretValue()); + sc.view().writeTextFile(file, input); + fullB = CommandBuilder.of().add(sc.getShellDialect() == ShellDialects.CMD ? "type" : "cat").addFile(file).add("|").add(b); + } } else { fullB = b; } @@ -192,6 +206,9 @@ public class KeeperPasswordManager implements PasswordManager { } var message = !err.isEmpty() ? outPrefix + "\n" + err : outPrefix; + if (message.isEmpty()) { + message = result[0] + "\n" + result[1]; + } ErrorEventFactory.fromMessage(message).expected().handle(); return null; } @@ -205,6 +222,8 @@ public class KeeperPasswordManager implements PasswordManager { return null; } + hasCompletedRequestInSession = true; + var fields = tree.get("fields"); // There multiple schemas if (fields == null || !fields.isArray()) { @@ -255,6 +274,17 @@ public class KeeperPasswordManager implements PasswordManager { } } + private List getTotpDurationValues() { + var values = List.of("login", "12_hours", "24_hours", "30_days", "forever"); + return values; + } + + private int getTotpDurationIndex() { + var values = getTotpDurationValues(); + var index = totpDuration != null ? values.indexOf(totpDuration) : -1; + return index; + } + @Override public String getKeyPlaceholder() { return "Record UID"; @@ -264,4 +294,9 @@ public class KeeperPasswordManager implements PasswordManager { public String getWebsite() { return "https://www.keepersecurity.com"; } + + @Override + public Duration getCacheDuration() { + return (mfa != null && mfa && getTotpDurationIndex() < 1) ? Duration.ofDays(10) : Duration.ofSeconds(3); + } } diff --git a/app/src/main/java/io/xpipe/app/pwman/PasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/PasswordManager.java index 716fb6ac0..ab9ed29c2 100644 --- a/app/src/main/java/io/xpipe/app/pwman/PasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/PasswordManager.java @@ -6,6 +6,7 @@ import io.xpipe.core.SecretValue; import com.fasterxml.jackson.annotation.JsonTypeInfo; import lombok.Value; +import java.time.Duration; import java.util.ArrayList; import java.util.List; @@ -38,6 +39,10 @@ public interface PasswordManager { String getWebsite(); + default Duration getCacheDuration() { + return Duration.ofSeconds(30); + } + @Value class CredentialResult { diff --git a/lang/strings/translations_en.properties b/lang/strings/translations_en.properties index 3708e6fc1..9556f6399 100644 --- a/lang/strings/translations_en.properties +++ b/lang/strings/translations_en.properties @@ -1906,6 +1906,8 @@ testingConnection=Testing connection ... openManagementConsole=Open management console keeperUseMfa=Use 2FA authenticator app keeperUseMfaDescription=Enable this if your Keeper account requires an 2FA TOTP to access passwords. +keeperTotpDuration=Custom 2FA code duration +keeperTotpDurationDescription=Override the default duration on how long a 2FA code is valid. Only applies if your organization policy allows changing the duration.\n\nPossible values are: $VALUES$ extractReusableIdentities=Extract reusable identities identitiesAdded=Identities added syncMode=Sync mode