diff --git a/.gitignore b/.gitignore
index e42187762ba..f0779e6424e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,4 @@ store/.pmd
store/.ruleset
eclipse-ivysettings.xml
store/tmp/
+common-passwords.txt
diff --git a/build.xml b/build.xml
index a1aaaee681b..9449da7552a 100644
--- a/build.xml
+++ b/build.xml
@@ -6,6 +6,7 @@
+
diff --git a/common/src/java-test/com/zimbra/common/util/UnmodifiableBloomFilterTest.java b/common/src/java-test/com/zimbra/common/util/UnmodifiableBloomFilterTest.java
new file mode 100644
index 00000000000..34381378900
--- /dev/null
+++ b/common/src/java-test/com/zimbra/common/util/UnmodifiableBloomFilterTest.java
@@ -0,0 +1,124 @@
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Zimbra Collaboration Suite Server
+ * Copyright (C) 2019 Synacor, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software Foundation,
+ * version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see .
+ * ***** END LICENSE BLOCK *****
+ */
+package com.zimbra.common.util;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class UnmodifiableBloomFilterTest {
+
+ protected UnmodifiableBloomFilter bloomFilter = UnmodifiableBloomFilter
+ .createFilterFromFile("src/java-test/common-passwords.txt");
+
+ @Before
+ public void setUp() {
+ assertTrue(bloomFilter.isInitialized());
+ assertFalse(bloomFilter.isDisabled());
+ }
+
+ @Test
+ public void testMightContain() {
+ assertTrue(bloomFilter.isInitialized());
+ assertTrue(bloomFilter.mightContain("test123"));
+ assertTrue(bloomFilter.mightContain("hunter2"));
+ }
+
+ @Test
+ public void testMightContainFalse() {
+ assertTrue(bloomFilter.isInitialized());
+ assertFalse(bloomFilter.mightContain("not-in-the-test-file"));
+ }
+
+ @Test
+ public void testCreateFilterFromMissingFile() {
+ UnmodifiableBloomFilter missingFileFilter = UnmodifiableBloomFilter
+ .createFilterFromFile("src/java-test/fake-file-not-found");
+ // expect to immediately initialize
+ assertTrue(missingFileFilter.isInitialized());
+ assertTrue(missingFileFilter.isDisabled());
+ assertFalse(missingFileFilter.mightContain("test123"));
+ }
+
+ @Test
+ public void testCreateFilterFromEmptySpecifiedFile() {
+ UnmodifiableBloomFilter noFileFilter = UnmodifiableBloomFilter
+ .createFilterFromFile("");
+ // expect to immediately consider empty file as initialized
+ assertTrue(noFileFilter.isInitialized());
+ assertTrue(noFileFilter.isDisabled());
+ assertFalse(noFileFilter.mightContain("test123"));
+ }
+
+ @Test
+ public void testCreateFilterFromNullSpecifiedFile() {
+ UnmodifiableBloomFilter noFileFilter = UnmodifiableBloomFilter
+ .createFilterFromFile(null);
+ // expect to immediately consider null file as initialized
+ assertTrue(noFileFilter.isInitialized());
+ assertTrue(noFileFilter.isDisabled());
+ assertFalse(noFileFilter.mightContain("test123"));
+ }
+
+ @Test
+ public void testMightContainLazyLoad() {
+ UnmodifiableBloomFilter lazyFilter = UnmodifiableBloomFilter
+ .createLazyFilterFromFile("src/java-test/common-passwords.txt");
+ // expect to initialize on demand
+ assertFalse(lazyFilter.isInitialized());
+ assertFalse(lazyFilter.isDisabled());
+ assertTrue(lazyFilter.mightContain("test123"));
+ assertTrue(lazyFilter.mightContain("hunter2"));
+ assertTrue(lazyFilter.isInitialized());
+ assertFalse(lazyFilter.isDisabled());
+ }
+
+ @Test
+ public void testCreateLazyFilterFromMissingFile() {
+ UnmodifiableBloomFilter missingFileFilter = UnmodifiableBloomFilter
+ .createLazyFilterFromFile("src/java-test/fake-file-not-found");
+ // expect to initialize on demand
+ assertFalse(missingFileFilter.isInitialized());
+ assertFalse(missingFileFilter.mightContain("test123"));
+ assertTrue(missingFileFilter.isInitialized());
+ // file not found results in disabled instance
+ assertTrue(missingFileFilter.isDisabled());
+ }
+
+ @Test
+ public void testCreateLazyFilterFromEmptySpecifiedFile() {
+ UnmodifiableBloomFilter noFileFilter = UnmodifiableBloomFilter
+ .createLazyFilterFromFile("");
+ // expect to immediately consider empty file as initialized
+ assertTrue(noFileFilter.isInitialized());
+ assertTrue(noFileFilter.isDisabled());
+ assertFalse(noFileFilter.mightContain("test123"));
+ }
+
+ @Test
+ public void testCreateLazyFilterFromNullSpecifiedFile() {
+ UnmodifiableBloomFilter noFileFilter = UnmodifiableBloomFilter
+ .createLazyFilterFromFile(null);
+ // expect to immediately consider null file as initialized
+ assertTrue(noFileFilter.isInitialized());
+ assertTrue(noFileFilter.isDisabled());
+ assertFalse(noFileFilter.mightContain("test123"));
+ }
+
+}
diff --git a/common/src/java-test/common-passwords.txt b/common/src/java-test/common-passwords.txt
new file mode 100644
index 00000000000..5141a614c24
--- /dev/null
+++ b/common/src/java-test/common-passwords.txt
@@ -0,0 +1,14 @@
+test123
+bad-pass
+hunter2
+abc123
+football
+monkey
+letmein
+696969
+shadow
+master
+666666
+qwertyuiop
+123321
+mustang
\ No newline at end of file
diff --git a/common/src/java/com/zimbra/common/localconfig/LC.java b/common/src/java/com/zimbra/common/localconfig/LC.java
index 6e656498143..8ffc090a720 100644
--- a/common/src/java/com/zimbra/common/localconfig/LC.java
+++ b/common/src/java/com/zimbra/common/localconfig/LC.java
@@ -1436,6 +1436,12 @@ public enum PUBLIC_SHARE_VISIBILITY { samePrimaryDomain, all, none };
// to switch to Tika com.zimbra.cs.convert.TikaExtractionClient
public static final KnownKey attachment_extraction_client_class = KnownKey.newKey("com.zimbra.cs.convert.LegacyConverterClient");
+ // list file for blocking common passwords
+ public static final KnownKey common_passwords_txt = KnownKey.newKey("${zimbra_home}/conf/common-passwords.txt");
+
+ // enable blocking common passwords
+ public static final KnownKey zimbra_block_common_passwords_enabled = KnownKey.newKey(false);
+
static {
// Automatically set the key name with the variable name.
for (Field field : LC.class.getFields()) {
diff --git a/common/src/java/com/zimbra/common/util/UnmodifiableBloomFilter.java b/common/src/java/com/zimbra/common/util/UnmodifiableBloomFilter.java
new file mode 100644
index 00000000000..b7c39af8c6e
--- /dev/null
+++ b/common/src/java/com/zimbra/common/util/UnmodifiableBloomFilter.java
@@ -0,0 +1,163 @@
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Zimbra Collaboration Suite Server
+ * Copyright (C) 2019 Synacor, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software Foundation,
+ * version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License along with this program.
+ * If not, see .
+ * ***** END LICENSE BLOCK *****
+ */
+package com.zimbra.common.util;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.LineNumberReader;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+import com.google.common.hash.BloomFilter;
+import com.google.common.hash.Funnels;
+
+public class UnmodifiableBloomFilter {
+
+ private static final Double DEFAULT_TOLERANCE = 0.03;
+ private static final Integer DEFAULT_BUFFER_SIZE = 4096;
+ private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
+
+ private BloomFilter cache;
+ private boolean isInitialized = false;
+ private double tolerance;
+ private final String fileLocation;
+
+ private UnmodifiableBloomFilter(String fileLocation, BloomFilter cache, double tolerance) {
+ this.fileLocation = fileLocation;
+ this.cache = cache;
+ this.tolerance = tolerance;
+ this.isInitialized = StringUtil.isNullOrEmpty(fileLocation) ? true : false;
+ }
+
+ private UnmodifiableBloomFilter(BloomFilter cache) {
+ this.fileLocation = null;
+ this.cache = cache;
+ this.isInitialized = true;
+ }
+
+ public boolean mightContain(E entry) {
+ ensureInitialized();
+ return cache != null && cache.mightContain(entry);
+ }
+
+ /**
+ * @return True if an attempt to load/set the inner cache was made
+ */
+ public boolean isInitialized() {
+ return isInitialized;
+ }
+
+ /**
+ * @return True if the instance is initialized but still null.
+ */
+ public boolean isDisabled() {
+ return isInitialized && cache == null;
+ }
+
+ @SuppressWarnings("unchecked")
+ private void ensureInitialized() {
+ if (isInitialized) {
+ return;
+ }
+ synchronized (fileLocation) {
+ if (isInitialized) {
+ return;
+ }
+ this.cache = (BloomFilter) loadFilter(new File(fileLocation), tolerance);
+ isInitialized = true;
+ }
+ }
+
+ /**
+ * Wraps a bloom filter so it may not be modified.
+ * @param filter The filter to wrap
+ * @return Unmodifiable bloom filter
+ */
+ public static UnmodifiableBloomFilter createFilter(BloomFilter filter) {
+ return new UnmodifiableBloomFilter(filter);
+ }
+
+ /**
+ * @see UnmodifiableBloomFilter#createFilterFromFile(String, double)
+ */
+ public static UnmodifiableBloomFilter createFilterFromFile(String fileLocation) {
+ return createFilterFromFile(fileLocation, DEFAULT_TOLERANCE);
+ }
+
+ /**
+ * Creates an instance that will immediately initialize from file.
+ * @param fileLocation The file to load from
+ * @param tolerance The expected false positive tolerance (0.xx-1)
+ * @return A string instance that cannot be modified externally
+ */
+ public static UnmodifiableBloomFilter createFilterFromFile(String fileLocation, double tolerance) {
+ UnmodifiableBloomFilter bloom = createLazyFilterFromFile(fileLocation, tolerance);
+ bloom.ensureInitialized();
+ return bloom;
+ }
+
+ /**
+ * @see UnmodifiableBloomFilter#createLazyFilterFromFile(String, double)
+ */
+ public static UnmodifiableBloomFilter createLazyFilterFromFile(String fileLocation) {
+ return createLazyFilterFromFile(fileLocation, DEFAULT_TOLERANCE);
+ }
+
+ /**
+ * Creates a bloom filter instance that will initialize from file on first use.
+ * @param fileLocation The file to load from
+ * @param tolerance The expected false positive tolerance (0.xx-1)
+ * @return A lazy loading string instance that cannot be modified externally
+ */
+ public static UnmodifiableBloomFilter createLazyFilterFromFile(String fileLocation, double tolerance) {
+ // password filter file is unset, return disabled cache instance without warn
+ if (StringUtil.isNullOrEmpty(fileLocation)) {
+ return new UnmodifiableBloomFilter(null, null, tolerance);
+ }
+ return new UnmodifiableBloomFilter(fileLocation, null, tolerance);
+ }
+
+ private static BloomFilter loadFilter(File file, Double tolerance) {
+ try (
+ BufferedReader reader = new BufferedReader(new FileReader(file), DEFAULT_BUFFER_SIZE)) {
+ // determine entry count for accurate filter creation
+ long entryCount = countEntries(new FileReader(file));
+ ZimbraLog.cache.debug("Creating bloom filter for file with %d entries", entryCount);
+ BloomFilter pendingCache = BloomFilter
+ .create(Funnels.stringFunnel(DEFAULT_CHARSET), entryCount, tolerance);
+ // fill the filter as we read
+ String st;
+ while ((st = reader.readLine()) != null) {
+ pendingCache.put(st);
+ }
+ return pendingCache;
+ } catch (IOException e) {
+ ZimbraLog.cache.warnQuietly("Unable to load bloom filter from file.", e);
+ // return an instance with disabled cache since we failed during read/load
+ return null;
+ }
+ }
+
+ private static long countEntries(FileReader fileReader) throws IOException {
+ try (LineNumberReader reader = new LineNumberReader(fileReader, DEFAULT_BUFFER_SIZE)) {
+ reader.skip(Long.MAX_VALUE);
+ return reader.getLineNumber() + 1L;
+ }
+ }
+}
diff --git a/pkg-builder.pl b/pkg-builder.pl
index 4280439d991..6e6128ba6bd 100755
--- a/pkg-builder.pl
+++ b/pkg-builder.pl
@@ -256,6 +256,7 @@ ()
cpy_file( "store-conf/conf/globs2.zimbra", "$stage_base_dir/opt/zimbra/conf/globs2.zimbra" );
cpy_file( "store-conf/conf/spnego_java_options.in", "$stage_base_dir/opt/zimbra/conf/spnego_java_options.in" );
cpy_file( "store-conf/conf/contacts/zimbra-contact-fields.xml", "$stage_base_dir/opt/zimbra/conf/zimbra-contact-fields.xml" );
+ cpy_file( "store-conf/conf/common-passwords.txt", "$stage_base_dir/opt/zimbra/conf/common-passwords.txt" );
return ["store-conf/conf"];
}
diff --git a/store-conf/README.md b/store-conf/README.md
index 31c4b54c758..81aebcfa8f7 100644
--- a/store-conf/README.md
+++ b/store-conf/README.md
@@ -14,3 +14,10 @@
- `Everything under build/conf after a build should end up under /opt/zimbra/conf`
~
+
+
+## Common Password List
+
+- `The common password list is sourced from the [SecLists repository].`
+
+[SecLists repository]: https://github.com/danielmiessler/SecLists
\ No newline at end of file
diff --git a/store-conf/build.xml b/store-conf/build.xml
index 4aa69023f2b..b6770fb9a26 100644
--- a/store-conf/build.xml
+++ b/store-conf/build.xml
@@ -1,10 +1,20 @@
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/store-conf/conf/common/common-passwords.gz b/store-conf/conf/common/common-passwords.gz
new file mode 100644
index 00000000000..4bc051224e7
Binary files /dev/null and b/store-conf/conf/common/common-passwords.gz differ
diff --git a/store/src/java/com/zimbra/cs/account/ldap/LdapProvisioning.java b/store/src/java/com/zimbra/cs/account/ldap/LdapProvisioning.java
index 4036aae6306..e65af30bf30 100644
--- a/store/src/java/com/zimbra/cs/account/ldap/LdapProvisioning.java
+++ b/store/src/java/com/zimbra/cs/account/ldap/LdapProvisioning.java
@@ -1,7 +1,7 @@
/*
* ***** BEGIN LICENSE BLOCK *****
* Zimbra Collaboration Suite Server
- * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2018, 2019 Synacor, Inc.
+ * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2018, 2019, 2020 Synacor, Inc.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software Foundation,
@@ -58,6 +58,7 @@
import com.zimbra.common.service.ServiceException.Argument;
import com.zimbra.common.soap.AdminConstants;
import com.zimbra.common.soap.Element;
+import com.zimbra.common.util.UnmodifiableBloomFilter;
import com.zimbra.common.util.Constants;
import com.zimbra.common.util.EmailUtil;
import com.zimbra.common.util.L10nUtil;
@@ -264,6 +265,9 @@ public class LdapProvisioning extends LdapProv implements CacheAwareProvisioning
private final INamedEntryCache xmppComponentCache;
private final INamedEntryCache zimletCache;
+ private final UnmodifiableBloomFilter commonPasswordFilter;
+ private boolean blockCommonPasswordsEnabled;
+
private LdapConfig cachedGlobalConfig = null;
private GlobalGrant cachedGlobalGrant = null;
private static final Random sPoolRandom = new Random();
@@ -315,6 +319,10 @@ public LdapProvisioning(CacheMode cacheMode) {
zimletCache = cache.zimletCache();
alwaysOnClusterCache = cache.alwaysOnClusterCache();
+ commonPasswordFilter = UnmodifiableBloomFilter
+ .createLazyFilterFromFile(LC.common_passwords_txt.value());
+ blockCommonPasswordsEnabled = LC.zimbra_block_common_passwords_enabled.booleanValue();
+
setDIT();
setHelper(new ZLdapHelper(this));
allDLs = new Groups(this);
@@ -6090,7 +6098,6 @@ private String getString(Account acct, Cos cos, ZMutableEntry entry, String name
return cos.getAttr(name);
}
-
/**
* called to check password strength. Should pass in either an Account, or Cos/Attributes (during creation).
*
@@ -6114,6 +6121,10 @@ private void checkPasswordStrength(String password, Account acct, Cos cos, ZMuta
new Argument(Provisioning.A_zimbraPasswordMaxLength, maxLength, Argument.Type.NUM));
}
+ if (blockCommonPasswordsEnabled && commonPasswordFilter.mightContain(password)) {
+ throw AccountServiceException.INVALID_PASSWORD("password is known to be too common");
+ }
+
int minUpperCase = getInt(acct, cos, entry, Provisioning.A_zimbraPasswordMinUpperCaseChars, 0);
int minLowerCase = getInt(acct, cos, entry, Provisioning.A_zimbraPasswordMinLowerCaseChars, 0);
int minNumeric = getInt(acct, cos, entry, Provisioning.A_zimbraPasswordMinNumericChars, 0);