diff --git a/doc/licenses/bouncycastle-fips-1.0.2.1/LICENSE b/doc/licenses/bouncycastle-fips-1.0.2.1/LICENSE
new file mode 100644
index 000000000..a02bc176b
--- /dev/null
+++ b/doc/licenses/bouncycastle-fips-1.0.2.1/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2000 - 2021 The Legion of the Bouncy Castle Inc.
+(https://www.bouncycastle.org)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/doc/licenses/bouncycastle-fips-1.0.2.1/README b/doc/licenses/bouncycastle-fips-1.0.2.1/README
new file mode 100644
index 000000000..4d92a89ee
--- /dev/null
+++ b/doc/licenses/bouncycastle-fips-1.0.2.1/README
@@ -0,0 +1,8 @@
+BouncyCastle FIPS Distribution (https://www.bouncycastle.org/fips-java)
+-----------------------------------------------------------------------
+
+ Version: 1.0.2.1
+ From: 'The Legion of Bouncy Castle' (https://www.bouncycastle.org)
+ License(s):
+ MIT (bundled/bouncycastle-fips-1.0.2.1/LICENSE)
+
diff --git a/doc/licenses/bouncycastle-fips-1.0.2.1/dep-coordinates.txt b/doc/licenses/bouncycastle-fips-1.0.2.1/dep-coordinates.txt
new file mode 100644
index 000000000..854f49a58
--- /dev/null
+++ b/doc/licenses/bouncycastle-fips-1.0.2.1/dep-coordinates.txt
@@ -0,0 +1 @@
+org.bouncycastle:bc-fips:jar:1.0.2.1
diff --git a/doc/licenses/jetbrains-annotations-13.0/README b/doc/licenses/jetbrains-annotations-13.0/README
new file mode 100644
index 000000000..e09618cab
--- /dev/null
+++ b/doc/licenses/jetbrains-annotations-13.0/README
@@ -0,0 +1,9 @@
+Annotations for JVM-based languages
+(https://github.com/JetBrains/java-annotations)
+-----------------------------------------------
+
+ Version: 13.0
+ From: 'JetBrains s.r.o.' (http://www.jetbrains.com)
+ License(s):
+ Apache v2.0
+
diff --git a/doc/licenses/jetbrains-annotations-13.0/dep-coordinates.txt b/doc/licenses/jetbrains-annotations-13.0/dep-coordinates.txt
new file mode 100644
index 000000000..bb558cd6c
--- /dev/null
+++ b/doc/licenses/jetbrains-annotations-13.0/dep-coordinates.txt
@@ -0,0 +1 @@
+org.jetbrains:annotations:jar:13.0
diff --git a/doc/licenses/kotlin-1.5.30/NOTICE.txt b/doc/licenses/kotlin-1.5.30/NOTICE.txt
new file mode 100644
index 000000000..3efb9a198
--- /dev/null
+++ b/doc/licenses/kotlin-1.5.30/NOTICE.txt
@@ -0,0 +1,2 @@
+Kotlin Compiler
+Copyright 2010-2020 JetBrains s.r.o and respective authors and developers
diff --git a/doc/licenses/kotlin-1.5.30/README b/doc/licenses/kotlin-1.5.30/README
new file mode 100644
index 000000000..f2de926e9
--- /dev/null
+++ b/doc/licenses/kotlin-1.5.30/README
@@ -0,0 +1,8 @@
+Kotlin (https://kotlinlang.org/)
+--------------------------------
+
+ Version: 1.5.30
+ From: 'JetBrains s.r.o and respective authors and developers'
+ License(s):
+ Apache v2.0
+
diff --git a/doc/licenses/kotlin-1.5.30/dep-coordinates.txt b/doc/licenses/kotlin-1.5.30/dep-coordinates.txt
new file mode 100644
index 000000000..474a373f0
--- /dev/null
+++ b/doc/licenses/kotlin-1.5.30/dep-coordinates.txt
@@ -0,0 +1,5 @@
+org.jetbrains.kotlin:kotlin-reflect:jar:1.5.30
+org.jetbrains.kotlin:kotlin-stdlib:jar:1.5.30
+org.jetbrains.kotlin:kotlin-stdlib-common:jar:1.5.30
+org.jetbrains.kotlin:kotlin-stdlib-jdk8:jar:1.5.30
+org.jetbrains.kotlin:kotlin-stdlib-jdk7:jar:1.5.30
diff --git a/doc/licenses/kotlinx-serialization-1.2.1/NOTICE.txt b/doc/licenses/kotlinx-serialization-1.2.1/NOTICE.txt
new file mode 100644
index 000000000..16f7e9bee
--- /dev/null
+++ b/doc/licenses/kotlinx-serialization-1.2.1/NOTICE.txt
@@ -0,0 +1,2 @@
+kotlinx.serialization library.
+Copyright 2017-2019 JetBrains s.r.o and respective authors and developers
diff --git a/doc/licenses/kotlinx-serialization-1.2.1/README b/doc/licenses/kotlinx-serialization-1.2.1/README
new file mode 100644
index 000000000..5ded4539e
--- /dev/null
+++ b/doc/licenses/kotlinx-serialization-1.2.1/README
@@ -0,0 +1,8 @@
+Kotlin Serialization (https://github.com/Kotlin/kotlinx.serialization)
+----------------------------------------------------------------------
+
+ Version: 1.2.1
+ From: 'JetBrains s.r.o and respective authors and developers'
+ License(s):
+ Apache v2.0
+
diff --git a/doc/licenses/kotlinx-serialization-1.2.1/dep-coordinates.txt b/doc/licenses/kotlinx-serialization-1.2.1/dep-coordinates.txt
new file mode 100644
index 000000000..e6fe5522d
--- /dev/null
+++ b/doc/licenses/kotlinx-serialization-1.2.1/dep-coordinates.txt
@@ -0,0 +1,2 @@
+org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:jar:1.2.1
+org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:jar:1.2.1
diff --git a/doc/licenses/ksm-sdk-16.2.1/LICENSE b/doc/licenses/ksm-sdk-16.2.1/LICENSE
new file mode 100644
index 000000000..b63322a17
--- /dev/null
+++ b/doc/licenses/ksm-sdk-16.2.1/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Keeper Security
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/doc/licenses/ksm-sdk-16.2.1/README b/doc/licenses/ksm-sdk-16.2.1/README
new file mode 100644
index 000000000..4bf0527cf
--- /dev/null
+++ b/doc/licenses/ksm-sdk-16.2.1/README
@@ -0,0 +1,9 @@
+Keeper Secrets Manager Java SDK
+(https://github.com/Keeper-Security/secrets-manager)
+----------------------------------------------------
+
+ Version: 16.2.1
+ From: 'Keeper Security' (https://www.keepersecurity.com/)
+ License(s):
+ MIT (bundled/ksm-sdk-16.2.1/LICENSE)
+
diff --git a/doc/licenses/ksm-sdk-16.2.1/dep-coordinates.txt b/doc/licenses/ksm-sdk-16.2.1/dep-coordinates.txt
new file mode 100644
index 000000000..5b9330316
--- /dev/null
+++ b/doc/licenses/ksm-sdk-16.2.1/dep-coordinates.txt
@@ -0,0 +1 @@
+com.keepersecurity.secrets-manager:core:jar:16.2.1
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/.ratignore b/extensions/guacamole-vault/modules/guacamole-vault-ksm/.ratignore
new file mode 100644
index 000000000..e69de29bb
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/pom.xml b/extensions/guacamole-vault/modules/guacamole-vault-ksm/pom.xml
new file mode 100644
index 000000000..ac3b75f9a
--- /dev/null
+++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/pom.xml
@@ -0,0 +1,98 @@
+
+
+
+
+ 4.0.0
+ org.apache.guacamole
+ guacamole-vault-ksm
+ jar
+ 1.4.0
+ guacamole-vault-ksm
+ http://guacamole.apache.org/
+
+
+ org.apache.guacamole
+ guacamole-vault
+ 1.4.0
+ ../../
+
+
+
+ 1.5.30
+
+
+
+
+
+
+ org.apache.guacamole
+ guacamole-ext
+ provided
+
+
+
+
+ org.apache.guacamole
+ guacamole-vault-base
+ 1.4.0
+
+
+
+ com.keepersecurity.secrets-manager
+ core
+ 16.2.1
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib-common
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib-jdk8
+
+
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ ${kotlin.version}
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib-jdk8
+ ${kotlin.version}
+
+
+
+
+
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProvider.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProvider.java
new file mode 100644
index 000000000..a24dfc875
--- /dev/null
+++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProvider.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.vault.ksm;
+
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.vault.VaultAuthenticationProvider;
+
+/**
+ * VaultAuthenticationProvider implementation which reads secrets from Keeper
+ * Secrets Manager
+ */
+public class KsmAuthenticationProvider extends VaultAuthenticationProvider {
+
+ /**
+ * Creates a new KsmKeyVaultAuthenticationProvider which reads secrets
+ * from a configured Keeper Secrets Manager.
+ *
+ * @throws GuacamoleException
+ * If configuration details cannot be read from guacamole.properties.
+ */
+ public KsmAuthenticationProvider() throws GuacamoleException {
+ super(new KsmAuthenticationProviderModule());
+ }
+
+ @Override
+ public String getIdentifier() {
+ return "keeper-secrets-manager";
+ }
+
+}
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java
new file mode 100644
index 000000000..df3adf8c6
--- /dev/null
+++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.vault.ksm;
+
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.vault.VaultAuthenticationProviderModule;
+import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService;
+import org.apache.guacamole.vault.ksm.secret.KsmSecretService;
+import org.apache.guacamole.vault.conf.VaultConfigurationService;
+import org.apache.guacamole.vault.ksm.secret.KsmClient;
+import org.apache.guacamole.vault.secret.VaultSecretService;
+
+/**
+ * Guice module which configures injections specific to Keeper Secrets
+ * Manager support.
+ */
+public class KsmAuthenticationProviderModule
+ extends VaultAuthenticationProviderModule {
+
+ /**
+ * Creates a new KsmAuthenticationProviderModule which
+ * configures dependency injection for the Keeper Secrets Manager
+ * authentication provider and related services.
+ *
+ * @throws GuacamoleException
+ * If configuration details in guacamole.properties cannot be parsed.
+ */
+ public KsmAuthenticationProviderModule() throws GuacamoleException {}
+
+ @Override
+ protected void configureVault() {
+
+ // Bind services specific to Keeper Secrets Manager
+ bind(KsmClient.class);
+ bind(VaultConfigurationService.class).to(KsmConfigurationService.class);
+ bind(VaultSecretService.class).to(KsmSecretService.class);
+ }
+
+}
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigProperty.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigProperty.java
new file mode 100644
index 000000000..aaddb0de0
--- /dev/null
+++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigProperty.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.vault.ksm.conf;
+
+import com.keepersecurity.secretsManager.core.InMemoryStorage;
+import com.keepersecurity.secretsManager.core.KeyValueStorage;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleServerException;
+import org.apache.guacamole.properties.GuacamoleProperty;
+
+/**
+ * A GuacamoleProperty whose value is Keeper Secrets Manager {@link KeyValueStorage}
+ * object. The value of this property must be base64-encoded JSON, as output by
+ * the Keeper Commander CLI tool via the "sm client add" command.
+ */
+public abstract class KsmConfigProperty implements GuacamoleProperty {
+
+ @Override
+ public KeyValueStorage parseValue(String value) throws GuacamoleException {
+
+ // If no property provided, return null.
+ if (value == null)
+ return null;
+
+ // Parse base64 value as KSM config storage
+ try {
+ return new InMemoryStorage(value);
+ }
+ catch (IllegalArgumentException e) {
+ throw new GuacamoleServerException("Invalid base64 configuration "
+ + "for Keeper Secrets Manager.", e);
+ }
+
+ }
+
+}
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java
new file mode 100644
index 000000000..398b9bb3a
--- /dev/null
+++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.vault.ksm.conf;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.environment.Environment;
+import org.apache.guacamole.properties.BooleanGuacamoleProperty;
+import org.apache.guacamole.vault.conf.VaultConfigurationService;
+import com.keepersecurity.secretsManager.core.SecretsManagerOptions;
+
+/**
+ * Service for retrieving configuration information regarding the Keeper
+ * Secrets Manager authentication extension.
+ */
+@Singleton
+public class KsmConfigurationService extends VaultConfigurationService {
+
+ /**
+ * The Guacamole server environment.
+ */
+ @Inject
+ private Environment environment;
+
+ /**
+ * The name of the file which contains the YAML mapping of connection
+ * parameter token to secrets within Keeper Secrets Manager.
+ */
+ private static final String TOKEN_MAPPING_FILENAME = "ksm-token-mapping.yml";
+
+ /**
+ * The base64-encoded configuration information generated by the Keeper
+ * Commander CLI tool.
+ */
+ private static final KsmConfigProperty KSM_CONFIG = new KsmConfigProperty() {
+
+ @Override
+ public String getName() {
+ return "ksm-config";
+ }
+ };
+
+ /**
+ * Whether unverified server certificates should be accepted.
+ */
+ private static final BooleanGuacamoleProperty ALLOW_UNVERIFIED_CERT = new BooleanGuacamoleProperty() {
+
+ @Override
+ public String getName() {
+ return "ksm-allow-unverified-cert";
+ }
+ };
+
+ /**
+ * Creates a new KsmConfigurationService which reads the configuration
+ * from "ksm-token-mapping.yml". The token mapping is a YAML file which
+ * lists each connection parameter token and the title of the secret from
+ * which the value for that token should be read.
+ */
+ public KsmConfigurationService() {
+ super(TOKEN_MAPPING_FILENAME);
+ }
+
+ /**
+ * Return whether unverified server certificates should be accepted when
+ * communicating with Keeper Secrets Manager.
+ *
+ * @return
+ * true if unverified server certificates should be accepted, false
+ * otherwise.
+ *
+ * @throws GuacamoleException
+ * If the value specified within guacamole.properties cannot be
+ * parsed.
+ */
+ public boolean getAllowUnverifiedCertificate() throws GuacamoleException {
+ return environment.getProperty(ALLOW_UNVERIFIED_CERT, false);
+ }
+
+ /**
+ * Returns the options required to authenticate with Keeper Secrets Manager
+ * when retrieving secrets. These options are read from the contents of
+ * base64-encoded JSON configuration data generated by the Keeper Commander
+ * CLI tool.
+ *
+ * @return
+ * The options that should be used when connecting to Keeper Secrets
+ * Manager when retrieving secrets.
+ *
+ * @throws GuacamoleException
+ * If required properties are not specified within
+ * guacamole.properties or cannot be parsed.
+ */
+ public SecretsManagerOptions getSecretsManagerOptions() throws GuacamoleException {
+ return new SecretsManagerOptions(environment.getRequiredProperty(KSM_CONFIG), null,
+ getAllowUnverifiedCertificate());
+ }
+}
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java
new file mode 100644
index 000000000..a72a2fdaa
--- /dev/null
+++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java
@@ -0,0 +1,491 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.vault.ksm.secret;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.keepersecurity.secretsManager.core.Hosts;
+import com.keepersecurity.secretsManager.core.KeeperFile;
+import com.keepersecurity.secretsManager.core.KeeperRecord;
+import com.keepersecurity.secretsManager.core.KeeperRecordData;
+import com.keepersecurity.secretsManager.core.KeeperRecordField;
+import com.keepersecurity.secretsManager.core.KeeperSecrets;
+import com.keepersecurity.secretsManager.core.Login;
+import com.keepersecurity.secretsManager.core.Notation;
+import com.keepersecurity.secretsManager.core.SecretsManager;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Client which retrieves records from Keeper Secrets Manager, allowing
+ * content-based record retrieval. Note that because KSM is zero-knowledge,
+ * searching or indexing based on content can only be accomplished by
+ * retrieving and indexing everything. Except for record UIDs (which contain no
+ * information), it's not possible for the server to perform a search of
+ * content on the client's behalf. The client has to perform its own search.
+ */
+@Singleton
+public class KsmClient {
+
+ /**
+ * Logger for this class.
+ */
+ private static final Logger logger = LoggerFactory.getLogger(KsmClient.class);
+
+ /**
+ * Service for retrieving configuration information.
+ */
+ @Inject
+ private KsmConfigurationService confService;
+
+ /**
+ * The publicly-accessible URL for Keeper's documentation covering Keeper
+ * notation.
+ */
+ private static final String KEEPER_NOTATION_DOC_URL =
+ "https://docs.keeper.io/secrets-manager/secrets-manager/about/keeper-notation";
+
+ /**
+ * The regular expression that Keeper notation must match to be related to
+ * file retrieval. As the Keeper SDK provides mutually-exclusive for
+ * retrieving secret values and files via notation, the notation must first
+ * be tested to determine whether it refers to a file.
+ */
+ private static final Pattern KEEPER_FILE_NOTATION = Pattern.compile("^(keeper://)?[^/]*/file/.+");
+
+ /**
+ * The maximum amount of time that an entry will be stored in the cache
+ * before being refreshed, in milliseconds.
+ */
+ private static final long CACHE_INTERVAL = 5000;
+
+ /**
+ * Read/write lock which guards access to all cached data, including the
+ * timestamp recording the last time the cache was refreshed. Readers of
+ * the cache must first acquire (and eventually release) the read lock, and
+ * writers of the cache must first acquire (and eventually release) the
+ * write lock.
+ */
+ private final ReadWriteLock cacheLock = new ReentrantReadWriteLock();
+
+ /**
+ * The timestamp that the cache was last refreshed, in milliseconds, as
+ * returned by System.currentTimeMillis(). This value is automatically
+ * updated if {@link #validateCache()} refreshes the cache. This value must
+ * not be accessed without {@link #cacheLock} acquired appropriately.
+ */
+ private volatile long cacheTimestamp = 0;
+
+ /**
+ * The full cached set of secrets last retrieved from Keeper Secrets
+ * Manager. This value is automatically updated if {@link #validateCache()}
+ * refreshes the cache. This value must not be accessed without
+ * {@link #cacheLock} acquired appropriately.
+ */
+ private KeeperSecrets cachedSecrets = null;
+
+ /**
+ * All records retrieved from Keeper Secrets Manager, where each key is the
+ * UID of the corresponding record. The contents of this Map are
+ * automatically updated if {@link #validateCache()} refreshes the cache.
+ * This Map must not be accessed without {@link #cacheLock} acquired
+ * appropriately.
+ */
+ private final Map cachedRecordsByUid = new HashMap<>();
+
+ /**
+ * All records retrieved from Keeper Secrets Manager, where each key is the
+ * hostname or IP address of the corresponding record. The hostname or IP
+ * address of a record is determined by {@link Hosts} fields, thus a record
+ * may be associated with multiple hosts. If a record is associated with
+ * multiple hosts, there will be multiple references to that record within
+ * this Map. The contents of this Map are automatically updated if
+ * {@link #validateCache()} refreshes the cache. This Map must not be
+ * accessed without {@link #cacheLock} acquired appropriately. Before using
+ * a value from this Map, {@link #cachedAmbiguousHosts} must first be
+ * checked to verify that there is indeed only one record associated with
+ * that host.
+ */
+ private final Map cachedRecordsByHost = new HashMap<>();
+
+ /**
+ * The set of all hostnames or IP addresses that are associated with
+ * multiple records, and thus cannot uniquely identify a record. The
+ * contents of this Set are automatically updated if
+ * {@link #validateCache()} refreshes the cache. This Set must not be
+ * accessed without {@link #cacheLock} acquired appropriately.This Set
+ * must be checked before using a value retrieved from
+ * {@link #cachedRecordsByHost}.
+ */
+ private final Set cachedAmbiguousHosts = new HashSet<>();
+
+ /**
+ * All records retrieved from Keeper Secrets Manager, where each key is the
+ * username of the corresponding record. The username of a record is
+ * determined by {@link Login} fields, thus a record may be associated with
+ * multiple users. If a record is associated with multiple users, there
+ * will be multiple references to that record within this Map. The contents
+ * of this Map are automatically updated if {@link #validateCache()}
+ * refreshes the cache. This Map must not be accessed without
+ * {@link #cacheLock} acquired appropriately. Before using a value from
+ * this Map, {@link #cachedAmbiguousUsernames} must first be checked to
+ * verify that there is indeed only one record associated with that user.
+ */
+ private final Map cachedRecordsByUsername = new HashMap<>();
+
+ /**
+ * The set of all usernames that are associated with multiple records, and
+ * thus cannot uniquely identify a record. The contents of this Set are
+ * automatically updated if {@link #validateCache()} refreshes the cache.
+ * This Set must not be accessed without {@link #cacheLock} acquired
+ * appropriately.This Set must be checked before using a value retrieved
+ * from {@link #cachedRecordsByUsername}.
+ */
+ private final Set cachedAmbiguousUsernames = new HashSet<>();
+
+ /**
+ * Validates that all cached data is current with respect to
+ * {@link #CACHE_INTERVAL}, refreshing data from the server as needed.
+ *
+ * @throws GuacamoleException
+ * If an error occurs preventing the cached data from being refreshed.
+ */
+ private void validateCache() throws GuacamoleException {
+
+ long currentTime = System.currentTimeMillis();
+
+ // Perform a read-only check that the cache has actually expired before
+ // continuing
+ cacheLock.readLock().lock();
+ try {
+ if (currentTime - cacheTimestamp < CACHE_INTERVAL)
+ return;
+ }
+ finally {
+ cacheLock.readLock().unlock();
+ }
+
+ cacheLock.writeLock().lock();
+ try {
+
+ // Cache may have been updated since the read-only check. Re-verify
+ // that the cache has expired before continuing with a full refresh
+ if (currentTime - cacheTimestamp < CACHE_INTERVAL)
+ return;
+
+ // Attempt to pull all records first, allowing that operation to
+ // succeed/fail BEFORE we clear out the last cached success
+ KeeperSecrets secrets = SecretsManager.getSecrets(confService.getSecretsManagerOptions());
+ List records = secrets.getRecords();
+
+ // Store all secrets within cache
+ cachedSecrets = secrets;
+
+ // Clear unambiguous cache of all records by UID
+ cachedRecordsByUid.clear();
+
+ // Clear cache of host-based records
+ cachedAmbiguousHosts.clear();
+ cachedRecordsByHost.clear();
+
+ // Clear cache of login-based records
+ cachedAmbiguousUsernames.clear();
+ cachedRecordsByUsername.clear();
+
+ // Store all records, sorting each into host-based and login-based
+ // buckets (note that a single record may be associated with
+ // multiple hosts and logins)
+ records.forEach(record -> {
+
+ // Store based on UID ...
+ cachedRecordsByUid.put(record.getRecordUid(), record);
+
+ // ... and standard fields ...
+ KeeperRecordData data = record.getData();
+ addRecordForHosts(record, (Hosts) data.getField(Hosts.class));
+ addRecordForLogin(record, (Login) data.getField(Login.class));
+
+ // ... and custom fields
+ List custom = data.getCustom();
+ if (custom != null) {
+ custom.forEach(field -> {
+ if (field instanceof Hosts)
+ addRecordForHosts(record, (Hosts) field);
+ else if (field instanceof Login)
+ addRecordForLogin(record, (Login) field);
+ });
+ }
+
+ });
+
+ // Cache has been refreshed
+ this.cacheTimestamp = System.currentTimeMillis();
+
+ }
+ finally {
+ cacheLock.writeLock().unlock();
+ }
+
+ }
+
+ /**
+ * Associates the given record with each of the hosts in the given Hosts
+ * field. The given Hosts field may be null. Both {@link #cachedRecordsByHost}
+ * and {@link #cachedAmbiguousHosts} are updated appropriately. The write
+ * lock of {@link #cacheLock} must already be acquired before invoking this
+ * function.
+ *
+ * @param record
+ * The record to associate with the hosts in the given field.
+ *
+ * @param hosts
+ * The Hosts field containing the hosts that the given record should be
+ * associated with. This may be null.
+ */
+ private void addRecordForHosts(KeeperRecord record, Hosts hosts) {
+
+ if (hosts == null)
+ return;
+
+ hosts.getValue().stream().map(host -> host.getHostName())
+ .forEachOrdered(hostname -> {
+
+ KeeperRecord existing = cachedRecordsByHost.putIfAbsent(hostname, record);
+ if (existing != null && record != existing)
+ cachedAmbiguousHosts.add(hostname);
+
+ });
+
+ }
+
+ /**
+ * Associates the given record with each of the usernames in the given
+ * Login field. The given Hosts field may be null. Both
+ * {@link #cachedRecordsByUsername} and {@link #cachedAmbiguousUsernames}
+ * are updated appropriately. The write lock of {@link #cacheLock} must
+ * already be acquired before invoking this function.
+ *
+ * @param record
+ * The record to associate with the hosts in the given field.
+ *
+ * @param login
+ * The Login field containing the usernames that the given record
+ * should be associated with. This may be null.
+ */
+ private void addRecordForLogin(KeeperRecord record, Login login) {
+
+ if (login == null)
+ return;
+
+ login.getValue().stream()
+ .forEachOrdered(username -> {
+
+ KeeperRecord existing = cachedRecordsByUsername.putIfAbsent(username, record);
+ if (existing != null && record != existing)
+ cachedAmbiguousUsernames.add(username);
+
+ });
+
+ }
+
+ /**
+ * Returns all records accessible via Keeper Secrets Manager. The records
+ * returned are arbitrarily ordered.
+ *
+ * @return
+ * An unmodifiable Collection of all records accessible via Keeper
+ * Secrets Manager, in no particular order.
+ *
+ * @throws GuacamoleException
+ * If an error occurs that prevents records from being retrieved.
+ */
+ public Collection getRecords() throws GuacamoleException {
+ validateCache();
+ cacheLock.readLock().lock();
+ try {
+ return Collections.unmodifiableCollection(cachedRecordsByUid.values());
+ }
+ finally {
+ cacheLock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Returns the record having the given UID. If no such record exists, null
+ * is returned.
+ *
+ * @param uid
+ * The UID of the record to return.
+ *
+ * @return
+ * The record having the given UID, or null if there is no such record.
+ *
+ * @throws GuacamoleException
+ * If an error occurs that prevents the record from being retrieved.
+ */
+ public KeeperRecord getRecord(String uid) throws GuacamoleException {
+ validateCache();
+ cacheLock.readLock().lock();
+ try {
+ return cachedRecordsByUid.get(uid);
+ }
+ finally {
+ cacheLock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Returns the record associated with the given hostname or IP address. If
+ * no such record exists, or there are multiple such records, null is
+ * returned.
+ *
+ * @param hostname
+ * The hostname of the record to return.
+ *
+ * @return
+ * The record associated with the given hostname, or null if there is
+ * no such record or multiple such records.
+ *
+ * @throws GuacamoleException
+ * If an error occurs that prevents the record from being retrieved.
+ */
+ public KeeperRecord getRecordByHost(String hostname) throws GuacamoleException {
+ validateCache();
+ cacheLock.readLock().lock();
+ try {
+
+ if (cachedAmbiguousHosts.contains(hostname))
+ return null;
+
+ return cachedRecordsByHost.get(hostname);
+
+ }
+ finally {
+ cacheLock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Returns the record associated with the given username. If no such record
+ * exists, or there are multiple such records, null is returned.
+ *
+ * @param username
+ * The username of the record to return.
+ *
+ * @return
+ * The record associated with the given username, or null if there is
+ * no such record or multiple such records.
+ *
+ * @throws GuacamoleException
+ * If an error occurs that prevents the record from being retrieved.
+ */
+ public KeeperRecord getRecordByLogin(String username) throws GuacamoleException {
+ validateCache();
+ cacheLock.readLock().lock();
+ try {
+
+ if (cachedAmbiguousUsernames.contains(username))
+ return null;
+
+ return cachedRecordsByUsername.get(username);
+
+ }
+ finally {
+ cacheLock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Returns the value of the secret stored within Keeper Secrets Manager and
+ * represented by the given Keeper notation. Keeper notation locates the
+ * value of a specific field, custom field, or file associated with a
+ * specific record. See: https://docs.keeper.io/secrets-manager/secrets-manager/about/keeper-notation
+ *
+ * @param notation
+ * The Keeper notation of the secret to retrieve.
+ *
+ * @return
+ * A Future which completes with the value of the secret represented by
+ * the given Keeper notation, or null if there is no such secret.
+ *
+ * @throws GuacamoleException
+ * If the requested secret cannot be retrieved or the Keeper notation
+ * is invalid.
+ */
+ public Future getSecret(String notation) throws GuacamoleException {
+ validateCache();
+ cacheLock.readLock().lock();
+ try {
+
+ Matcher fileNotationMatcher = KEEPER_FILE_NOTATION.matcher(notation);
+ if (fileNotationMatcher.matches()) {
+
+ // Retrieve any relevant file asynchronously
+ KeeperFile file = Notation.getFile(cachedSecrets, notation);
+ return CompletableFuture.supplyAsync(() -> {
+ return new String(SecretsManager.downloadFile(file), StandardCharsets.UTF_8);
+ });
+
+ }
+
+ // Retrieve string values synchronously
+ return CompletableFuture.completedFuture(Notation.getValue(cachedSecrets, notation));
+
+ }
+
+ // Unfortunately, the notation parser within the Keeper SDK throws
+ // plain Errors for retrieval failures ...
+ catch (Error e) {
+ logger.warn("Record \"{}\" does not exist.", notation);
+ logger.debug("Retrieval of record by Keeper notation failed.", e);
+ return CompletableFuture.completedFuture(null);
+ }
+
+ // ... and plain Exceptions for parse failures (no subclasses)
+ catch (Exception e) {
+ logger.warn("\"{}\" is not valid Keeper notation. Please check "
+ + "the documentation at {} for valid formatting.",
+ notation, KEEPER_NOTATION_DOC_URL);
+ logger.debug("Provided Keeper notation could not be parsed.", e);
+ return CompletableFuture.completedFuture(null);
+ }
+ finally {
+ cacheLock.readLock().unlock();
+ }
+
+ }
+
+}
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java
new file mode 100644
index 000000000..c8ae0d7e3
--- /dev/null
+++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.vault.ksm.secret;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.concurrent.Future;
+
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.vault.secret.VaultSecretService;
+
+/**
+ * Service which retrieves secrets from Keeper Secrets Manager.
+ */
+@Singleton
+public class KsmSecretService implements VaultSecretService {
+
+ /**
+ * Client for retrieving records and secrets from Keeper Secrets Manager.
+ */
+ @Inject
+ private KsmClient ksm;
+
+ @Override
+ public String canonicalize(String nameComponent) {
+ try {
+
+ // As Keeper notation is essentially a URL, encode all components
+ // using standard URL escaping
+ return URLEncoder.encode(nameComponent, "UTF-8");
+
+ }
+ catch (UnsupportedEncodingException e) {
+ throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e);
+ }
+ }
+
+ @Override
+ public Future getValue(String name) throws GuacamoleException {
+ return ksm.getSecret(name);
+ }
+
+}
diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/resources/guac-manifest.json b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/resources/guac-manifest.json
new file mode 100644
index 000000000..a584b88b8
--- /dev/null
+++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/resources/guac-manifest.json
@@ -0,0 +1,16 @@
+{
+
+ "guacamoleVersion" : "1.4.0",
+
+ "name" : "Keeper Secrets Manager",
+ "namespace" : "keeper-secrets-manager",
+
+ "authProviders" : [
+ "org.apache.guacamole.vault.ksm.KsmAuthenticationProvider"
+ ],
+
+ "translations" : [
+ "translations/en.json"
+ ]
+
+}
diff --git a/extensions/guacamole-vault/pom.xml b/extensions/guacamole-vault/pom.xml
index 281572b3a..80eb3da57 100644
--- a/extensions/guacamole-vault/pom.xml
+++ b/extensions/guacamole-vault/pom.xml
@@ -47,6 +47,7 @@
modules/guacamole-vault-azure
+ modules/guacamole-vault-ksm