Skip to content

Commit 36c06bd

Browse files
committed
Secure keystores with filesystem acl, use pkcs12
Signed-off-by: Mitch Gaffigan <mitch.gaffigan@comcast.net>
1 parent e765919 commit 36c06bd

6 files changed

Lines changed: 167 additions & 41 deletions

File tree

‎server/basedir-includes/configure-from-env‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,16 @@ if [ $custom_extension_count != 0 ]; then
1616
done
1717
fi
1818

19-
# set storepass and keypass to 'changeme' so they aren't overwritten later
20-
KEYSTORE_PASS=changeme
21-
sed -i "s/^keystore\.storepass\s*=\s*.*\$/keystore.storepass = ${KEYSTORE_PASS//\//\\/}/" "$APP_DIR/conf/mirth.properties"
22-
sed -i "s/^keystore\.keypass\s*=\s*.*\$/keystore.keypass = ${KEYSTORE_PASS//\//\\/}/" "$APP_DIR/conf/mirth.properties"
19+
# The keystore is created with no passphrase and protected by file permissions instead, so there is
20+
# no longer a password to pin here. Keystores created by earlier versions are JCEKS and were pinned
21+
# to 'changeme', so keep those settings when one is already present in appdata.
22+
KEYSTORE_FILE="$APP_DIR/appdata/keystore.jks"
23+
if [ -f "$KEYSTORE_FILE" ] && [ "$(head -c 4 "$KEYSTORE_FILE" | od -An -tx1 | tr -d '[:space:]')" = "cececece" ]; then
24+
echo "Found an existing JCEKS keystore, keeping the previous keystore settings."
25+
sed -i "s/^keystore\.storepass\s*=\s*.*\$/keystore.storepass = changeme/" "$APP_DIR/conf/mirth.properties"
26+
sed -i "s/^keystore\.keypass\s*=\s*.*\$/keystore.keypass = changeme/" "$APP_DIR/conf/mirth.properties"
27+
sed -i "s/^keystore\.type\s*=\s*.*\$/keystore.type = JCEKS/" "$APP_DIR/conf/mirth.properties"
28+
fi
2329

2430
# merge the environment variables into /opt/engine/conf/mirth.properties
2531
# db type

‎server/conf/mirth.properties‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@ password.reuselimit = 0
2525
version = 4.6.0
2626

2727
# keystore
28+
# The keystore is created on first boot with no passphrase and is instead protected by file
29+
# permissions that restrict it to the user the server runs as. Set storepass/keypass if you would
30+
# rather protect it with a passphrase, and set type to JCEKS for keystores created before 4.6.0.
2831
keystore.path = ${dir.appdata}/keystore.jks
29-
keystore.storepass = 81uWxplDtB
30-
keystore.keypass = 81uWxplDtB
31-
keystore.type = JCEKS
32+
keystore.storepass =
33+
keystore.keypass =
34+
keystore.type = PKCS12
3235

3336
# server
3437
http.contextpath = /

‎server/src/main/java/com/mirth/connect/server/controllers/DefaultConfigurationController.java‎

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import java.io.FileInputStream;
1414
import java.io.FileNotFoundException;
1515
import java.io.FileOutputStream;
16+
import java.io.IOException;
1617
import java.io.InputStream;
1718
import java.io.InputStreamReader;
1819
import java.io.OutputStream;
@@ -121,6 +122,7 @@
121122
import com.mirth.connect.server.mybatis.KeyValuePair;
122123
import com.mirth.connect.server.tools.ClassPathResource;
123124
import com.mirth.connect.server.util.DatabaseUtil;
125+
import com.mirth.connect.server.util.FilePermissionUtil;
124126
import com.mirth.connect.server.util.PasswordRequirementsChecker;
125127
import com.mirth.connect.server.util.ResourceUtil;
126128
import com.mirth.connect.server.util.SqlConfig;
@@ -195,8 +197,6 @@ public class DefaultConfigurationController extends ConfigurationController {
195197
private static final String XSTREAM_ALLOW_TYPES = "xstream.allowtypes";
196198
private static final String XSTREAM_ALLOW_TYPE_HIERARCHIES = "xstream.allowtypehierarchies";
197199

198-
private static final String DEFAULT_STOREPASS = "81uWxplDtB";
199-
200200
// singleton pattern
201201
private static ConfigurationController instance = null;
202202

@@ -1239,22 +1239,6 @@ public void initializeSecuritySettings() {
12391239
keyStore.load(keyStoreFileIs, keyStorePassword);
12401240
logger.debug("found and loaded keystore: " + keyStoreFile.getAbsolutePath());
12411241
} else {
1242-
/*
1243-
* If a new keystore is being created, and the passwords are the defaults, then
1244-
* create new passwords.
1245-
*/
1246-
if (Arrays.equals(keyStorePassword, DEFAULT_STOREPASS.toCharArray()) && Arrays.equals(keyPassword, DEFAULT_STOREPASS.toCharArray())) {
1247-
String keyStorePasswordStr = generateNewPassword();
1248-
mirthConfig.setProperty("keystore.storepass", keyStorePasswordStr);
1249-
keyStorePassword = keyStorePasswordStr.toCharArray();
1250-
1251-
String keyPasswordStr = generateNewPassword();
1252-
mirthConfig.setProperty("keystore.keypass", keyPasswordStr);
1253-
keyPassword = keyPasswordStr.toCharArray();
1254-
1255-
saveMirthConfig();
1256-
}
1257-
12581242
keyStore.load(null, keyStorePassword);
12591243
logger.debug("keystore file not found, created new one");
12601244
}
@@ -1263,6 +1247,7 @@ public void initializeSecuritySettings() {
12631247
generateDefaultCertificate(provider, keyStore, keyPassword);
12641248

12651249
// write the keystore back to the file
1250+
FilePermissionUtil.createOwnerOnlyFile(keyStoreFile);
12661251
fos = new FileOutputStream(keyStoreFile);
12671252
keyStore.store(fos, keyStorePassword);
12681253
} catch (Exception e) {
@@ -1273,19 +1258,6 @@ public void initializeSecuritySettings() {
12731258
}
12741259
}
12751260

1276-
/**
1277-
* Creates a random 12-character alphanumeric password.
1278-
*/
1279-
private String generateNewPassword() {
1280-
String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
1281-
SecureRandom random = new SecureRandom();
1282-
StringBuilder builder = new StringBuilder();
1283-
for (int i = 1; i <= 12; i++) {
1284-
builder.append(characters.charAt(random.nextInt(characters.length())));
1285-
}
1286-
return builder.toString();
1287-
}
1288-
12891261
@Override
12901262
public void initializeDatabaseSettings() {
12911263
try {
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.mirth.connect.server.util;
2+
3+
import java.io.File;
4+
import java.io.IOException;
5+
import java.nio.file.Files;
6+
import java.nio.file.Path;
7+
import java.nio.file.attribute.AclEntry;
8+
import java.nio.file.attribute.AclEntryPermission;
9+
import java.nio.file.attribute.AclEntryType;
10+
import java.nio.file.attribute.AclFileAttributeView;
11+
import java.nio.file.attribute.PosixFileAttributeView;
12+
import java.nio.file.attribute.PosixFilePermission;
13+
import java.nio.file.attribute.PosixFilePermissions;
14+
import java.nio.file.attribute.UserPrincipal;
15+
import java.util.Collections;
16+
import java.util.EnumSet;
17+
import java.util.Set;
18+
19+
public class FilePermissionUtil {
20+
21+
private static final Set<PosixFilePermission> OWNER_ONLY = EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE);
22+
23+
private FilePermissionUtil() {}
24+
25+
/*
26+
* Creates the file if it does not already exist, and restricts it so that only the user the
27+
* server runs as may read or write it. This is what protects files that hold key material in
28+
* place of a passphrase, so the file must never be created readable and then locked down
29+
* afterwards; on POSIX the permissions are applied as part of the create itself.
30+
*/
31+
public static void createOwnerOnlyFile(File file) throws IOException {
32+
Path path = file.toPath();
33+
34+
if (!Files.exists(path)) {
35+
File parent = file.getParentFile();
36+
37+
if (parent != null) {
38+
Files.createDirectories(parent.toPath());
39+
}
40+
41+
if (isPosix(path)) {
42+
Files.createFile(path, PosixFilePermissions.asFileAttribute(OWNER_ONLY));
43+
return;
44+
}
45+
46+
Files.createFile(path);
47+
}
48+
49+
restrictToOwner(path);
50+
}
51+
52+
/*
53+
* Replaces the permissions on an existing file with owner read/write only. On Windows the
54+
* entire ACL is replaced with a single entry for the file's owner, which also detaches the file
55+
* from any permissions inherited from its parent directory.
56+
*/
57+
private static void restrictToOwner(Path path) throws IOException {
58+
if (isPosix(path)) {
59+
Files.setPosixFilePermissions(path, OWNER_ONLY);
60+
return;
61+
}
62+
63+
AclFileAttributeView aclView = Files.getFileAttributeView(path, AclFileAttributeView.class);
64+
65+
if (aclView != null) {
66+
UserPrincipal owner = aclView.getOwner();
67+
// @formatter:off
68+
AclEntry entry = AclEntry.newBuilder()
69+
.setType(AclEntryType.ALLOW)
70+
.setPrincipal(owner)
71+
.setPermissions(EnumSet.allOf(AclEntryPermission.class))
72+
.build();
73+
// @formatter:on
74+
aclView.setAcl(Collections.singletonList(entry));
75+
}
76+
}
77+
78+
private static boolean isPosix(Path path) {
79+
return Files.getFileAttributeView(path, PosixFileAttributeView.class) != null;
80+
}
81+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.mirth.connect.server.util;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertTrue;
5+
import static org.junit.Assume.assumeTrue;
6+
7+
import java.io.File;
8+
import java.io.FileOutputStream;
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
11+
import java.nio.file.attribute.PosixFileAttributeView;
12+
import java.nio.file.attribute.PosixFilePermissions;
13+
14+
import org.junit.Rule;
15+
import org.junit.Test;
16+
import org.junit.rules.TemporaryFolder;
17+
18+
public class FilePermissionUtilTest {
19+
20+
@Rule
21+
public TemporaryFolder temporaryFolder = new TemporaryFolder();
22+
23+
@Test
24+
public void testCreatesMissingFileOwnerOnly() throws Exception {
25+
File file = new File(temporaryFolder.getRoot(), "nested/keystore.p12");
26+
27+
FilePermissionUtil.createOwnerOnlyFile(file);
28+
29+
assertTrue(file.exists());
30+
assertPermissions(file);
31+
}
32+
33+
@Test
34+
public void testRestrictsExistingFile() throws Exception {
35+
File file = temporaryFolder.newFile("keystore.p12");
36+
assumeTrue(file.setReadable(true, false));
37+
38+
FilePermissionUtil.createOwnerOnlyFile(file);
39+
40+
assertPermissions(file);
41+
}
42+
43+
@Test
44+
public void testPreservesExistingContent() throws Exception {
45+
File file = temporaryFolder.newFile("keystore.p12");
46+
47+
try (FileOutputStream fos = new FileOutputStream(file)) {
48+
fos.write(new byte[] { 1, 2, 3 });
49+
}
50+
51+
FilePermissionUtil.createOwnerOnlyFile(file);
52+
53+
assertEquals(3, file.length());
54+
}
55+
56+
private void assertPermissions(File file) throws Exception {
57+
Path path = file.toPath();
58+
assumeTrue(Files.getFileAttributeView(path, PosixFileAttributeView.class) != null);
59+
assertEquals("rw-------", PosixFilePermissions.toString(Files.getPosixFilePermissions(path)));
60+
}
61+
}

‎server/src/test/resources/mirth.properties‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@ password.reuselimit = 0
2525
version = 4.6.0
2626

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

3336
# server
3437
http.contextpath = /

0 commit comments

Comments
 (0)