PasswordUtil.java

/*
 * Copyright (c) 2002-2022, City of Paris
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice
 *     and the following disclaimer.
 *
 *  2. Redistributions in binary form must reproduce the above copyright notice
 *     and the following disclaimer in the documentation and/or other materials
 *     provided with the distribution.
 *
 *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
 *     contributors may be used to endorse or promote products derived from
 *     this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * License 1.0
 */
package fr.paris.lutece.util.password;

import fr.paris.lutece.portal.service.admin.AdminUserService;
import fr.paris.lutece.portal.service.util.AppPropertiesService;
import fr.paris.lutece.util.date.DateUtil;

import java.security.SecureRandom;
import java.sql.Timestamp;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Random;

/**
 * Utility class used to generate random passwords
 */
public final class PasswordUtil
{
    public static final String PROPERTY_PASSWORD_SIZE = "randomPassword.size";
    public static final int CONSTANT_DEFAULT_RANDOM_PASSWORD_SIZE = 16;
    private static final int CONSTANT_NUMBER_LETTERS = 26;
    private static final int CONSTANT_NUMBER_NUMBERS_BASE10 = 10;
    private static final int CONSTANT_ASCII_CODE_A_UPPERCASE = 65;
    private static final int CONSTANT_ASCII_CODE_A_LOWERCASE = 97;
    private static final int CONSTANT_ASCII_CODE_ZERO = 48;
    private static final char [ ] CONSTANT_SPECIAL_CHARACTERS = {
            '!', ',', ':', '?', '$', '-', '@', '}', '{', '(', ')', '*', '+', '=', '[', ']', '%', '.',
    };
    private static final String CONSTANT_PASSWORD_BEGIN_REGEX = "^";
    private static final String CONSTANT_PASSWORD_REGEX_NUM = "(?=.*[0-9])";
    private static final String CONSTANT_PASSWORD_REGEX_SPECIAL = "(?=.*[^a-zA-Z0-9])";
    private static final String CONSTANT_PASSWORD_REGEX_UPPER_LOWER = "(?=.*[a-z])(?=.*[A-Z])";
    private static final String CONSTANT_PASSWORD_END_REGEX = "(.*)$";
    private static final String PARAMETER_PASSWORD_MINIMUM_LENGTH = "password_minimum_length";

    /** Private Constructor */
    private PasswordUtil( )
    {
    }

    /**
     * Generate a new random password
     * 
     * @return the new password
     */
    public static String makePassword( )
    {
        // reinitialize password
        int nPasswordSize = AppPropertiesService.getPropertyInt( PROPERTY_PASSWORD_SIZE, CONSTANT_DEFAULT_RANDOM_PASSWORD_SIZE );
        int nMinPasswordSize = AdminUserService.getIntegerSecurityParameter( PARAMETER_PASSWORD_MINIMUM_LENGTH );

        if ( nMinPasswordSize > nPasswordSize )
        {
            nPasswordSize = nMinPasswordSize;
        }

        return makePassword( nPasswordSize, true, true, true );
    }

    /**
     * Generate a new random password
     * 
     * @param nPasswordSize
     *            the password size
     * @param bUpperAndLowerCase
     *            true if the password must contain upper and lower case
     * @param bNumbers
     *            if the password must contain numbers
     * @param bSpecialCaracters
     *            if the password must contain special characters
     *
     * @return the new password
     */
    public static String makePassword( int nPasswordSize, boolean bUpperAndLowerCase, boolean bNumbers, boolean bSpecialCaracters )
    {
        // reinitialize password
        Random r = new SecureRandom( );

        ArrayList<Character> listCharacters = new ArrayList<>( nPasswordSize );

        // No of Big letters
        int nNumCapitalLetters = bUpperAndLowerCase ? ( r.nextInt( nPasswordSize - 3 ) + 1 ) : 0; // choose a number between 1 and CONSTANT_PASSWORD_SIZE -1

        // no on special characters
        int nNumSpecial = bSpecialCaracters ? ( r.nextInt( nPasswordSize - 2 - nNumCapitalLetters ) + 1 ) : 0; // choose a number beetwen 1 and
                                                                                                               // CONSTANT_PASSWORD_SIZE - a1

        // no of nos
        int nNumNumbers = bNumbers ? ( r.nextInt( nPasswordSize - 1 - nNumCapitalLetters - nNumSpecial ) + 1 ) : 0; // choose a number to complete list of
                                                                                                                    // CONSTANT_PASSWORD_SIZE characters

        // no of small
        int nNumSmallLetters = nPasswordSize - nNumCapitalLetters - nNumSpecial - nNumNumbers; // choose a number to complete list of CONSTANT_PASSWORD_SIZE
                                                                                               // characters

        for ( int j = 0; j < nNumCapitalLetters; j++ )
        {
            char c1 = (char) ( r.nextInt( CONSTANT_NUMBER_LETTERS ) + CONSTANT_ASCII_CODE_A_UPPERCASE );
            listCharacters.add( Character.valueOf( c1 ) );
        }

        for ( int j = 0; j < nNumSmallLetters; j++ )
        {
            char c1 = (char) ( r.nextInt( CONSTANT_NUMBER_LETTERS ) + CONSTANT_ASCII_CODE_A_LOWERCASE );
            listCharacters.add( Character.valueOf( c1 ) );
        }

        for ( int j = 0; j < nNumNumbers; j++ )
        {
            char c1 = (char) ( r.nextInt( CONSTANT_NUMBER_NUMBERS_BASE10 - 1 ) + CONSTANT_ASCII_CODE_ZERO );
            listCharacters.add( Character.valueOf( c1 ) );
        }

        for ( int j = 0; j < nNumSpecial; j++ )
        {
            char c1 = CONSTANT_SPECIAL_CHARACTERS [r.nextInt( CONSTANT_SPECIAL_CHARACTERS.length )];
            listCharacters.add( Character.valueOf( c1 ) );
        }

        Collections.shuffle( listCharacters, r );

        StringBuilder sbPassword = new StringBuilder( listCharacters.size( ) );

        for ( Character myChar : listCharacters )
        {
            sbPassword.append( myChar );
        }

        return sbPassword.toString( );
    }

    /**
     * Check whether a password contains upper and lower case letters, special characters and numbers.
     * 
     * @param strPassword
     *            The password to check
     * @return True if the password format is correct, false otherwise
     */
    public static boolean checkPasswordFormat( String strPassword )
    {
        return checkPasswordFormat( strPassword, true, true, true );
    }

    /**
     * Check whether a password contains upper and lower case letters, special characters and numbers.
     * 
     * @param strPassword
     *            The password to check
     * @param bUpperAndLowerCase
     *            true if the password must contain upper and lower case
     * @param bNumero
     *            if the password must contain numero
     * @param bSpecialCaracters
     *            if the password must contain special characters
     *
     * @return True if the password format is correct, false otherwise
     */
    public static boolean checkPasswordFormat( String strPassword, boolean bUpperAndLowerCase, boolean bNumero, boolean bSpecialCaracters )
    {
        if ( ( strPassword == null ) || strPassword.isEmpty( ) )
        {
            return false;
        }

        StringBuilder sbRegex = new StringBuilder( CONSTANT_PASSWORD_BEGIN_REGEX );

        if ( bUpperAndLowerCase )
        {
            sbRegex.append( CONSTANT_PASSWORD_REGEX_UPPER_LOWER );
        }

        if ( bNumero )
        {
            sbRegex.append( CONSTANT_PASSWORD_REGEX_NUM );
        }

        if ( bSpecialCaracters )
        {
            sbRegex.append( CONSTANT_PASSWORD_REGEX_SPECIAL );
        }

        sbRegex.append( CONSTANT_PASSWORD_END_REGEX );

        return strPassword.matches( sbRegex.toString( ) );
    }

    /**
     * Get the maximum valid date of a password starting from now with the given number of days.
     * 
     * @param nNumberDay
     *            The number of days the password is valid
     * @return The maximum valid date of a password
     */
    public static Timestamp getPasswordMaxValidDate( int nNumberDay )
    {
        if ( nNumberDay <= 0 )
        {
            return null;
        }

        long nMilliSeconds = DateUtil.convertDaysInMiliseconds( nNumberDay );
        return new Timestamp( new java.util.Date( ).getTime( ) + nMilliSeconds );
    }
}