From 22cfb0acb18abd0813c4055bf95f4d620be935ea Mon Sep 17 00:00:00 2001 From: Mitch Gaffigan Date: Wed, 26 Aug 2026 17:37:50 -0500 Subject: [PATCH 1/2] Secure keystores with filesystem acl, use pkcs12 Signed-off-by: Mitch Gaffigan --- server/basedir-includes/configure-from-env | 20 +++-- server/conf/mirth.properties | 11 ++- .../DefaultConfigurationController.java | 34 +------- .../server/util/FilePermissionUtil.java | 81 +++++++++++++++++++ .../server/util/FilePermissionUtilTest.java | 61 ++++++++++++++ server/src/test/resources/mirth.properties | 8 +- 6 files changed, 170 insertions(+), 45 deletions(-) create mode 100644 server/src/main/java/com/mirth/connect/server/util/FilePermissionUtil.java create mode 100644 server/src/test/java/com/mirth/connect/server/util/FilePermissionUtilTest.java diff --git a/server/basedir-includes/configure-from-env b/server/basedir-includes/configure-from-env index 3ab0ebbb7b..a5f6a00b5b 100755 --- a/server/basedir-includes/configure-from-env +++ b/server/basedir-includes/configure-from-env @@ -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 @@ -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 diff --git a/server/conf/mirth.properties b/server/conf/mirth.properties index cbc3a21cec..27236e7887 100644 --- a/server/conf/mirth.properties +++ b/server/conf/mirth.properties @@ -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 = / diff --git a/server/src/main/java/com/mirth/connect/server/controllers/DefaultConfigurationController.java b/server/src/main/java/com/mirth/connect/server/controllers/DefaultConfigurationController.java index bfee2eddbf..2dec9c7e0f 100644 --- a/server/src/main/java/com/mirth/connect/server/controllers/DefaultConfigurationController.java +++ b/server/src/main/java/com/mirth/connect/server/controllers/DefaultConfigurationController.java @@ -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; @@ -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; @@ -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; @@ -1239,22 +1239,6 @@ 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"); } @@ -1263,6 +1247,7 @@ public void initializeSecuritySettings() { generateDefaultCertificate(provider, keyStore, keyPassword); // write the keystore back to the file + FilePermissionUtil.createOwnerOnlyFile(keyStoreFile); fos = new FileOutputStream(keyStoreFile); keyStore.store(fos, keyStorePassword); } catch (Exception e) { @@ -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 { diff --git a/server/src/main/java/com/mirth/connect/server/util/FilePermissionUtil.java b/server/src/main/java/com/mirth/connect/server/util/FilePermissionUtil.java new file mode 100644 index 0000000000..52a43303e1 --- /dev/null +++ b/server/src/main/java/com/mirth/connect/server/util/FilePermissionUtil.java @@ -0,0 +1,81 @@ +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.AclEntry; +import java.nio.file.attribute.AclEntryPermission; +import java.nio.file.attribute.AclEntryType; +import java.nio.file.attribute.AclFileAttributeView; +import java.nio.file.attribute.PosixFileAttributeView; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.nio.file.attribute.UserPrincipal; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + +public class FilePermissionUtil { + + private static final Set OWNER_ONLY = EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE); + + private FilePermissionUtil() {} + + /* + * Creates the file if it does not already exist, and restricts it so that only the user the + * server runs as may read or write it. This is what protects files that hold key material in + * place of a passphrase, so the file must never be created readable and then locked down + * afterwards; on POSIX the permissions are applied as part of the create itself. + */ + public static void createOwnerOnlyFile(File file) throws IOException { + Path path = file.toPath(); + + if (!Files.exists(path)) { + File parent = file.getParentFile(); + + if (parent != null) { + Files.createDirectories(parent.toPath()); + } + + if (isPosix(path)) { + Files.createFile(path, PosixFilePermissions.asFileAttribute(OWNER_ONLY)); + return; + } + + Files.createFile(path); + } + + restrictToOwner(path); + } + + /* + * Replaces the permissions on an existing file with owner read/write only. On Windows the + * entire ACL is replaced with a single entry for the file's owner, which also detaches the file + * from any permissions inherited from its parent directory. + */ + private static void restrictToOwner(Path path) throws IOException { + if (isPosix(path)) { + Files.setPosixFilePermissions(path, OWNER_ONLY); + return; + } + + AclFileAttributeView aclView = Files.getFileAttributeView(path, AclFileAttributeView.class); + + if (aclView != null) { + UserPrincipal owner = aclView.getOwner(); + // @formatter:off + AclEntry entry = AclEntry.newBuilder() + .setType(AclEntryType.ALLOW) + .setPrincipal(owner) + .setPermissions(EnumSet.allOf(AclEntryPermission.class)) + .build(); + // @formatter:on + aclView.setAcl(Collections.singletonList(entry)); + } + } + + private static boolean isPosix(Path path) { + return Files.getFileAttributeView(path, PosixFileAttributeView.class) != null; + } +} diff --git a/server/src/test/java/com/mirth/connect/server/util/FilePermissionUtilTest.java b/server/src/test/java/com/mirth/connect/server/util/FilePermissionUtilTest.java new file mode 100644 index 0000000000..3e7ba102f7 --- /dev/null +++ b/server/src/test/java/com/mirth/connect/server/util/FilePermissionUtilTest.java @@ -0,0 +1,61 @@ +package com.mirth.connect.server.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +import java.io.File; +import java.io.FileOutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFileAttributeView; +import java.nio.file.attribute.PosixFilePermissions; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class FilePermissionUtilTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testCreatesMissingFileOwnerOnly() throws Exception { + File file = new File(temporaryFolder.getRoot(), "nested/keystore.p12"); + + FilePermissionUtil.createOwnerOnlyFile(file); + + assertTrue(file.exists()); + assertPermissions(file); + } + + @Test + public void testRestrictsExistingFile() throws Exception { + File file = temporaryFolder.newFile("keystore.p12"); + assumeTrue(file.setReadable(true, false)); + + FilePermissionUtil.createOwnerOnlyFile(file); + + assertPermissions(file); + } + + @Test + public void testPreservesExistingContent() throws Exception { + File file = temporaryFolder.newFile("keystore.p12"); + + try (FileOutputStream fos = new FileOutputStream(file)) { + fos.write(new byte[] { 1, 2, 3 }); + } + + FilePermissionUtil.createOwnerOnlyFile(file); + + assertEquals(3, file.length()); + } + + private void assertPermissions(File file) throws Exception { + Path path = file.toPath(); + assumeTrue(Files.getFileAttributeView(path, PosixFileAttributeView.class) != null); + assertEquals("rw-------", PosixFilePermissions.toString(Files.getPosixFilePermissions(path))); + } +} diff --git a/server/src/test/resources/mirth.properties b/server/src/test/resources/mirth.properties index cbc3a21cec..cec324861f 100644 --- a/server/src/test/resources/mirth.properties +++ b/server/src/test/resources/mirth.properties @@ -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 = +keystore.keypass = +keystore.type = PKCS12 # server http.contextpath = / From 63b34899d6c4f6fe52ee9d496f1f8d1c79a54171 Mon Sep 17 00:00:00 2001 From: Mitch Gaffigan Date: Thu, 27 Aug 2026 11:53:51 -0500 Subject: [PATCH 2/2] Use SDDL on windows to secure keystore to BA,SY,CO Signed-off-by: Mitch Gaffigan --- .../DefaultConfigurationController.java | 2 +- .../server/util/FilePermissionUtil.java | 65 +++---------- .../util/WindowsFilePermissionUtil.java | 94 +++++++++++++++++++ .../server/util/FilePermissionUtilTest.java | 65 ++++++++----- 4 files changed, 146 insertions(+), 80 deletions(-) create mode 100644 server/src/main/java/com/mirth/connect/server/util/WindowsFilePermissionUtil.java diff --git a/server/src/main/java/com/mirth/connect/server/controllers/DefaultConfigurationController.java b/server/src/main/java/com/mirth/connect/server/controllers/DefaultConfigurationController.java index 2dec9c7e0f..b97f85ed8c 100644 --- a/server/src/main/java/com/mirth/connect/server/controllers/DefaultConfigurationController.java +++ b/server/src/main/java/com/mirth/connect/server/controllers/DefaultConfigurationController.java @@ -1241,13 +1241,13 @@ public void initializeSecuritySettings() { } else { keyStore.load(null, keyStorePassword); logger.debug("keystore file not found, created new one"); + FilePermissionUtil.createOwnerOnlyFile(keyStoreFile); } configureEncryption(provider, keyStore, keyPassword); generateDefaultCertificate(provider, keyStore, keyPassword); // write the keystore back to the file - FilePermissionUtil.createOwnerOnlyFile(keyStoreFile); fos = new FileOutputStream(keyStoreFile); keyStore.store(fos, keyStorePassword); } catch (Exception e) { diff --git a/server/src/main/java/com/mirth/connect/server/util/FilePermissionUtil.java b/server/src/main/java/com/mirth/connect/server/util/FilePermissionUtil.java index 52a43303e1..68f41e3f89 100644 --- a/server/src/main/java/com/mirth/connect/server/util/FilePermissionUtil.java +++ b/server/src/main/java/com/mirth/connect/server/util/FilePermissionUtil.java @@ -4,18 +4,13 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.attribute.AclEntry; -import java.nio.file.attribute.AclEntryPermission; -import java.nio.file.attribute.AclEntryType; -import java.nio.file.attribute.AclFileAttributeView; -import java.nio.file.attribute.PosixFileAttributeView; import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; -import java.nio.file.attribute.UserPrincipal; -import java.util.Collections; import java.util.EnumSet; import java.util.Set; +import com.sun.jna.Platform; + public class FilePermissionUtil { private static final Set OWNER_ONLY = EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE); @@ -23,59 +18,21 @@ public class FilePermissionUtil { private FilePermissionUtil() {} /* - * Creates the file if it does not already exist, and restricts it so that only the user the - * server runs as may read or write it. This is what protects files that hold key material in - * place of a passphrase, so the file must never be created readable and then locked down - * afterwards; on POSIX the permissions are applied as part of the create itself. + * 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 (!Files.exists(path)) { - File parent = file.getParentFile(); - - if (parent != null) { - Files.createDirectories(parent.toPath()); - } - - if (isPosix(path)) { - Files.createFile(path, PosixFilePermissions.asFileAttribute(OWNER_ONLY)); - return; - } - - Files.createFile(path); + if (parent != null) { + Files.createDirectories(parent.toPath()); } - restrictToOwner(path); - } - - /* - * Replaces the permissions on an existing file with owner read/write only. On Windows the - * entire ACL is replaced with a single entry for the file's owner, which also detaches the file - * from any permissions inherited from its parent directory. - */ - private static void restrictToOwner(Path path) throws IOException { - if (isPosix(path)) { - Files.setPosixFilePermissions(path, OWNER_ONLY); - return; - } - - AclFileAttributeView aclView = Files.getFileAttributeView(path, AclFileAttributeView.class); - - if (aclView != null) { - UserPrincipal owner = aclView.getOwner(); - // @formatter:off - AclEntry entry = AclEntry.newBuilder() - .setType(AclEntryType.ALLOW) - .setPrincipal(owner) - .setPermissions(EnumSet.allOf(AclEntryPermission.class)) - .build(); - // @formatter:on - aclView.setAcl(Collections.singletonList(entry)); + if (Platform.isWindows()) { + WindowsFilePermissionUtil.createRestrictedFile(path); + } else { + Files.createFile(path, PosixFilePermissions.asFileAttribute(OWNER_ONLY)); } } - - private static boolean isPosix(Path path) { - return Files.getFileAttributeView(path, PosixFileAttributeView.class) != null; - } } diff --git a/server/src/main/java/com/mirth/connect/server/util/WindowsFilePermissionUtil.java b/server/src/main/java/com/mirth/connect/server/util/WindowsFilePermissionUtil.java new file mode 100644 index 0000000000..47d65b07d7 --- /dev/null +++ b/server/src/main/java/com/mirth/connect/server/util/WindowsFilePermissionUtil.java @@ -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())); + } +} diff --git a/server/src/test/java/com/mirth/connect/server/util/FilePermissionUtilTest.java b/server/src/test/java/com/mirth/connect/server/util/FilePermissionUtilTest.java index 3e7ba102f7..fe21cde443 100644 --- a/server/src/test/java/com/mirth/connect/server/util/FilePermissionUtilTest.java +++ b/server/src/test/java/com/mirth/connect/server/util/FilePermissionUtilTest.java @@ -2,60 +2,75 @@ 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.io.FileOutputStream; +import java.nio.charset.Charset; import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.attribute.PosixFileAttributeView; 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 testCreatesMissingFileOwnerOnly() throws Exception { - File file = new File(temporaryFolder.getRoot(), "nested/keystore.p12"); + 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); - assertTrue(file.exists()); - assertPermissions(file); + assertEquals("rw-------", PosixFilePermissions.toString(Files.getPosixFilePermissions(file.toPath()))); } @Test - public void testRestrictsExistingFile() throws Exception { - File file = temporaryFolder.newFile("keystore.p12"); - assumeTrue(file.setReadable(true, false)); + 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); - - assertPermissions(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 */); } - @Test - public void testPreservesExistingContent() throws Exception { - File file = temporaryFolder.newFile("keystore.p12"); + private void assertContains(String output, String expected) { + assertTrue("Expected output to contain '" + expected + "' but was '" + output + "'", output.contains(expected)); + } - try (FileOutputStream fos = new FileOutputStream(file)) { - fos.write(new byte[] { 1, 2, 3 }); - } + private void assertNotContains(String output, String expected) { + assertFalse("Expected output to not contain '" + expected + "' but was '" + output + "'", output.contains(expected)); + } - FilePermissionUtil.createOwnerOnlyFile(file); + private String system(String command, String... arguments) throws Exception { + List commandLine = new ArrayList(); + commandLine.add(command); + commandLine.addAll(Arrays.asList(arguments)); - assertEquals(3, file.length()); - } + Process process = new ProcessBuilder(commandLine).redirectErrorStream(true).start(); + String output = IOUtils.toString(process.getInputStream(), Charset.defaultCharset()); - private void assertPermissions(File file) throws Exception { - Path path = file.toPath(); - assumeTrue(Files.getFileAttributeView(path, PosixFileAttributeView.class) != null); - assertEquals("rw-------", PosixFilePermissions.toString(Files.getPosixFilePermissions(path))); + assertEquals(output, 0, process.waitFor()); + return output; } }