Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions server/basedir-includes/configure-from-env
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,18 @@ if [ $custom_extension_count != 0 ]; then
done
fi

# set storepass and keypass to 'changeme' so they aren't overwritten later
KEYSTORE_PASS=changeme
sed -i "s/^keystore\.storepass\s*=\s*.*\$/keystore.storepass = ${KEYSTORE_PASS//\//\\/}/" "$APP_DIR/conf/mirth.properties"
sed -i "s/^keystore\.keypass\s*=\s*.*\$/keystore.keypass = ${KEYSTORE_PASS//\//\\/}/" "$APP_DIR/conf/mirth.properties"
# The keystore is created with no passphrase and protected by file permissions instead, so there is
# no longer a password to pin here. Keystores created by earlier versions are JCEKS and were pinned
# to 'changeme', so keep those settings when one is already present in appdata.
KEYSTORE_FILE="$APP_DIR/appdata/keystore.pfx"
LEGACY_KEYSTORE_FILE="$APP_DIR/appdata/keystore.jks"
if [ -f "$LEGACY_KEYSTORE_FILE" ] || ! [ -z "${KEYSTORE_DOWNLOAD+x}" ]; then
echo "Found an existing keystore at ${LEGACY_KEYSTORE_FILE}, using jks keystore defaults."
sed -i 's|^keystore\.path\s*=\s*.*$|keystore.path = ${dir.appdata}/keystore.jks|' "$APP_DIR/conf/mirth.properties"
sed -i "s/^keystore\.storepass\s*=\s*.*\$/keystore.storepass = changeme/" "$APP_DIR/conf/mirth.properties"
sed -i "s/^keystore\.keypass\s*=\s*.*\$/keystore.keypass = changeme/" "$APP_DIR/conf/mirth.properties"
sed -i "s/^keystore\.type\s*=\s*.*\$/keystore.type = JCEKS/" "$APP_DIR/conf/mirth.properties"
fi

# merge the environment variables into /opt/engine/conf/mirth.properties
# db type
Expand Down Expand Up @@ -230,9 +238,9 @@ fi
if ! [ -z "${KEYSTORE_DOWNLOAD+x}" ]; then
echo "Downloading keystore at ${KEYSTORE_DOWNLOAD}"
if ! [ -z "${ALLOW_INSECURE}" ] && [ "${ALLOW_INSECURE}" == "true" ]; then
curl -ksSLf "${KEYSTORE_DOWNLOAD}" -o "$APP_DIR/appdata/keystore.jks" || echo "problem with keystore download"
curl -ksSLf "${KEYSTORE_DOWNLOAD}" -o "$LEGACY_KEYSTORE_FILE" || echo "problem with keystore download"
else
curl -sSLf "${KEYSTORE_DOWNLOAD}" -o "$APP_DIR/appdata/keystore.jks" || echo "problem with keystore download"
curl -sSLf "${KEYSTORE_DOWNLOAD}" -o "$LEGACY_KEYSTORE_FILE" || echo "problem with keystore download"
fi
fi

Expand Down
11 changes: 7 additions & 4 deletions server/conf/mirth.properties
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@ password.reuselimit = 0
version = 4.6.0

# keystore
keystore.path = ${dir.appdata}/keystore.jks
keystore.storepass = 81uWxplDtB
keystore.keypass = 81uWxplDtB
keystore.type = JCEKS
# Created if missing with no passphrase and secured by chmod 600.
keystore.path = ${dir.appdata}/keystore.pfx
keystore.storepass =
keystore.keypass =
# Set to JCEKS for compatibility with older versions of the engine.
# a PKCS12 is a standard pfx file. JCEKS is a the older java-specific jks format.
keystore.type = PKCS12

# server
http.contextpath = /
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
Expand Down Expand Up @@ -121,6 +122,7 @@
import com.mirth.connect.server.mybatis.KeyValuePair;
import com.mirth.connect.server.tools.ClassPathResource;
import com.mirth.connect.server.util.DatabaseUtil;
import com.mirth.connect.server.util.FilePermissionUtil;
import com.mirth.connect.server.util.PasswordRequirementsChecker;
import com.mirth.connect.server.util.ResourceUtil;
import com.mirth.connect.server.util.SqlConfig;
Expand Down Expand Up @@ -195,8 +197,6 @@ public class DefaultConfigurationController extends ConfigurationController {
private static final String XSTREAM_ALLOW_TYPES = "xstream.allowtypes";
private static final String XSTREAM_ALLOW_TYPE_HIERARCHIES = "xstream.allowtypehierarchies";

private static final String DEFAULT_STOREPASS = "81uWxplDtB";

// singleton pattern
private static ConfigurationController instance = null;

Expand Down Expand Up @@ -1239,24 +1239,9 @@ public void initializeSecuritySettings() {
keyStore.load(keyStoreFileIs, keyStorePassword);
logger.debug("found and loaded keystore: " + keyStoreFile.getAbsolutePath());
} else {
/*
* If a new keystore is being created, and the passwords are the defaults, then
* create new passwords.
*/
if (Arrays.equals(keyStorePassword, DEFAULT_STOREPASS.toCharArray()) && Arrays.equals(keyPassword, DEFAULT_STOREPASS.toCharArray())) {
String keyStorePasswordStr = generateNewPassword();
mirthConfig.setProperty("keystore.storepass", keyStorePasswordStr);
keyStorePassword = keyStorePasswordStr.toCharArray();

String keyPasswordStr = generateNewPassword();
mirthConfig.setProperty("keystore.keypass", keyPasswordStr);
keyPassword = keyPasswordStr.toCharArray();

saveMirthConfig();
}

keyStore.load(null, keyStorePassword);
logger.debug("keystore file not found, created new one");
FilePermissionUtil.createOwnerOnlyFile(keyStoreFile);
}

configureEncryption(provider, keyStore, keyPassword);
Expand All @@ -1273,19 +1258,6 @@ public void initializeSecuritySettings() {
}
}

/**
* Creates a random 12-character alphanumeric password.
*/
private String generateNewPassword() {
String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
SecureRandom random = new SecureRandom();
StringBuilder builder = new StringBuilder();
for (int i = 1; i <= 12; i++) {
builder.append(characters.charAt(random.nextInt(characters.length())));
}
return builder.toString();
}

@Override
public void initializeDatabaseSettings() {
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.mirth.connect.server.util;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.EnumSet;
import java.util.Set;

import com.sun.jna.Platform;

public class FilePermissionUtil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non blocking comment - I checked if file utils does this for us. It does not. I dont live introducing a util class but its appropriate here

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe touch to create the file? But then you still gotta mess with perms. https://commons.apache.org/proper/commons-io/apidocs/org/apache/commons/io/FileUtils.html#touch(java.io.File)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see an alternative here for any major portion.


private static final Set<PosixFilePermission> OWNER_ONLY = EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE);

private FilePermissionUtil() {}

/*
* Creates a file that only the account the server runs as, and whoever administers the machine,
* may read or write.
*/
public static void createOwnerOnlyFile(File file) throws IOException {
Path path = file.toPath();
File parent = file.getParentFile();

if (parent != null) {
Files.createDirectories(parent.toPath());
}

if (Platform.isWindows()) {
WindowsFilePermissionUtil.createRestrictedFile(path);
} else {
Files.createFile(path, PosixFilePermissions.asFileAttribute(OWNER_ONLY));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.mirth.connect.server.util;

import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Path;

import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.Kernel32Util;
import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.platform.win32.WinError;
import com.sun.jna.platform.win32.WinNT;
import com.sun.jna.platform.win32.WinNT.HANDLE;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.ptr.PointerByReference;
import com.sun.jna.win32.StdCallLibrary;
import com.sun.jna.win32.W32APIOptions;
import com.sun.security.auth.module.NTSystem;

/*
* The Windows half of FilePermissionUtil, kept apart so that nothing here is loaded on a platform
* that has no Win32 API to call.
*/
class WindowsFilePermissionUtil {

/*
* Full control to creator, LocalSystem and the local administrators. Matches
* user profile directories.
*/
private static final String SDDL = "D:P(A;;FA;;;{CURRENTUSERSID})(A;;FA;;;SY)(A;;FA;;;BA)";

private static final int SDDL_REVISION_1 = 1;

/*
* The SDDL conversions are not part of JNA's Advapi32 mapping.
*/
private interface Advapi32Sddl extends StdCallLibrary {
Advapi32Sddl INSTANCE = Native.loadLibrary("Advapi32", Advapi32Sddl.class, W32APIOptions.DEFAULT_OPTIONS);

boolean ConvertStringSecurityDescriptorToSecurityDescriptor(String sddl, int revision, PointerByReference securityDescriptor, IntByReference size);
}

private WindowsFilePermissionUtil() {}

/*
* Creates the file with a DACL that only lets the account the server runs as and the machine's
* administrators near it. The DACL is handed to CreateFile rather than applied afterwards, so
* the file is never briefly readable by anyone else.
*/
static void createRestrictedFile(Path path) throws IOException {
Pointer securityDescriptor = buildSecurityDescriptor();

try {
createFile(path, securityDescriptor);
} finally {
Kernel32.INSTANCE.LocalFree(securityDescriptor);
}
}

private static Pointer buildSecurityDescriptor() throws IOException {
PointerByReference securityDescriptor = new PointerByReference();

String sddl = SDDL.replace("{CURRENTUSERSID}", new NTSystem().getUserSID());
if (!Advapi32Sddl.INSTANCE.ConvertStringSecurityDescriptorToSecurityDescriptor(sddl, SDDL_REVISION_1, securityDescriptor, null)) {
throw lastError("Could not build a security descriptor from \"" + sddl + "\"");
}

return securityDescriptor.getValue();
}

private static void createFile(Path path, Pointer securityDescriptor) throws IOException {
WinBase.SECURITY_ATTRIBUTES securityAttributes = new WinBase.SECURITY_ATTRIBUTES();
securityAttributes.lpSecurityDescriptor = securityDescriptor;
securityAttributes.bInheritHandle = false;

HANDLE handle = Kernel32.INSTANCE.CreateFile(path.toAbsolutePath().toString(), WinNT.GENERIC_WRITE, 0, securityAttributes, WinNT.CREATE_NEW, WinNT.FILE_ATTRIBUTE_NORMAL, null);

if (WinBase.INVALID_HANDLE_VALUE.equals(handle)) {
// reported the same way as Files.createFile, so that callers need not care which is which
if (Kernel32.INSTANCE.GetLastError() == WinError.ERROR_FILE_EXISTS) {
throw new FileAlreadyExistsException(path.toString());
}

throw lastError("Could not create " + path);
}

Kernel32.INSTANCE.CloseHandle(handle);
}

private static IOException lastError(String message) {
return new IOException(message + ": " + Kernel32Util.formatMessage(Kernel32.INSTANCE.GetLastError()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.mirth.connect.server.util;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;

import java.io.File;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.apache.commons.io.IOUtils;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

import com.sun.jna.Platform;

public class FilePermissionUtilTest {

@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();


@Test
public void testPosixPermissions() throws Exception {
assumeFalse("Test only applies on POSIX filesystems", Platform.isWindows());
File file = new File(temporaryFolder.getRoot(), "keystore.p12");

FilePermissionUtil.createOwnerOnlyFile(file);

assertEquals("rw-------", PosixFilePermissions.toString(Files.getPosixFilePermissions(file.toPath())));
}

@Test
public void testWindowsPermissions() throws Exception {
assumeTrue("Test only applies on windows", Platform.isWindows());
assumeTrue("Test assumes english locale", System.getProperty("user.language").equals("en"));
File file = new File(temporaryFolder.getRoot(), "keystore.p12");
String path = file.getAbsolutePath();

FilePermissionUtil.createOwnerOnlyFile(file);
var icaclsOutput = system("icacls", path);
assertContains(icaclsOutput, System.getProperty("user.name") + ":(F)");
assertContains(icaclsOutput, "BUILTIN\\Administrators:(F)");
assertContains(icaclsOutput, "NT AUTHORITY\\SYSTEM:(F)");
assertNotContains(icaclsOutput, "Everyone:");
assertNotContains(icaclsOutput, "Users:");
assertNotContains(icaclsOutput, "(I)" /* inherited permissions */);
}

private void assertContains(String output, String expected) {
assertTrue("Expected output to contain '" + expected + "' but was '" + output + "'", output.contains(expected));
}

private void assertNotContains(String output, String expected) {
assertFalse("Expected output to not contain '" + expected + "' but was '" + output + "'", output.contains(expected));
}

private String system(String command, String... arguments) throws Exception {
List<String> commandLine = new ArrayList<String>();
commandLine.add(command);
commandLine.addAll(Arrays.asList(arguments));

Process process = new ProcessBuilder(commandLine).redirectErrorStream(true).start();
String output = IOUtils.toString(process.getInputStream(), Charset.defaultCharset());

assertEquals(output, 0, process.waitFor());
return output;
}
}
8 changes: 4 additions & 4 deletions server/src/test/resources/mirth.properties
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ password.reuselimit = 0
version = 4.6.0

# keystore
keystore.path = ${dir.appdata}/keystore.jks
keystore.storepass = 81uWxplDtB
keystore.keypass = 81uWxplDtB
keystore.type = JCEKS
keystore.path = ${dir.appdata}/keystore.pfx
keystore.storepass =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per Tonys comments from chat - add a comment in the props file explaining how this is generated if its blank.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did on line 28. With pkcs12, there is no need to have a password, so the blank is not a sentinel value - there just is no password.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry. I was off: this is a test resource, not a published asset.

keystore.keypass =
keystore.type = PKCS12

# server
http.contextpath = /
Expand Down
Loading