blob: 70e500e2ea1ecb414cb2847972465dc2ec6fb8f0 [file] [log] [blame]
/*
* 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.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;
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;
/**
* Service for automatically parsing out secrets and data from Keeper records.
*/
@Singleton
public class KsmRecordService {
/**
* Regular expression which matches the labels of custom fields containing
* hostnames/addresses.
*/
private static final Pattern HOSTNAME_LABEL_PATTERN =
Pattern.compile("hostname|(ip\\s*)?address", Pattern.CASE_INSENSITIVE);
/**
* Regular expression which matches the labels of custom fields containing
* usernames.
*/
private static final Pattern USERNAME_LABEL_PATTERN =
Pattern.compile("username", Pattern.CASE_INSENSITIVE);
/**
* Regular expression which matches the labels of custom fields containing
* passwords.
*/
private static final Pattern PASSWORD_LABEL_PATTERN =
Pattern.compile("password", Pattern.CASE_INSENSITIVE);
/**
* Regular expression which matches the labels of custom fields containing
* passphrases for private keys.
*/
private static final Pattern PASSPHRASE_LABEL_PATTERN =
Pattern.compile("passphrase", Pattern.CASE_INSENSITIVE);
/**
* Regular expression which matches the labels of custom fields containing
* private keys.
*/
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. Note that null will also
* be returned if the single value stored in the list is itself null.
*
* @param <T>
* The type of object stored in the list.
*
* @param values
* The list to retrieve a single value from.
*
* @return
* The single value stored in the given list, or null if the list is
* empty or contains multiple values.
*/
private <T> T getSingleValue(List<T> values) {
if (values == null || values.size() != 1)
return null;
return values.get(0);
}
/**
* Returns the single value stored in the given list of strings. If the
* list is empty, contains multiple values, or contains only a single empty
* string, null is returned. Note that null will also be returned if the
* single value stored in the list is itself null.
*
* @param values
* The list to retrieve a single value from.
*
* @return
* The single value stored in the given list, or null if the list is
* empty, contains multiple values, or contains only a single empty
* string.
*/
private String getSingleStringValue(List<String> values) {
String value = getSingleValue(values);
if (value != null && !value.isEmpty())
return value;
return null;
}
/**
* Returns the single value stored in the given list, additionally
* performing a mapping transformation on the single value. If the list is
* empty or contains multiple values, null is returned. Note that null will
* also be returned if the mapping transformation returns null for the
* single value stored in the list.
*
* @param <T>
* The type of object stored in the list.
*
* @param <R>
* The type of object to return.
*
* @param values
* The list to retrieve a single value from.
*
* @param mapper
* The function to use to map the single object of type T to type R.
*
* @return
* The single value stored in the given list, transformed using the
* provided mapping function, or null if the list is empty or contains
* multiple values.
*/
private <T, R> R getSingleValue(List<T> values, Function<T, R> mapper) {
T value = getSingleValue(values);
if (value == null)
return null;
return mapper.apply(value);
}
/**
* Returns the single value stored in the given list of strings,
* additionally performing a mapping transformation on the single value. If
* the list is empty, contains multiple values, or contains only a single
* empty string, null is returned. Note that null will also be returned if
* the mapping transformation returns null for the single value stored in
* the list.
*
* @param <T>
* The type of object stored in the list.
*
* @param values
* The list to retrieve a single value from.
*
* @param mapper
* The function to use to map the single object of type T to type R.
*
* @return
* The single value stored in the given list, transformed using the
* provided mapping function, or null if the list is empty, contains
* multiple values, or contains only a single empty string.
*/
private <T> String getSingleStringValue(List<T> values, Function<T, String> mapper) {
String value = getSingleValue(values, mapper);
if (value != null && !value.isEmpty())
return value;
return null;
}
/**
* Returns the instance of the only field that has the given type and
* matches the given label pattern. If there are no such fields, or
* multiple such fields, null is returned.
*
* @param <T>
* The type of field to return.
*
* @param fields
* The list of fields to retrieve the field from. For convenience, this
* may be null. A null list will be considered equivalent to an empty
* list.
*
* @param fieldClass
* The class representing the type of field to return.
*
* @param labelPattern
* The pattern to match against the desired field's label, or null if
* no label pattern match should be performed.
*
* @return
* The field having the given type and matching the given label
* pattern, or null if there is not exactly one such field.
*/
@SuppressWarnings("unchecked") // Manually verified with isAssignableFrom()
private <T extends KeeperRecordField> T getField(List<KeeperRecordField> fields,
Class<T> fieldClass, Pattern labelPattern) {
// There are no fields if no List was provided at all
if (fields == null)
return null;
T foundField = null;
for (KeeperRecordField field : fields) {
// Ignore fields of wrong class
if (!fieldClass.isAssignableFrom(field.getClass()))
continue;
// Match against provided pattern, if any
if (labelPattern != null) {
// Ignore fields without labels if a label match is requested
String label = field.getLabel();
if (label == null)
continue;
// Ignore fields whose labels do not match
Matcher labelMatcher = labelPattern.matcher(label);
if (!labelMatcher.matches())
continue;
}
// Ignore ambiguous fields
if (foundField != null)
return null;
// Tentative match found - we can use this as long as no other
// field matches the criteria
foundField = (T) field;
}
return foundField;
}
/**
* Returns the instance of the only field that has the given type and
* matches the given label pattern. If there are no such fields, or
* multiple such fields, null is returned. Both standard and custom fields
* are searched. As standard fields do not have labels, any given label
* pattern is ignored for standard fields.
*
* @param <T>
* The type of field to return.
*
* @param record
* The Keeper record to retrieve the field from.
*
* @param fieldClass
* The class representing the type of field to return.
*
* @param labelPattern
* The pattern to match against the labels of custom fields, or null if
* no label pattern match should be performed.
*
* @return
* The field having the given type and matching the given label
* pattern, or null if there is not exactly one such field.
*/
private <T extends KeeperRecordField> T getField(KeeperRecord record,
Class<T> fieldClass, Pattern labelPattern) {
KeeperRecordData data = record.getData();
// Attempt to find standard field first, ignoring custom fields if a
// standard field exists (NOTE: standard fields do not have labels)
T field = getField(data.getFields(), fieldClass, null);
if (field != null)
return field;
// Fall back on custom fields
return getField(data.getCustom(), fieldClass, labelPattern);
}
/**
* 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<KeeperFile> 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<String> 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,
* null is returned. Hostnames are retrieved from "Hosts" fields, as well
* as "Text" and "Hidden" fields that have the label "hostname", "address",
* or "ip address" (case-insensitive, space optional).
*
* @param record
* The record to retrieve the hostname from.
*
* @return
* The hostname associated with the given record, or null if the record
* has no associated hostname or multiple hostnames.
*/
public String getHostname(KeeperRecord record) {
// Prefer standard login field
Hosts hostsField = getField(record, Hosts.class, null);
if (hostsField != null)
return getSingleStringValue(hostsField.getValue(), Host::getHostName);
KeeperRecordData data = record.getData();
List<KeeperRecordField> custom = data.getCustom();
// Use text "hostname" custom field as fallback ...
Text textField = getField(custom, Text.class, HOSTNAME_LABEL_PATTERN);
if (textField != null)
return getSingleStringValue(textField.getValue());
// ... or hidden "hostname" custom field
HiddenField hiddenField = getField(custom, HiddenField.class, HOSTNAME_LABEL_PATTERN);
if (hiddenField != null)
return getSingleStringValue(hiddenField.getValue());
return null;
}
/**
* Returns the single username associated with the given record. If the
* record has no associated username, or multiple usernames, null is
* returned. Usernames are retrieved from "Login" fields, as well as
* "Text" and "Hidden" fields that have the label "username"
* (case-insensitive).
*
* @param record
* The record to retrieve the username from.
*
* @return
* The username associated with the given record, or null if the record
* has no associated username or multiple usernames.
*/
public String getUsername(KeeperRecord record) {
// Prefer standard login field
Login loginField = getField(record, Login.class, null);
if (loginField != null)
return getSingleStringValue(loginField.getValue());
KeeperRecordData data = record.getData();
List<KeeperRecordField> custom = data.getCustom();
// Use text "username" custom field as fallback ...
Text textField = getField(custom, Text.class, USERNAME_LABEL_PATTERN);
if (textField != null)
return getSingleStringValue(textField.getValue());
// ... or hidden "username" custom field
HiddenField hiddenField = getField(custom, HiddenField.class, USERNAME_LABEL_PATTERN);
if (hiddenField != null)
return getSingleStringValue(hiddenField.getValue());
return null;
}
/**
* Returns the password associated with the given record. Both standard and
* custom fields are searched. Only "Password" and "Hidden" field types are
* considered. Custom fields must additionally have the label "password"
* (case-insensitive).
*
* @param record
* The record to retrieve the password from.
*
* @return
* The password associated with the given record, or null if the record
* has no associated password.
*/
public String getPassword(KeeperRecord record) {
Password passwordField = getField(record, Password.class, PASSWORD_LABEL_PATTERN);
if (passwordField != null)
return getSingleStringValue(passwordField.getValue());
HiddenField hiddenField = getField(record, HiddenField.class, PASSWORD_LABEL_PATTERN);
if (hiddenField != null)
return getSingleStringValue(hiddenField.getValue());
return null;
}
/**
* 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 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
* 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 Future<String> getPrivateKey(KeeperRecord record) {
// Attempt to find single matching keypair field
KeyPairs keyPairsField = getField(record, KeyPairs.class, PRIVATE_KEY_LABEL_PATTERN);
if (keyPairsField != null) {
String privateKey = getSingleStringValue(keyPairsField.getValue(), KeyPair::getPrivateKey);
if (privateKey != null && !privateKey.isEmpty())
return CompletableFuture.completedFuture(privateKey);
}
// 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<KeeperRecordField> custom = data.getCustom();
// Use password "private key" custom field as fallback ...
Password passwordField = getField(custom, Password.class, PRIVATE_KEY_LABEL_PATTERN);
if (passwordField != null)
return CompletableFuture.completedFuture(getSingleStringValue(passwordField.getValue()));
// ... or hidden "private key" custom field
HiddenField hiddenField = getField(custom, HiddenField.class, PRIVATE_KEY_LABEL_PATTERN);
if (hiddenField != null)
return CompletableFuture.completedFuture(getSingleStringValue(hiddenField.getValue()));
return CompletableFuture.completedFuture(null);
}
/**
* Returns the passphrase for the private key associated with the given
* record. Both standard and custom fields are searched. Only "Password"
* and "Hidden" field types are considered. Custom fields must additionally
* have the label "passphrase" (case-insensitive). Note that there is no
* specific association between private keys and passphrases in the
* "KeyPairs" field type.
*
* @param record
* The record to retrieve the passphrase from.
*
* @return
* The passphrase for the private key associated with the given record,
* or null if there is no such passphrase associated with the record.
*/
public String getPassphrase(KeeperRecord record) {
KeeperRecordData data = record.getData();
List<KeeperRecordField> fields = data.getFields();
List<KeeperRecordField> custom = data.getCustom();
// For records with a standard keypair field, the passphrase is the
// standard password field
if (getField(fields, KeyPairs.class, null) != null) {
Password passwordField = getField(fields, Password.class, null);
if (passwordField != null)
return getSingleStringValue(passwordField.getValue());
}
// For records WITHOUT a standard keypair field, the passphrase can
// only reasonably be a custom field (consider a "Login" record with
// a pair of custom hidden fields for the private key and passphrase:
// the standard password field of the "Login" record refers to the
// user's own password, if any, not the passphrase of their key)
// Use password "private key" custom field as fallback ...
Password passwordField = getField(custom, Password.class, PASSPHRASE_LABEL_PATTERN);
if (passwordField != null)
return getSingleStringValue(passwordField.getValue());
// ... or hidden "private key" custom field
HiddenField hiddenField = getField(custom, HiddenField.class, PASSPHRASE_LABEL_PATTERN);
if (hiddenField != null)
return getSingleStringValue(hiddenField.getValue());
return null;
}
}