001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.security;
029
030import org.opencms.configuration.CmsParameterConfiguration;
031import org.opencms.i18n.CmsEncoder;
032import org.opencms.i18n.CmsMessageContainer;
033import org.opencms.main.CmsLog;
034import org.opencms.util.CmsStringUtil;
035
036import java.io.UnsupportedEncodingException;
037import java.security.MessageDigest;
038import java.security.NoSuchAlgorithmException;
039import java.security.SecureRandom;
040import java.util.Locale;
041
042import org.apache.commons.codec.binary.Base64;
043import org.apache.commons.logging.Log;
044
045import com.lambdaworks.crypto.SCryptUtil;
046
047/**
048 * Default implementation for OpenCms password validation,
049 * just checks if a password is at last 4 characters long.<p>
050 *
051 * @since 6.0.0
052 */
053public class CmsDefaultPasswordHandler
054implements I_CmsPasswordHandler, I_CmsPasswordSecurityEvaluator, I_CmsPasswordGenerator {
055
056    /** Parameter for SCrypt fall back. */
057    public static String PARAM_SCRYPT_FALLBACK = "scrypt.fallback";
058
059    /** Parameter for SCrypt settings. */
060    public static String PARAM_SCRYPT_SETTINGS = "scrypt.settings";
061
062    /**  The minimum length of a password. */
063    public static final int PASSWORD_MIN_LENGTH = 4;
064
065    /** The password length that is considered to be secure. */
066    public static final int PASSWORD_SECURE_LENGTH = 8;
067
068    /** The log object for this class. */
069    private static final Log LOG = CmsLog.getLog(CmsDefaultPasswordHandler.class);
070
071    /** The secure random number generator. */
072    private static SecureRandom m_secureRandom;
073
074    /** The configuration of the password handler. */
075    private CmsParameterConfiguration m_configuration;
076
077    /** The digest type used. */
078    private String m_digestType = DIGEST_TYPE_SCRYPT;
079
080    /** The encoding the encoding used for translating the input string to bytes. */
081    private String m_inputEncoding = CmsEncoder.ENCODING_UTF_8;
082
083    /** SCrypt fall back algorithm. */
084    private String m_scryptFallback;
085
086    /** SCrypt parameter: CPU cost, must be a power of 2. */
087    private int m_scryptN;
088
089    /** SCrypt parameter: Parallelization parameter. */
090    private int m_scryptP;
091
092    /** SCrypt parameter: Memory cost. */
093    private int m_scryptR;
094
095    /**
096     * The constructor does not perform any operation.<p>
097     */
098    public CmsDefaultPasswordHandler() {
099
100        m_configuration = new CmsParameterConfiguration();
101    }
102
103    /**
104     * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#addConfigurationParameter(java.lang.String, java.lang.String)
105     */
106    public void addConfigurationParameter(String paramName, String paramValue) {
107
108        m_configuration.put(paramName, paramValue);
109    }
110
111    /**
112     * @see org.opencms.security.I_CmsPasswordHandler#checkPassword(String, String, boolean)
113     */
114    public boolean checkPassword(String plainPassword, String digestedPassword, boolean useFallback) {
115
116        boolean success = false;
117        if (DIGEST_TYPE_PLAIN.equals(m_digestType)) {
118
119            success = plainPassword.equals(digestedPassword);
120        } else if (DIGEST_TYPE_SCRYPT.equals(m_digestType)) {
121            try {
122                success = SCryptUtil.check(plainPassword, digestedPassword);
123            } catch (IllegalArgumentException e) {
124                // hashed valued not right, check if we want to fall back to MD5
125                if (useFallback) {
126                    try {
127                        success = digestedPassword.equals(digest(plainPassword, m_scryptFallback, m_inputEncoding));
128                    } catch (CmsPasswordEncryptionException e1) {
129                        // success will be false
130                    }
131                }
132            }
133        } else {
134            // old default MD5
135            try {
136                success = digestedPassword.equals(digest(plainPassword));
137            } catch (CmsPasswordEncryptionException e) {
138                // this indicates validation has failed
139            }
140        }
141        return success;
142    }
143
144    /**
145     * @see org.opencms.security.I_CmsPasswordHandler#digest(java.lang.String)
146     */
147    public String digest(String password) throws CmsPasswordEncryptionException {
148
149        return digest(password, m_digestType, m_inputEncoding);
150    }
151
152    /**
153     * @see org.opencms.security.I_CmsPasswordHandler#digest(java.lang.String, java.lang.String, java.lang.String)
154     */
155    public String digest(String password, String digestType, String inputEncoding)
156    throws CmsPasswordEncryptionException {
157
158        MessageDigest md;
159        String result;
160
161        try {
162            if (DIGEST_TYPE_PLAIN.equals(digestType.toLowerCase())) {
163
164                result = password;
165
166            } else if (DIGEST_TYPE_SCRYPT.equals(digestType.toLowerCase())) {
167
168                result = SCryptUtil.scrypt(password, m_scryptN, m_scryptR, m_scryptP);
169            } else if (DIGEST_TYPE_SSHA.equals(digestType.toLowerCase())) {
170
171                byte[] salt = new byte[4];
172                byte[] digest;
173                byte[] total;
174
175                if (m_secureRandom == null) {
176                    m_secureRandom = SecureRandom.getInstance("SHA1PRNG");
177                }
178                m_secureRandom.nextBytes(salt);
179
180                md = MessageDigest.getInstance(DIGEST_TYPE_SHA);
181                md.reset();
182                md.update(password.getBytes(inputEncoding));
183                md.update(salt);
184
185                digest = md.digest();
186                total = new byte[digest.length + salt.length];
187                System.arraycopy(digest, 0, total, 0, digest.length);
188                System.arraycopy(salt, 0, total, digest.length, salt.length);
189
190                result = new String(Base64.encodeBase64(total));
191
192            } else {
193
194                md = MessageDigest.getInstance(digestType);
195                md.reset();
196                md.update(password.getBytes(inputEncoding));
197                result = new String(Base64.encodeBase64(md.digest()));
198
199            }
200        } catch (NoSuchAlgorithmException e) {
201            CmsMessageContainer message = Messages.get().container(Messages.ERR_UNSUPPORTED_ALGORITHM_1, digestType);
202            if (LOG.isErrorEnabled()) {
203                LOG.error(message.key(), e);
204            }
205            throw new CmsPasswordEncryptionException(message, e);
206        } catch (UnsupportedEncodingException e) {
207            CmsMessageContainer message = Messages.get().container(
208                Messages.ERR_UNSUPPORTED_PASSWORD_ENCODING_1,
209                inputEncoding);
210            if (LOG.isErrorEnabled()) {
211                LOG.error(message.key(), e);
212            }
213            throw new CmsPasswordEncryptionException(message, e);
214        }
215
216        return result;
217    }
218
219    /**
220     * @see org.opencms.security.I_CmsPasswordSecurityEvaluator#evaluatePasswordSecurity(java.lang.String)
221     */
222    public SecurityLevel evaluatePasswordSecurity(String password) {
223
224        SecurityLevel result;
225        if (password.length() < PASSWORD_MIN_LENGTH) {
226            result = SecurityLevel.invalid;
227        } else if (password.length() < PASSWORD_SECURE_LENGTH) {
228            result = SecurityLevel.weak;
229        } else {
230            result = SecurityLevel.strong;
231        }
232        return result;
233    }
234
235    /**
236     * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#getConfiguration()
237     */
238    public CmsParameterConfiguration getConfiguration() {
239
240        return m_configuration;
241    }
242
243    /**
244     * Returns the digestType.<p>
245     *
246     * @return the digestType
247     */
248    public String getDigestType() {
249
250        return m_digestType;
251    }
252
253    /**
254     * Returns the input encoding.<p>
255     *
256     * @return the input encoding
257     */
258    public String getInputEncoding() {
259
260        return m_inputEncoding;
261    }
262
263    /**
264     * @see org.opencms.security.I_CmsPasswordSecurityEvaluator#getPasswordSecurityHint(java.util.Locale)
265     */
266    public String getPasswordSecurityHint(Locale locale) {
267
268        return Messages.get().getBundle(locale).key(
269            Messages.GUI_PASSWORD_SECURITY_HINT_1,
270            Integer.valueOf(PASSWORD_SECURE_LENGTH));
271    }
272
273    /**
274     * @see org.opencms.security.I_CmsPasswordGenerator#getRandomPassword()
275     */
276    public String getRandomPassword() {
277
278        return CmsDefaultPasswordGenerator.getRandomPWD();
279    }
280
281    /**
282     * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#initConfiguration()
283     */
284    public void initConfiguration() {
285
286        // simple default configuration does not need to be initialized
287        if (LOG.isDebugEnabled()) {
288            CmsMessageContainer message = Messages.get().container(Messages.LOG_INIT_CONFIG_CALLED_1, this);
289            LOG.debug(message.key());
290            LOG.debug(Messages.get().getBundle().key(Messages.LOG_INIT_CONFIG_CALLED_1, this));
291        }
292        m_configuration = CmsParameterConfiguration.unmodifiableVersion(m_configuration);
293
294        // Set default SCrypt parameter values
295        m_scryptN = 16384; // CPU cost, must be a power of 2
296        m_scryptR = 8; // Memory cost
297        m_scryptP = 1; // Parallelization parameter
298
299        String scryptSettings = m_configuration.get(PARAM_SCRYPT_SETTINGS);
300        if (scryptSettings != null) {
301            String[] settings = CmsStringUtil.splitAsArray(scryptSettings, ',');
302            if (settings.length == 3) {
303                // we just require 3 correct parameters
304                m_scryptN = CmsStringUtil.getIntValue(settings[0], m_scryptN, "scryptN using " + m_scryptN);
305                m_scryptR = CmsStringUtil.getIntValue(settings[1], m_scryptR, "scryptR using " + m_scryptR);
306                m_scryptP = CmsStringUtil.getIntValue(settings[2], m_scryptP, "scryptP using " + m_scryptP);
307            } else {
308                if (LOG.isDebugEnabled()) {
309                    LOG.debug(Messages.get().getBundle().key(Messages.LOG_SCRYPT_PARAMETERS_1, scryptSettings));
310                }
311            }
312        }
313
314        // Initialize the SCrypt fall back
315        m_scryptFallback = DIGEST_TYPE_MD5;
316        String scryptFallback = m_configuration.get(PARAM_SCRYPT_FALLBACK);
317        if (scryptFallback != null) {
318
319            try {
320                MessageDigest.getInstance(scryptFallback);
321                // Configured fall back algorithm available
322                m_scryptFallback = scryptFallback;
323            } catch (NoSuchAlgorithmException e) {
324                // Configured fall back algorithm not available, use default MD5
325                if (LOG.isDebugEnabled()) {
326                    LOG.debug(Messages.get().getBundle().key(Messages.LOG_SCRYPT_PARAMETERS_1, scryptFallback));
327                }
328            }
329        }
330    }
331
332    /**
333     * Sets the digestType.<p>
334     *
335     * @param digestType the digestType to set
336     */
337    public void setDigestType(String digestType) {
338
339        m_digestType = digestType.toLowerCase();
340    }
341
342    /**
343     * Sets the input encoding.<p>
344     *
345     * @param inputEncoding the input encoding to set
346     */
347    public void setInputEncoding(String inputEncoding) {
348
349        m_inputEncoding = inputEncoding;
350    }
351
352    /**
353     * @see org.opencms.security.I_CmsPasswordHandler#validatePassword(java.lang.String)
354     */
355    public void validatePassword(String password) throws CmsSecurityException {
356
357        if ((password == null) || (password.length() < PASSWORD_MIN_LENGTH)) {
358            throw new CmsSecurityException(
359                Messages.get().container(Messages.ERR_PASSWORD_TOO_SHORT_1, Integer.valueOf(PASSWORD_MIN_LENGTH)));
360        }
361    }
362}