PasswordFactory.java

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

  35. import java.security.MessageDigest;
  36. import java.security.NoSuchAlgorithmException;
  37. import java.security.SecureRandom;
  38. import java.security.spec.InvalidKeySpecException;
  39. import java.util.Arrays;
  40. import java.util.Random;
  41. import java.util.regex.Matcher;
  42. import java.util.regex.Pattern;

  43. import javax.crypto.SecretKeyFactory;
  44. import javax.crypto.spec.PBEKeySpec;

  45. import org.apache.commons.codec.DecoderException;
  46. import org.apache.commons.codec.binary.Hex;

  47. import fr.paris.lutece.portal.service.util.AppException;
  48. import fr.paris.lutece.portal.service.util.AppLogService;
  49. import fr.paris.lutece.portal.service.util.AppPropertiesService;
  50. import fr.paris.lutece.portal.service.util.CryptoService;
  51. import fr.paris.lutece.util.password.IPassword;
  52. import fr.paris.lutece.util.password.IPasswordFactory;

  53. /**
  54.  * A factory for getting storable password representation
  55.  */
  56. final class PasswordFactory implements IPasswordFactory
  57. {
  58.     // storage types
  59.     private static final String ERROR_PASSWORD_STORAGE = "Invalid stored password ";
  60.     private static final String PBKDF2WITHHMACSHA1_STORAGE_TYPE = "PBKDF2";
  61.     private static final String PBKDF2WITHHMACSHA512_STORAGE_TYPE = "PBKDF2WITHHMACSHA512";
  62.     private static final String PLAINTEXT_STORAGE_TYPE = "PLAINTEXT";
  63.     private static final String DUMMY_STORAGE_TYPE = "\0DUMMY\0";
  64.     private static final String DUMMY_STORED_PASSWORD = DUMMY_STORAGE_TYPE + ":\0";

  65.     @Override
  66.     public IPassword getPassword( String strStoredPassword )
  67.     {
  68.         int storageTypeSeparatorIndex = strStoredPassword.indexOf( ':' );
  69.         if ( storageTypeSeparatorIndex == -1 )
  70.         {
  71.             throw new IllegalArgumentException( strStoredPassword );
  72.         }
  73.         String storageType = strStoredPassword.substring( 0, storageTypeSeparatorIndex );
  74.         String password = strStoredPassword.substring( storageTypeSeparatorIndex + 1 );
  75.         switch( storageType )
  76.         {
  77.             case PLAINTEXT_STORAGE_TYPE:
  78.                 return new PlaintextPassword( password );
  79.             case PBKDF2WITHHMACSHA1_STORAGE_TYPE:
  80.                 return new PBKDF2WithHmacSHA1Password( password );
  81.             case PBKDF2WITHHMACSHA512_STORAGE_TYPE:
  82.                 return new PBKDF2WithHmacSHA512Password( password );
  83.             case DUMMY_STORAGE_TYPE:
  84.                 return new DummyPassword( );
  85.             default:
  86.                 return new DigestPassword( storageType, password );
  87.         }
  88.     }

  89.     @Override
  90.     public IPassword getPasswordFromCleartext( String strUserPassword )
  91.     {
  92.         return new PBKDF2WithHmacSHA512Password( strUserPassword, PBKDF2Password.PASSWORD_REPRESENTATION.CLEARTEXT );
  93.     }

  94.     @Override
  95.     public IPassword getDummyPassword( )
  96.     {
  97.         return getPassword( DUMMY_STORED_PASSWORD );
  98.     }

  99.     /**
  100.      * A Password stored using PBKDF2
  101.      */
  102.     private abstract static class PBKDF2Password implements IPassword
  103.     {

  104.         /**
  105.          * Enum to specify if the password is constructed from cleartext or hashed form
  106.          */
  107.         enum PASSWORD_REPRESENTATION
  108.         {
  109.             CLEARTEXT,
  110.             STORABLE
  111.         }

  112.         /** Storage format : iterations:hex(salt):hex(hash) */
  113.         private static final Pattern FORMAT = Pattern.compile( "^(\\d+):([a-z0-9]+):([a-z0-9]+)$", Pattern.CASE_INSENSITIVE );
  114.         private static final Random RANDOM;

  115.         // init the random number generator
  116.         static
  117.         {
  118.             Random rand;
  119.             try
  120.             {
  121.                 rand = SecureRandom.getInstance( "SHA1PRNG" );
  122.             }
  123.             catch( NoSuchAlgorithmException e )
  124.             {
  125.                 AppLogService.error( "SHA1PRNG is not availabled. Picking the default SecureRandom.", e );
  126.                 rand = new SecureRandom( );
  127.             }
  128.             RANDOM = rand;
  129.         }

  130.         static final String PROPERTY_PASSWORD_HASH_ITERATIONS = "password.hash.iterations";
  131.         static final int DEFAULT_HASH_ITERATIONS = 210000;
  132.         private static final String PROPERTY_PASSWORD_HASH_LENGTH = "password.hash.length";

  133.         /** number of iterations */
  134.         final int _iterations;
  135.         /** salt */
  136.         private final byte [ ] _salt;
  137.         /** hashed password */
  138.         private final byte [ ] _hash;

  139.         /**
  140.          * Construct a password from the stored representation
  141.          *
  142.          * @param strStoredPassword
  143.          *            the stored representation
  144.          */
  145.         public PBKDF2Password( String strStoredPassword )
  146.         {
  147.             this( strStoredPassword, PASSWORD_REPRESENTATION.STORABLE );
  148.         }

  149.         /**
  150.          * Construct a password
  151.          *
  152.          * @param strPassword
  153.          *            the password text
  154.          * @param representation
  155.          *            representation of strPassword
  156.          */
  157.         public PBKDF2Password( String strPassword, PASSWORD_REPRESENTATION representation )
  158.         {
  159.             switch( representation )
  160.             {
  161.                 case CLEARTEXT:
  162.                     _iterations = AppPropertiesService.getPropertyInt( PROPERTY_PASSWORD_HASH_ITERATIONS, DEFAULT_HASH_ITERATIONS );
  163.                     int hashLength = AppPropertiesService.getPropertyInt( PROPERTY_PASSWORD_HASH_LENGTH, 128 );
  164.                     try
  165.                     {
  166.                         _salt = new byte [ 16];
  167.                         RANDOM.nextBytes( _salt );
  168.                         PBEKeySpec spec = new PBEKeySpec( strPassword.toCharArray( ), _salt, _iterations, hashLength * 8 );
  169.                         SecretKeyFactory skf = SecretKeyFactory.getInstance( getAlgorithm( ) );
  170.                         _hash = skf.generateSecret( spec ).getEncoded( );
  171.                     }
  172.                     catch( NoSuchAlgorithmException | InvalidKeySpecException e )
  173.                     {
  174.                         throw new AppException( "Invalid Algo or key", e ); // should not happen
  175.                     }
  176.                     break;
  177.                 case STORABLE:
  178.                     Matcher matcher = FORMAT.matcher( strPassword );

  179.                     if ( !matcher.matches( ) || matcher.groupCount( ) != 3 )
  180.                     {
  181.                         throw new IllegalArgumentException( ERROR_PASSWORD_STORAGE + strPassword );
  182.                     }
  183.                     _iterations = Integer.valueOf( matcher.group( 1 ) );
  184.                     try
  185.                     {
  186.                         _salt = Hex.decodeHex( matcher.group( 2 ).toCharArray( ) );
  187.                         _hash = Hex.decodeHex( matcher.group( 3 ).toCharArray( ) );
  188.                     }
  189.                     catch( DecoderException e )
  190.                     {
  191.                         throw new IllegalArgumentException( ERROR_PASSWORD_STORAGE + strPassword );
  192.                     }
  193.                     break;
  194.                 default:
  195.                     throw new IllegalArgumentException( representation.toString( ) );
  196.             }
  197.         }

  198.         /**
  199.          * Get the PBKDF2 algorithm
  200.          *
  201.          * @return the PBKDF2 algorithm to use
  202.          */
  203.         protected abstract String getAlgorithm( );

  204.         @Override
  205.         public boolean check( String strCleartextPassword )
  206.         {
  207.             PBEKeySpec spec = new PBEKeySpec( strCleartextPassword.toCharArray( ), _salt, _iterations, _hash.length * 8 );
  208.             try
  209.             {
  210.                 SecretKeyFactory skf = SecretKeyFactory.getInstance( getAlgorithm( ) );
  211.                 byte [ ] testHash = skf.generateSecret( spec ).getEncoded( );
  212.                 return Arrays.equals( _hash, testHash );
  213.             }
  214.             catch( NoSuchAlgorithmException | InvalidKeySpecException e )
  215.             {
  216.                 throw new AppException( "Invalid Algo or key", e ); // should not happen
  217.             }
  218.         }

  219.         /**
  220.          * Get the storage type identifier
  221.          *
  222.          * @return the storage type identifier
  223.          */
  224.         protected abstract String getStorageType( );

  225.         @Override
  226.         public String getStorableRepresentation( )
  227.         {
  228.             StringBuilder sb = new StringBuilder( );
  229.             sb.append( getStorageType( ) ).append( ':' );
  230.             sb.append( _iterations ).append( ':' ).append( Hex.encodeHex( _salt ) );
  231.             sb.append( ':' ).append( Hex.encodeHex( _hash ) );
  232.             return sb.toString( );
  233.         }

  234.     }

  235.     /**
  236.      * A Password stored using PBKDF2WithHmacSHA1
  237.      */
  238.     private static final class PBKDF2WithHmacSHA1Password extends PBKDF2Password
  239.     {

  240.         /**
  241.          * Construct a password from the stored representation
  242.          *
  243.          * @param strStoredPassword
  244.          *            the stored representation
  245.          */
  246.         public PBKDF2WithHmacSHA1Password( String strStoredPassword )
  247.         {
  248.             super( strStoredPassword );
  249.         }

  250.         @Override
  251.         public boolean isLegacy( )
  252.         {
  253.             return true;
  254.         }

  255.         @Override
  256.         protected String getAlgorithm( )
  257.         {
  258.             return "PBKDF2WithHmacSHA1";
  259.         }

  260.         @Override
  261.         protected String getStorageType( )
  262.         {
  263.             return PBKDF2WITHHMACSHA1_STORAGE_TYPE;
  264.         }

  265.         /**
  266.          * Legacy passwords must not be stored.
  267.          *
  268.          * @return never returns
  269.          * @throws UnsupportedOperationException
  270.          *             Always thrown because Legacy passwords must not be stored.
  271.          */
  272.         @Override
  273.         public String getStorableRepresentation( )
  274.         {
  275.             throw new UnsupportedOperationException( "Must not store a legacy password" );
  276.         }

  277.     }

  278.     /**
  279.      * A Password stored using PBKDF2WithHmacSHA512
  280.      */
  281.     private static class PBKDF2WithHmacSHA512Password extends PBKDF2Password
  282.     {

  283.         /**
  284.          * Construct a password from the stored representation
  285.          *
  286.          * @param strStoredPassword
  287.          *            the stored representation
  288.          */
  289.         public PBKDF2WithHmacSHA512Password( String strStoredPassword )
  290.         {
  291.             super( strStoredPassword );
  292.         }

  293.         /**
  294.          * Construct a password
  295.          *
  296.          * @param strStoredPassword
  297.          *            the password text
  298.          * @param representation
  299.          *            representation of strPassword
  300.          */
  301.         public PBKDF2WithHmacSHA512Password( String strStoredPassword, PASSWORD_REPRESENTATION representation )
  302.         {
  303.             super( strStoredPassword, representation );
  304.         }

  305.         @Override
  306.         public boolean isLegacy( )
  307.         {
  308.             int iterations = AppPropertiesService.getPropertyInt( PROPERTY_PASSWORD_HASH_ITERATIONS, DEFAULT_HASH_ITERATIONS );
  309.             return _iterations < iterations;
  310.         }

  311.         @Override
  312.         protected String getAlgorithm( )
  313.         {
  314.             return "PBKDF2WithHmacSHA512";
  315.         }

  316.         @Override
  317.         protected String getStorageType( )
  318.         {
  319.             return PBKDF2WITHHMACSHA512_STORAGE_TYPE;
  320.         }

  321.     }

  322.     /**
  323.      * Dummy password which never matches a user password, but takes the same time as the PBKDF2Password to do so.
  324.      */
  325.     private static final class DummyPassword extends PBKDF2WithHmacSHA512Password
  326.     {
  327.         DummyPassword( )
  328.         {
  329.             // take the same time to construct as a proper PBKDF2Password
  330.             super( "", PASSWORD_REPRESENTATION.CLEARTEXT );
  331.         }

  332.         @Override
  333.         public boolean check( String strCleartextPassword )
  334.         {
  335.             // take the same time to check as a proper PBKDF2Password
  336.             super.check( strCleartextPassword );
  337.             return false;
  338.         }

  339.         @Override
  340.         public String getStorableRepresentation( )
  341.         {
  342.             throw new UnsupportedOperationException( "Must not store a dummy password" );
  343.         }
  344.     }

  345.     /**
  346.      * Legacy password implementation super class
  347.      */
  348.     private abstract static class LegacyPassword implements IPassword
  349.     {
  350.         /**
  351.          * Legacy passwords are legacy
  352.          *
  353.          * @return <code>true</code>
  354.          */
  355.         @Override
  356.         public final boolean isLegacy( )
  357.         {
  358.             return true;
  359.         }

  360.         /**
  361.          * Legacy passwords must not be stored.
  362.          *
  363.          * @return never returns
  364.          * @throws UnsupportedOperationException
  365.          *             Always thrown because Legacy passwords must not be stored.
  366.          */
  367.         @Override
  368.         public final String getStorableRepresentation( )
  369.         {
  370.             throw new UnsupportedOperationException( "Passwords should not be stored without proper hashing and salting" );
  371.         }

  372.     }

  373.     /**
  374.      * Password stored as plaintext
  375.      */
  376.     private static final class PlaintextPassword extends LegacyPassword
  377.     {

  378.         /** the stored password */
  379.         private final String _strPassword;

  380.         /**
  381.          * Constructor
  382.          *
  383.          * @param strStoredPassword
  384.          *            the stored password
  385.          */
  386.         public PlaintextPassword( String strStoredPassword )
  387.         {
  388.             _strPassword = strStoredPassword;
  389.         }

  390.         @Override
  391.         public boolean check( String strCleartextPassword )
  392.         {
  393.             return _strPassword != null && _strPassword.equals( strCleartextPassword );
  394.         }

  395.     }

  396.     /**
  397.      * Password stored as {@link MessageDigest} output
  398.      */
  399.     private static final class DigestPassword extends LegacyPassword
  400.     {
  401.         /** the stored password */
  402.         private final String _strPassword;
  403.         /** the digest algorithm */
  404.         private final String _strAlgorithm;

  405.         /**
  406.          * Constructor
  407.          *
  408.          * @param strAlgorithm
  409.          *            the digest algorithm
  410.          * @param strStoredPassword
  411.          *            the stored password
  412.          */
  413.         public DigestPassword( String strAlgorithm, String strStoredPassword )
  414.         {
  415.             _strPassword = strStoredPassword;
  416.             // check for algorithm support
  417.             try
  418.             {
  419.                 MessageDigest.getInstance( strAlgorithm );
  420.             }
  421.             catch( NoSuchAlgorithmException e )
  422.             {
  423.                 throw new IllegalArgumentException( e );
  424.             }
  425.             _strAlgorithm = strAlgorithm;
  426.         }

  427.         @Override
  428.         public boolean check( String strCleartextPassword )
  429.         {
  430.             return _strPassword != null && _strPassword.equals( CryptoService.encrypt( strCleartextPassword, _strAlgorithm ) );
  431.         }
  432.     }
  433. }