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 index 4812b904c..2372dcb29 100644 --- 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 @@ -24,13 +24,10 @@ 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; @@ -446,16 +443,10 @@ public class KsmClient { cacheLock.readLock().lock(); try { + // Retrieve any relevant file asynchronously 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); - }); - - } + if (fileNotationMatcher.matches()) + return recordService.download(Notation.getFile(cachedSecrets, notation)); // Retrieve string values synchronously return CompletableFuture.completedFuture(Notation.getValue(cachedSecrets, notation)); diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmRecordService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmRecordService.java index a82434300..e2543ba1b 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmRecordService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmRecordService.java @@ -23,6 +23,7 @@ import com.google.inject.Singleton; import com.keepersecurity.secretsManager.core.HiddenField; import com.keepersecurity.secretsManager.core.Host; 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; @@ -30,8 +31,12 @@ import com.keepersecurity.secretsManager.core.KeyPair; import com.keepersecurity.secretsManager.core.KeyPairs; import com.keepersecurity.secretsManager.core.Login; import com.keepersecurity.secretsManager.core.Password; +import com.keepersecurity.secretsManager.core.SecretsManager; import com.keepersecurity.secretsManager.core.Text; +import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -77,6 +82,13 @@ public class KsmRecordService { private static final Pattern PRIVATE_KEY_LABEL_PATTERN = Pattern.compile("private\\s*key", Pattern.CASE_INSENSITIVE); + /** + * Regular expression which matches the filenames of private keys attached + * to Keeper records. + */ + private static final Pattern PRIVATE_KEY_FILENAME_PATTERN = + Pattern.compile(".*\\.pem", Pattern.CASE_INSENSITIVE); + /** * Returns the single value stored in the given list. If the list is empty * or contains multiple values, null is returned. @@ -234,6 +246,70 @@ public class KsmRecordService { } + /** + * Returns the file attached to the give Keeper record whose filename + * matches the given pattern. If there are no such files, or multiple such + * files, null is returned. + * + * @param record + * The record to retrieve the file from. + * + * @param filenamePattern + * The pattern to match filenames against. + * + * @return + * The single matching file attached to the given Keeper record, or + * null if there is not exactly one matching file. + */ + private KeeperFile getFile(KeeperRecord record, Pattern filenamePattern) { + + List files = record.getFiles(); + if (files == null) + return null; + + KeeperFile foundFile = null; + for (KeeperFile file : files) { + + // Ignore files whose filenames do not match + Matcher filenameMatcher = filenamePattern.matcher(file.getData().getName()); + if (!filenameMatcher.matches()) + continue; + + // Ignore ambiguous fields + if (foundFile != null) + return null; + + foundFile = file; + + } + + return foundFile; + + } + + /** + * Downloads the given file from the Keeper vault asynchronously. All files + * are read as UTF-8. + * + * @param file + * The file to download, which may be null. + * + * @return + * A Future which resolves with the contents of the file once + * downloaded. If no file was provided (file was null), this Future + * resolves with null. + */ + public Future download(final KeeperFile file) { + + if (file == null) + return CompletableFuture.completedFuture(null); + + return CompletableFuture.supplyAsync(() -> { + return new String(SecretsManager.downloadFile(file), StandardCharsets.UTF_8); + }); + + } + /** * Returns the single hostname (or address) associated with the given * record. If the record has no associated hostname, or multiple hostnames, @@ -341,23 +417,30 @@ public class KsmRecordService { * Returns the private key associated with the given record. If the record * has no associated private key, or multiple private keys, null is * returned. Private keys are retrieved from "KeyPairs" fields. - * Alternatively, private keys are retrieved from custom fields with the - * label "private key" (case-insensitive, space optional) if they are - * "KeyPairs", "Password", or "Hidden" fields. + * Alternatively, private keys are retrieved from PEM-type attachments or + * custom fields with the label "private key" (case-insensitive, space + * optional) if they are "KeyPairs", "Password", or "Hidden" fields. If + * file downloads are required, they will be performed asynchronously. * * @param record * The record to retrieve the private key from. * * @return - * The private key associated with the given record, or null if the - * record has no associated private key or multiple private keys. + * A Future which resolves with the private key associated with the + * given record. If the record has no associated private key or + * multiple private keys, the returned Future will resolve to null. */ - public String getPrivateKey(KeeperRecord record) { + public Future getPrivateKey(KeeperRecord record) { // Attempt to find single matching keypair field KeyPairs keyPairsField = getField(record, KeyPairs.class, PRIVATE_KEY_LABEL_PATTERN); if (keyPairsField != null) - return getSingleValue(keyPairsField.getValue(), KeyPair::getPrivateKey); + return CompletableFuture.completedFuture(getSingleValue(keyPairsField.getValue(), KeyPair::getPrivateKey)); + + // Lacking a typed keypair field, prefer a PEM-type attachment + KeeperFile keyFile = getFile(record, PRIVATE_KEY_FILENAME_PATTERN); + if (keyFile != null) + return download(keyFile); KeeperRecordData data = record.getData(); List custom = data.getCustom(); @@ -365,14 +448,14 @@ public class KsmRecordService { // Use password "private key" custom field as fallback ... Password passwordField = getField(custom, Password.class, PRIVATE_KEY_LABEL_PATTERN); if (passwordField != null) - return getSingleValue(passwordField.getValue()); + return CompletableFuture.completedFuture(getSingleValue(passwordField.getValue())); // ... or hidden "private key" custom field HiddenField hiddenField = getField(custom, HiddenField.class, PRIVATE_KEY_LABEL_PATTERN); if (hiddenField != null) - return getSingleValue(hiddenField.getValue()); + return CompletableFuture.completedFuture(getSingleValue(hiddenField.getValue())); - return null; + return CompletableFuture.completedFuture(null); } 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 index d7b4deb50..824f9e54e 100644 --- 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 @@ -109,9 +109,8 @@ public class KsmSecretService implements VaultSecretService { tokens.put(prefix + "PASSPHRASE", CompletableFuture.completedFuture(passphrase)); // Private key of server-related record - String privateKey = recordService.getPrivateKey(record); - if (privateKey != null) - tokens.put(prefix + "KEY", CompletableFuture.completedFuture(privateKey)); + Future privateKey = recordService.getPrivateKey(record); + tokens.put(prefix + "KEY", privateKey); }