    001/*
    002    Licensed to the Apache Software Foundation (ASF) under one
    003    or more contributor license agreements.  See the NOTICE file
    004    distributed with this work for additional information
    005    regarding copyright ownership.  The ASF licenses this file
    006    to you under the Apache License, Version 2.0 (the
    007    "License"); you may not use this file except in compliance
    008    with the License.  You may obtain a copy of the License at
    009
    010       http://www.apache.org/licenses/LICENSE-2.0
    011
    012    Unless required by applicable law or agreed to in writing,
    013    software distributed under the License is distributed on an
    014    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    015    KIND, either express or implied.  See the License for the
    016    specific language governing permissions and limitations
    017    under the License.
    018 */
    019package org.apache.wiki.auth.user;
    020
    021import org.apache.commons.lang3.math.NumberUtils;
    022import org.apache.logging.log4j.LogManager;
    023import org.apache.logging.log4j.Logger;
    024import org.apache.wiki.api.core.Engine;
    025import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
    026import org.apache.wiki.auth.NoSuchPrincipalException;
    027import org.apache.wiki.auth.WikiPrincipal;
    028import org.apache.wiki.auth.WikiSecurityException;
    029import org.apache.wiki.util.ByteUtils;
    030import org.apache.wiki.util.CryptoUtil;
    031
    032import java.nio.charset.StandardCharsets;
    033import java.security.MessageDigest;
    034import java.security.NoSuchAlgorithmException;
    035import java.security.Principal;
    036import java.util.ArrayList;
    037import java.util.Properties;
    038import java.util.UUID;
    039
    040/**
    041 * Abstract UserDatabase class that provides convenience methods for finding profiles, building Principal collections and hashing passwords.
    042 *
    043 * @since 2.3
    044 */
    045public abstract class AbstractUserDatabase implements UserDatabase {
    046
    047    protected static final Logger LOG = LogManager.getLogger( AbstractUserDatabase.class );
    048    protected static final String SHA_PREFIX = "{SHA}";
    049    protected static final String SSHA_PREFIX = "{SSHA}";
    050    protected static final String SHA256_PREFIX = "{SHA-256}";
    051    protected Engine m_engine;
    052    /**
    053     * Looks up and returns the first {@link UserProfile} in the user database that whose login name, full name, or wiki name matches the
    054     * supplied string. This method provides a "forgiving" search algorithm for resolving principal names when the exact profile attribute
    055     * that supplied the name is unknown.
    056     *
    057     * @param index the login name, full name, or wiki name
    058     * @return non null
    059     * @throws org.apache.wiki.auth.NoSuchPrincipalException
    060     * @see org.apache.wiki.auth.user.UserDatabase#find(java.lang.String)
    061     */
    062    @Override
    063    public UserProfile find( final String index ) throws NoSuchPrincipalException {
    064        UserProfile profile = null;
    065
    066        // Try finding by full name
    067        try {
    068            profile = findByFullName( index );
    069        } catch( final NoSuchPrincipalException e ) {
    070            LOG.debug(e.getMessage(), e);
    071        }
    072        if( profile != null ) {
    073            return profile;
    074        }
    075
    076        // Try finding by wiki name
    077        try {
    078            profile = findByWikiName( index );
    079        } catch( final NoSuchPrincipalException e ) {
    080            LOG.debug(e.getMessage(), e);
    081        }
    082        if( profile != null ) {
    083            return profile;
    084        }
    085
    086        // Try finding by login name
    087        try {
    088            profile = findByLoginName( index );
    089        } catch( final NoSuchPrincipalException e ) {
    090            LOG.debug(e.getMessage(), e);
    091        }
    092        if( profile != null ) {
    093            return profile;
    094        }
    095
    096        throw new NoSuchPrincipalException( "Not in database: " + index );
    097    }
    098
    099    /**
    100     * {@inheritDoc}
    101     * @see org.apache.wiki.auth.user.UserDatabase#findByEmail(java.lang.String)
    102     */
    103    @Override
    104    public abstract UserProfile findByEmail( String index ) throws NoSuchPrincipalException;
    105
    106    /**
    107     * {@inheritDoc}
    108     * @see org.apache.wiki.auth.user.UserDatabase#findByFullName(java.lang.String)
    109     */
    110    @Override
    111    public abstract UserProfile findByFullName( String index ) throws NoSuchPrincipalException;
    112
    113    /**
    114     * {@inheritDoc}
    115     * @see org.apache.wiki.auth.user.UserDatabase#findByLoginName(java.lang.String)
    116     */
    117    @Override
    118    public abstract UserProfile findByLoginName( String index ) throws NoSuchPrincipalException;
    119
    120    /**
    121     * {@inheritDoc}
    122     * @see org.apache.wiki.auth.user.UserDatabase#findByWikiName(java.lang.String)
    123     */
    124    @Override
    125    public abstract UserProfile findByWikiName( String index ) throws NoSuchPrincipalException;
    126
    127    /**
    128     * <p>Looks up the Principals representing a user from the user database. These
    129     * are defined as a set of WikiPrincipals manufactured from the login name,
    130     * full name, and wiki name. If the user database does not contain a user
    131     * with the supplied identifier, throws a {@link NoSuchPrincipalException}.</p>
    132     * <p>When this method creates WikiPrincipals, the Principal containing
    133     * the user's full name is marked as containing the common name (see
    134     * {@link org.apache.wiki.auth.WikiPrincipal#WikiPrincipal(String, String)}).
    135     * @param identifier the name of the principal to retrieve; this corresponds to
    136     *            value returned by the user profile's
    137     *            {@link UserProfile#getLoginName()}method.
    138     * @return the array of Principals representing the user
    139     * @see org.apache.wiki.auth.user.UserDatabase#getPrincipals(java.lang.String)
    140     * @throws NoSuchPrincipalException If the user database does not contain user with the supplied identifier
    141     */
    142    @Override
    143    public Principal[] getPrincipals( final String identifier ) throws NoSuchPrincipalException {
    144        final UserProfile profile = findByLoginName( identifier );
    145        final ArrayList< Principal > principals = new ArrayList<>();
    146        if( profile.getLoginName() != null && !profile.getLoginName().isEmpty() ) {
    147            principals.add( new WikiPrincipal( profile.getLoginName(), WikiPrincipal.LOGIN_NAME ) );
    148        }
    149        if ("true".equalsIgnoreCase(m_engine.getWikiProperties().getProperty(Engine.PROP_USE_2_X_ACL_LOGIC, "false"))) {
    150            if( profile.getFullname() != null && !profile.getFullname().isEmpty() ) {
    151                principals.add( new WikiPrincipal( profile.getFullname(), WikiPrincipal.FULL_NAME ) );
    152            }
    153        }
    154
    155
    156        if( profile.getWikiName() != null && !profile.getWikiName().isEmpty() ) {
    157            principals.add( new WikiPrincipal( profile.getWikiName(), WikiPrincipal.WIKI_NAME ) );
    158        }
    159        return principals.toArray( new Principal[0] );
    160    }
    161
    162    /**
    163     * {@inheritDoc}
    164     *
    165     * @see org.apache.wiki.auth.user.UserDatabase#initialize(org.apache.wiki.api.core.Engine, java.util.Properties)
    166     */
    167    @Override
    168    public void initialize( Engine engine, Properties props ) throws NoRequiredPropertyException, WikiSecurityException {
    169        this.m_engine = engine;
    170    }
    171
    172    /**
    173     * Factory method that instantiates a new DefaultUserProfile with a new, distinct unique identifier.
    174     *
    175     * @return A new, empty profile.
    176     */
    177    @Override
    178    public UserProfile newProfile() {
    179        final UserProfile profile = new DefaultUserProfile();
    180        profile.setUid( AbstractUserDatabase.generateUid( this ) );
    181        return profile;
    182    }
    183
    184    /**
    185     * {@inheritDoc}
    186     * @see org.apache.wiki.auth.user.UserDatabase#save(org.apache.wiki.auth.user.UserProfile)
    187     */
    188    @Override
    189    public abstract void save( UserProfile profile ) throws WikiSecurityException;
    190
    191    /**
    192     * Validates the password for a given user. If the user does not exist in the user database, this method always returns
    193     * <code>false</code>. If the user exists, the supplied password is compared to the stored password. Note that if the stored password's
    194     * value starts with <code>{SHA}</code>, the supplied password is hashed prior to the comparison.
    195     *
    196     * @param loginName the user's login name
    197     * @param password the user's password (obtained from user input, e.g., a web form)
    198     * @return <code>true</code> if the supplied user password matches the stored password
    199     * @see org.apache.wiki.auth.user.UserDatabase#validatePassword(java.lang.String, java.lang.String)
    200     */
    201    @Override
    202    public boolean validatePassword( final String loginName, final String password ) {
    203        final String hashedPassword;
    204        try {
    205            final UserProfile profile = findByLoginName( loginName );
    206            String storedPassword = profile.getPassword();
    207            boolean verified = false;
    208
    209            // If the password is stored as SHA-256 or SSHA, verify the hash
    210            if( storedPassword.startsWith( SHA256_PREFIX ) || storedPassword.startsWith( SSHA_PREFIX ) ) {
    211                verified = CryptoUtil.verifySaltedPassword( password.getBytes( StandardCharsets.UTF_8 ), storedPassword );
    212            }
    213
    214            // Use older verification algorithm if password is stored as SHA
    215            if( storedPassword.startsWith( SHA_PREFIX ) ) {
    216                storedPassword = storedPassword.substring( SHA_PREFIX.length() );
    217                hashedPassword = getShaHash( password );
    218                verified = hashedPassword.equals( storedPassword );
    219            }
    220
    221            // If in the old format and password verified, upgrade the hash to SSHA
    222            if( verified && !storedPassword.startsWith( SHA256_PREFIX ) ) {
    223                profile.setPassword( password );
    224                save( profile );
    225            }
    226
    227            return verified;
    228        } catch( final NoSuchPrincipalException e ) {
    229            LOG.debug(e.getMessage(), e);
    230        } catch( final NoSuchAlgorithmException e ) {
    231            LOG.error( "Unsupported algorithm: " + e.getMessage() );
    232        } catch( final WikiSecurityException e ) {
    233            LOG.error( "Could not upgrade SHA password to SSHA because profile could not be saved. Reason: " + e.getMessage(), e );
    234        }
    235        return false;
    236    }
    237
    238    @Override
    239    public boolean validatePasswordReuse( final String loginName, final String password ) {
    240        try {
    241            final UserProfile profile = findByLoginName( loginName );
    242
    243            // If the password is stored as SHA-256 or SSHA, verify the hash
    244
    245             for (String storedPassword : profile.getPreviousHashedCredentials()) {
    246                if (storedPassword.startsWith(SHA256_PREFIX) || storedPassword.startsWith(SSHA_PREFIX)) {
    247                    boolean match = CryptoUtil.verifySaltedPassword(password.getBytes(StandardCharsets.UTF_8), storedPassword);
    248                    if (match) {
    249                        return false;
    250                    }
    251                }
    252                if (storedPassword.startsWith(SHA_PREFIX)) {
    253                    String fragment  = storedPassword.substring(SHA_PREFIX.length());
    254                    String hashedPassword = getShaHash(password);
    255                    boolean match  = hashedPassword.equals(fragment);
    256                     if (match) {
    257                        return false;
    258                    }
    259                }
    260            }
    261
    262            return true;
    263        } catch( final NoSuchPrincipalException e ) {
    264            LOG.debug(e.getMessage(), e);
    265        } catch( final NoSuchAlgorithmException e ) {
    266            LOG.error( "Unsupported algorithm: " + e.getMessage() );
    267        } catch( final WikiSecurityException e ) {
    268            LOG.error( "Could not upgrade SHA password to SSHA because profile could not be saved. Reason: " + e.getMessage(), e );
    269        }
    270        return true;
    271    }
    272
    273    /**
    274     * Generates a new random user identifier (uid) that is guaranteed to be unique.
    275     *
    276     * @param db The database for which the UID should be generated.
    277     * @return A random, unique UID.
    278     */
    279    protected static String generateUid( final UserDatabase db ) {
    280        // Keep generating UUIDs until we find one that doesn't collide
    281        String uid;
    282        boolean collision;
    283
    284        do {
    285            uid = UUID.randomUUID().toString();
    286            collision = true;
    287            try {
    288                db.findByUid( uid );
    289            } catch ( final NoSuchPrincipalException e ) {
    290                LOG.debug(e.getMessage(), e);
    291                collision = false;
    292            }
    293        }
    294        while ( collision || uid == null );
    295        return uid;
    296    }
    297
    298    /**
    299     * Private method that calculates the salted SHA-1 or SHA-256 hash of a given <code>String</code>. Note that as of JSPWiki 2.8, this method
    300     * calculates a <em>salted</em> hash rather than a plain hash.
    301     *
    302     * @param text the text to hash
    303     * @return the result hash
    304     */
    305    protected String getHash( final String text ) {
    306        try {
    307            return CryptoUtil.getSaltedPassword( text.getBytes(StandardCharsets.UTF_8), SHA256_PREFIX );
    308        } catch( final NoSuchAlgorithmException e ) {
    309            LOG.error( "Error creating salted password hash: {}", e.getMessage() );
    310            return text;
    311        }
    312    }
    313
    314    /**
    315     * Private method that calculates the SHA-1 hash of a given <code>String</code>
    316     *
    317     * @param text the text to hash
    318     * @return the result hash
    319     * @deprecated this method is retained for backwards compatibility purposes; use {@link #getHash(String)} instead
    320     */
    321    @Deprecated
    322    String getShaHash(final String text ) {
    323        try {
    324            final MessageDigest md = MessageDigest.getInstance( "SHA" );
    325            md.update( text.getBytes( StandardCharsets.UTF_8 ) );
    326            final byte[] digestedBytes = md.digest();
    327            return ByteUtils.bytes2hex( digestedBytes );
    328        } catch( final NoSuchAlgorithmException e ) {
    329            LOG.error( "Error creating SHA password hash:" + e.getMessage() );
    330            return text;
    331        }
    332    }
    333
    334    /**
    335     * Parses a long integer from a supplied string, or returns 0 if not parsable.
    336     *
    337     * @param value the string to parse
    338     * @return the value parsed
    339     */
    340    protected long parseLong( final String value ) {
    341        if( NumberUtils.isParsable( value ) ) {
    342            return Long.parseLong( value );
    343        } else {
    344            return 0L;
    345        }
    346    }
    347
    348}