From b0e6a6687c0d6fe23dbc3362e7c8cc0110645b58 Mon Sep 17 00:00:00 2001 From: nrupley Date: Mon, 10 Jul 2023 11:34:21 -0400 Subject: [PATCH 1/2] Adding fallback properties to allow old digests to be verified. --- .../mirth/commons/encryption/Digester.java | 90 +++++++++++++++++++ .../connect/model/EncryptionSettings.java | 76 ++++++++++++++++ .../DefaultConfigurationController.java | 5 ++ .../server/migration/Migrate4_4_0.java | 16 ++-- .../commons/encryption/test/DigesterTest.java | 87 +++++++++++++++++- 5 files changed, 264 insertions(+), 10 deletions(-) diff --git a/server/src/com/mirth/commons/encryption/Digester.java b/server/src/com/mirth/commons/encryption/Digester.java index f1fc57641..e0ef2a1d7 100644 --- a/server/src/com/mirth/commons/encryption/Digester.java +++ b/server/src/com/mirth/commons/encryption/Digester.java @@ -35,6 +35,13 @@ public class Digester { private boolean usePBE = true; private int keySizeBits = DEFAULT_KEY_SIZE_BITS; + private String fallbackAlgorithm; + private int fallbackSaltSizeBytes = 8; + private int fallbackIterations = 1000; + private boolean fallbackUsePBE = false; + private int fallbackKeySizeBits = 256; + private Digester fallbackDigester; + // Typically shouldn't need to be changed private Charset charset = Charset.forName(System.getProperty("mirth.digester.charset", StandardCharsets.UTF_8.name())); @@ -62,6 +69,54 @@ public void setFormat(Output format) { this.format = format; } + public String getFallbackAlgorithm() { + return fallbackAlgorithm; + } + + public void setFallbackAlgorithm(String fallbackAlgorithm) { + this.fallbackAlgorithm = fallbackAlgorithm; + } + + public int getFallbackSaltSizeBytes() { + return fallbackSaltSizeBytes; + } + + public void setFallbackSaltSizeBytes(Integer fallbackSaltSizeBytes) { + if (fallbackSaltSizeBytes != null) { + this.fallbackSaltSizeBytes = fallbackSaltSizeBytes; + } + } + + public int getFallbackIterations() { + return fallbackIterations; + } + + public void setFallbackIterations(Integer fallbackIterations) { + if (fallbackIterations != null) { + this.fallbackIterations = fallbackIterations; + } + } + + public boolean isFallbackUsePBE() { + return fallbackUsePBE; + } + + public void setFallbackUsePBE(Boolean fallbackUsePBE) { + if (fallbackUsePBE != null) { + this.fallbackUsePBE = fallbackUsePBE; + } + } + + public int getFallbackKeySizeBits() { + return fallbackKeySizeBits; + } + + public void setFallbackKeySizeBits(Integer fallbackKeySizeBits) { + if (fallbackKeySizeBits != null) { + this.fallbackKeySizeBits = fallbackKeySizeBits; + } + } + public SecureRandom getSaltGenerator() { return saltGenerator; } @@ -191,6 +246,27 @@ private byte[] digest(final String message, final byte[] salt) throws Exception } public boolean matches(final String message, final String digest) throws EncryptionException { + boolean matches = false; + EncryptionException firstCause = null; + try { + matches = doMatches(message, digest); + } catch (EncryptionException e) { + firstCause = e; + } + + if (!matches && getFallback() != null) { + // Try fallback algorithm + try { + matches = getFallback().matches(message, digest); + } catch (EncryptionException e) { + throw firstCause != null ? firstCause : e; + } + } + + return matches; + } + + private boolean doMatches(final String message, final String digest) throws EncryptionException { try { byte[] digestBytes = null; @@ -211,4 +287,18 @@ private boolean matches(final String message, final byte[] digest) throws Except System.arraycopy(digest, 0, salt, 0, saltSizeBytes); return Arrays.equals(digest(message, salt), digest); } + + private Digester getFallback() { + if (fallbackDigester == null && fallbackAlgorithm != null) { + fallbackDigester = new Digester(); + fallbackDigester.setProvider(provider); + fallbackDigester.setAlgorithm(fallbackAlgorithm); + fallbackDigester.setSaltSizeBytes(fallbackSaltSizeBytes); + fallbackDigester.setIterations(fallbackIterations); + fallbackDigester.setUsePBE(fallbackUsePBE); + fallbackDigester.setKeySizeBits(fallbackKeySizeBits); + fallbackDigester.setFormat(format); + } + return fallbackDigester; + } } diff --git a/server/src/com/mirth/connect/model/EncryptionSettings.java b/server/src/com/mirth/connect/model/EncryptionSettings.java index ae310fd88..dd7528423 100644 --- a/server/src/com/mirth/connect/model/EncryptionSettings.java +++ b/server/src/com/mirth/connect/model/EncryptionSettings.java @@ -51,6 +51,12 @@ public class EncryptionSettings extends AbstractSettings implements Serializable private static final String DIGEST_USE_PBE = "digest.usepbe"; private static final String DIGEST_KEY_SIZE = "digest.keysizeinbits"; + private static final String DIGEST_FALLBACK_ALGORITHM = "digest.fallback.algorithm"; + private static final String DIGEST_FALLBACK_SALT_SIZE = "digest.fallback.saltsizeinbytes"; + private static final String DIGEST_FALLBACK_ITERATIONS = "digest.fallback.iterations"; + private static final String DIGEST_FALLBACK_USE_PBE = "digest.fallback.usepbe"; + private static final String DIGEST_FALLBACK_KEY_SIZE = "digest.fallback.keysizeinbits"; + private static final String SECURITY_PROVIDER = "security.provider"; private Boolean encryptExport; @@ -65,6 +71,11 @@ public class EncryptionSettings extends AbstractSettings implements Serializable private Integer digestIterations; private Boolean digestUsePBE; private Integer digestKeySize; + private String digestFallbackAlgorithm; + private Integer digestFallbackSaltSize; + private Integer digestFallbackIterations; + private Boolean digestFallbackUsePBE; + private Integer digestFallbackKeySize; private String securityProvider; private byte[] secretKey; @@ -182,6 +193,46 @@ public void setDigestKeySize(Integer digestKeySize) { this.digestKeySize = digestKeySize; } + public String getDigestFallbackAlgorithm() { + return digestFallbackAlgorithm; + } + + public void setDigestFallbackAlgorithm(String digestFallbackAlgorithm) { + this.digestFallbackAlgorithm = digestFallbackAlgorithm; + } + + public Integer getDigestFallbackSaltSize() { + return digestFallbackSaltSize; + } + + public void setDigestFallbackSaltSize(Integer digestFallbackSaltSize) { + this.digestFallbackSaltSize = digestFallbackSaltSize; + } + + public Integer getDigestFallbackIterations() { + return digestFallbackIterations; + } + + public void setDigestFallbackIterations(Integer digestFallbackIterations) { + this.digestFallbackIterations = digestFallbackIterations; + } + + public Boolean getDigestFallbackUsePBE() { + return digestFallbackUsePBE; + } + + public void setDigestFallbackUsePBE(Boolean digestFallbackUsePBE) { + this.digestFallbackUsePBE = digestFallbackUsePBE; + } + + public Integer getDigestFallbackKeySize() { + return digestFallbackKeySize; + } + + public void setDigestFallbackKeySize(Integer digestFallbackKeySize) { + this.digestFallbackKeySize = digestFallbackKeySize; + } + public String getSecurityProvider() { return securityProvider; } @@ -212,6 +263,11 @@ public void setProperties(Properties properties) { setDigestIterations(toIntegerObject(properties.getProperty(DIGEST_ITERATIONS), DEFAULT_DIGEST_ITERATIONS)); setDigestUsePBE(intToBooleanObject(properties.getProperty(DIGEST_USE_PBE), DEFAULT_DIGEST_USE_PBE)); setDigestKeySize(toIntegerObject(properties.getProperty(DIGEST_KEY_SIZE), DEFAULT_DIGEST_KEY_SIZE)); + setDigestFallbackAlgorithm(properties.getProperty(DIGEST_FALLBACK_ALGORITHM, "SHA256")); + setDigestFallbackSaltSize(toIntegerObject(properties.getProperty(DIGEST_FALLBACK_SALT_SIZE), 8)); + setDigestFallbackIterations(toIntegerObject(properties.getProperty(DIGEST_FALLBACK_ITERATIONS), 1000)); + setDigestFallbackUsePBE(intToBooleanObject(properties.getProperty(DIGEST_FALLBACK_USE_PBE), false)); + setDigestFallbackKeySize(toIntegerObject(properties.getProperty(DIGEST_FALLBACK_KEY_SIZE), 256)); setSecurityProvider(properties.getProperty(SECURITY_PROVIDER, DEFAULT_SECURITY_PROVIDER)); } @@ -267,6 +323,26 @@ public Properties getProperties() { properties.put(DIGEST_KEY_SIZE, getDigestKeySize()); } + if (getDigestFallbackAlgorithm() != null) { + properties.put(DIGEST_FALLBACK_ALGORITHM, getDigestFallbackAlgorithm()); + } + + if (getDigestFallbackSaltSize() != null) { + properties.put(DIGEST_FALLBACK_SALT_SIZE, getDigestFallbackSaltSize()); + } + + if (getDigestFallbackIterations() != null) { + properties.put(DIGEST_FALLBACK_ITERATIONS, getDigestFallbackIterations()); + } + + if (getDigestFallbackUsePBE() != null) { + properties.put(DIGEST_FALLBACK_USE_PBE, getDigestFallbackUsePBE()); + } + + if (getDigestFallbackKeySize() != null) { + properties.put(DIGEST_FALLBACK_KEY_SIZE, getDigestFallbackKeySize()); + } + if (getSecurityProvider() != null) { properties.put(SECURITY_PROVIDER, getSecurityProvider()); } diff --git a/server/src/com/mirth/connect/server/controllers/DefaultConfigurationController.java b/server/src/com/mirth/connect/server/controllers/DefaultConfigurationController.java index 130f8ea9b..1c9da3f8a 100644 --- a/server/src/com/mirth/connect/server/controllers/DefaultConfigurationController.java +++ b/server/src/com/mirth/connect/server/controllers/DefaultConfigurationController.java @@ -1450,6 +1450,11 @@ private void configureEncryption(Provider provider, KeyStore keyStore, char[] ke digester.setIterations(encryptionConfig.getDigestIterations()); digester.setUsePBE(encryptionConfig.getDigestUsePBE()); digester.setKeySizeBits(encryptionConfig.getDigestKeySize()); + digester.setFallbackAlgorithm(encryptionConfig.getDigestFallbackAlgorithm()); + digester.setFallbackSaltSizeBytes(encryptionConfig.getDigestFallbackSaltSize()); + digester.setFallbackIterations(encryptionConfig.getDigestFallbackIterations()); + digester.setFallbackUsePBE(encryptionConfig.getDigestFallbackUsePBE()); + digester.setFallbackKeySizeBits(encryptionConfig.getDigestFallbackKeySize()); digester.setFormat(Output.BASE64); if (StringUtils.equalsAnyIgnoreCase(encryptionConfig.getEncryptionAlgorithm(), "AES", "DES", "DESede")) { diff --git a/server/src/com/mirth/connect/server/migration/Migrate4_4_0.java b/server/src/com/mirth/connect/server/migration/Migrate4_4_0.java index 08980ad19..702f7912f 100644 --- a/server/src/com/mirth/connect/server/migration/Migrate4_4_0.java +++ b/server/src/com/mirth/connect/server/migration/Migrate4_4_0.java @@ -25,14 +25,16 @@ public void updateConfiguration(PropertiesConfiguration configuration) { if (getStartingVersion() == null || getStartingVersion().ordinal() < Version.v4_4_0.ordinal()) { String digestAlgorithm = configuration.getString("digest.algorithm"); - // If no explicit digest algorithm was set, then default to the old SHA256 on upgrade. - if (StringUtils.isBlank(digestAlgorithm)) { - configuration.setProperty("digest.algorithm", "SHA256"); + if (StringUtils.isNotBlank(digestAlgorithm)) { + // Keep the current algorithm, and set these other properties since the defaults changed. + configuration.setProperty("digest.iterations", "1000"); + configuration.setProperty("digest.usepbe", "0"); + } else { + // Use the new algorithm, but set the old default as the fallback + configuration.setProperty("digest.fallback.algorithm", "SHA256"); + configuration.setProperty("digest.fallback.iterations", "1000"); + configuration.setProperty("digest.fallback.usepbe", "0"); } - - // Always set these properties on upgrade since the defaults changed. - configuration.setProperty("digest.iterations", "1000"); - configuration.setProperty("digest.usepbe", "0"); } } diff --git a/server/test/com/mirth/commons/encryption/test/DigesterTest.java b/server/test/com/mirth/commons/encryption/test/DigesterTest.java index 08231d87f..47d97de53 100644 --- a/server/test/com/mirth/commons/encryption/test/DigesterTest.java +++ b/server/test/com/mirth/commons/encryption/test/DigesterTest.java @@ -78,7 +78,7 @@ public void testPBKDF2() throws Exception { // Hardcoded hash used 600000 iterations assertFalse(digester.matches("admin", HASH_PBKDF2_ADMIN)); } - + @Test public void testArgon2id() throws Exception { Digester digester = createDigester(new org.bouncycastle.jce.provider.BouncyCastleProvider(), "Argon2", 3); @@ -112,7 +112,7 @@ public void testArgon2id() throws Exception { assertFalse(digester.matches("admin", HASH_ARGON2D_ADMIN)); assertFalse(digester.matches("admin", HASH_ARGON2I_ADMIN)); } - + @Test public void testArgon2d() throws Exception { Digester digester = createDigester(new org.bouncycastle.jce.provider.BouncyCastleProvider(), "Argon2d", 4); @@ -146,7 +146,7 @@ public void testArgon2d() throws Exception { assertFalse(digester.matches("admin", HASH_ARGON2I_ADMIN)); assertFalse(digester.matches("admin", HASH_ARGON2ID_ADMIN)); } - + @Test public void testArgon2i() throws Exception { Digester digester = createDigester(new org.bouncycastle.jce.provider.BouncyCastleProvider(), "Argon2i", 5); @@ -181,6 +181,87 @@ public void testArgon2i() throws Exception { assertFalse(digester.matches("admin", HASH_ARGON2ID_ADMIN)); } + @Test + public void testFallback1() throws Exception { + Digester digester = createDigester(new org.bouncycastle.jce.provider.BouncyCastleProvider(), "PBKDF2WithHmacSHA256", 600000, true, 256); + + assertFalse(digester.matches("admin", HASH_SHA256_ADMIN)); + + digester.setFallbackAlgorithm("SHA256"); + + assertTrue(digester.matches("admin", HASH_SHA256_ADMIN)); + } + + @Test + public void testFallback2() throws Exception { + Digester digester1 = createDigester(new org.bouncycastle.jce.provider.BouncyCastleProvider(), "Argon2", 3); + + String input1 = "admin"; + String digest1 = digester1.digest(input1); + assertFalse(StringUtils.isBlank(digest1)); + assertFalse(input1.equals(digest1)); + assertTrue(digester1.matches(input1, digest1)); + + Digester digester2 = createDigester(new org.bouncycastle.jce.provider.BouncyCastleProvider(), "PBKDF2WithHmacSHA256", 600000, true, 256); + + assertFalse(digester2.matches(input1, digest1)); + + digester2.setFallbackAlgorithm(digester1.getAlgorithm()); + digester2.setFallbackSaltSizeBytes(digester1.getSaltSizeBytes()); + digester2.setFallbackIterations(digester1.getIterations()); + digester2.setFallbackUsePBE(digester1.isUsePBE()); + digester2.setFallbackKeySizeBits(digester1.getKeySizeBits()); + + assertTrue(digester2.matches(input1, digest1)); + } + + @Test + public void testFallback3() throws Exception { + Digester digester1 = createDigester(new org.bouncycastle.jce.provider.BouncyCastleProvider(), "PBKDF2WithHmacSHA256", 600000, true, 256); + + String input1 = "admin"; + String digest1 = digester1.digest(input1); + assertFalse(StringUtils.isBlank(digest1)); + assertFalse(input1.equals(digest1)); + assertTrue(digester1.matches(input1, digest1)); + + Digester digester2 = createDigester(new org.bouncycastle.jce.provider.BouncyCastleProvider(), "Argon2", 3); + + assertFalse(digester2.matches(input1, digest1)); + + digester2.setFallbackAlgorithm(digester1.getAlgorithm()); + digester2.setFallbackSaltSizeBytes(digester1.getSaltSizeBytes()); + digester2.setFallbackIterations(digester1.getIterations()); + digester2.setFallbackUsePBE(digester1.isUsePBE()); + digester2.setFallbackKeySizeBits(digester1.getKeySizeBits()); + + assertTrue(digester2.matches(input1, digest1)); + } + + @Test + public void testFallback4() throws Exception { + Digester digester1 = createDigester(new org.bouncycastle.jce.provider.BouncyCastleProvider(), "PBKDF2WithHmacSHA256", 2222, true, 128); + digester1.setSaltSizeBytes(10); + + String input1 = "admin"; + String digest1 = digester1.digest(input1); + assertFalse(StringUtils.isBlank(digest1)); + assertFalse(input1.equals(digest1)); + assertTrue(digester1.matches(input1, digest1)); + + Digester digester2 = createDigester(new org.bouncycastle.jce.provider.BouncyCastleProvider(), "PBKDF2WithHmacSHA256", 600000, true, 256); + + assertFalse(digester2.matches(input1, digest1)); + + digester2.setFallbackAlgorithm(digester1.getAlgorithm()); + digester2.setFallbackSaltSizeBytes(digester1.getSaltSizeBytes()); + digester2.setFallbackIterations(digester1.getIterations()); + digester2.setFallbackUsePBE(digester1.isUsePBE()); + digester2.setFallbackKeySizeBits(digester1.getKeySizeBits()); + + assertTrue(digester2.matches(input1, digest1)); + } + private Digester createDigester(Provider provider, String algorithm, int iterations) { return createDigester(provider, algorithm, iterations, false, 256); } From 377675dccad3ebbcb04d6db510ad4540c08d5e64 Mon Sep 17 00:00:00 2001 From: nrupley Date: Mon, 10 Jul 2023 12:18:15 -0400 Subject: [PATCH 2/2] Adding comment to new properties on migration --- server/src/com/mirth/connect/server/migration/Migrate4_4_0.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/src/com/mirth/connect/server/migration/Migrate4_4_0.java b/server/src/com/mirth/connect/server/migration/Migrate4_4_0.java index 702f7912f..a0f0f3e92 100644 --- a/server/src/com/mirth/connect/server/migration/Migrate4_4_0.java +++ b/server/src/com/mirth/connect/server/migration/Migrate4_4_0.java @@ -32,6 +32,8 @@ public void updateConfiguration(PropertiesConfiguration configuration) { } else { // Use the new algorithm, but set the old default as the fallback configuration.setProperty("digest.fallback.algorithm", "SHA256"); + configuration.getLayout().setBlancLinesBefore("digest.fallback.algorithm", 1); + configuration.getLayout().setComment("digest.fallback.algorithm", "Allows old digest values to be verified"); configuration.setProperty("digest.fallback.iterations", "1000"); configuration.setProperty("digest.fallback.usepbe", "0"); }