    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
    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     * @see org.apache.wiki.auth.user.UserDatabase#find(java.lang.String)
    059     */
    060    @Override
    061    public UserProfile find( final String index ) throws NoSuchPrincipalException {
    062        UserProfile profile = null;
    063
    064        // Try finding by full name
    065        try {
    066            profile = findByFullName( index );
    067        } catch( final NoSuchPrincipalException e ) {
    068        }
    069        if( profile != null ) {
    070            return profile;
    071        }
    072
    073        // Try finding by wiki name
    074        try {
    075            profile = findByWikiName( index );
    076        } catch( final NoSuchPrincipalException e ) {
    077        }
    078        if( profile != null ) {
    079            return profile;
    080        }
    081
    082        // Try finding by login name
    083        try {
    084            profile = findByLoginName( index );
    085        } catch( final NoSuchPrincipalException e ) {
    086        }
    087        if( profile != null ) {
    088            return profile;
    089        }
    090
    091        throw new NoSuchPrincipalException( "Not in database: " + index );
    092    }
    093
    094    /**
    095     * {@inheritDoc}
    096     * @see org.apache.wiki.auth.user.UserDatabase#findByEmail(java.lang.String)
    097     */
    098    @Override
    099    public abstract UserProfile findByEmail( String index ) throws NoSuchPrincipalException;
    100
    101    /**
    102     * {@inheritDoc}
    103     * @see org.apache.wiki.auth.user.UserDatabase#findByFullName(java.lang.String)
    104     */
    105    @Override
    106    public abstract UserProfile findByFullName( String index ) throws NoSuchPrincipalException;
    107
    108    /**
    109     * {@inheritDoc}
    110     * @see org.apache.wiki.auth.user.UserDatabase#findByLoginName(java.lang.String)
    111     */
    112    @Override
    113    public abstract UserProfile findByLoginName( String index ) throws NoSuchPrincipalException;
    114
    115    /**
    116     * {@inheritDoc}
    117     * @see org.apache.wiki.auth.user.UserDatabase#findByWikiName(java.lang.String)
    118     */
    119    @Override
    120    public abstract UserProfile findByWikiName( String index ) throws NoSuchPrincipalException;
    121
    122    /**
    123     * <p>Looks up the Principals representing a user from the user database. These
    124     * are defined as a set of WikiPrincipals manufactured from the login name,
    125     * full name, and wiki name. If the user database does not contain a user
    126     * with the supplied identifier, throws a {@link NoSuchPrincipalException}.</p>
    127     * <p>When this method creates WikiPrincipals, the Principal containing
    128     * the user's full name is marked as containing the common name (see
    129     * {@link org.apache.wiki.auth.WikiPrincipal#WikiPrincipal(String, String)}).
    130     * @param identifier the name of the principal to retrieve; this corresponds to
    131     *            value returned by the user profile's
    132     *            {@link UserProfile#getLoginName()}method.
    133     * @return the array of Principals representing the user
    134     * @see org.apache.wiki.auth.user.UserDatabase#getPrincipals(java.lang.String)
    135     * @throws NoSuchPrincipalException If the user database does not contain user with the supplied identifier
    136     */
    137    @Override
    138    public Principal[] getPrincipals( final String identifier ) throws NoSuchPrincipalException {
    139        final UserProfile profile = findByLoginName( identifier );
    140        final ArrayList< Principal > principals = new ArrayList<>();
    141        if( profile.getLoginName() != null && !profile.getLoginName().isEmpty() ) {
    142            principals.add( new WikiPrincipal( profile.getLoginName(), WikiPrincipal.LOGIN_NAME ) );
    143        }
    144        if( profile.getFullname() != null && !profile.getFullname().isEmpty() ) {
    145            principals.add( new WikiPrincipal( profile.getFullname(), WikiPrincipal.FULL_NAME ) );
    146        }
    147        if( profile.getWikiName() != null && !profile.getWikiName().isEmpty() ) {
    148            principals.add( new WikiPrincipal( profile.getWikiName(), WikiPrincipal.WIKI_NAME ) );
    149        }
    150        return principals.toArray( new Principal[0] );
    151    }
    152
    153    /**
    154     * {@inheritDoc}
    155     *
    156     * @see org.apache.wiki.auth.user.UserDatabase#initialize(org.apache.wiki.api.core.Engine, java.util.Properties)
    157     */
    158    @Override
    159    public abstract void initialize( Engine engine, Properties props ) throws NoRequiredPropertyException, WikiSecurityException;
    160
    161    /**
    162     * Factory method that instantiates a new DefaultUserProfile with a new, distinct unique identifier.
    163     *
    164     * @return A new, empty profile.
    165     */
    166    @Override
    167    public UserProfile newProfile() {
    168        final UserProfile profile = new DefaultUserProfile();
    169        profile.setUid( AbstractUserDatabase.generateUid( this ) );
    170        return profile;
    171    }
    172
    173    /**
    174     * {@inheritDoc}
    175     * @see org.apache.wiki.auth.user.UserDatabase#save(org.apache.wiki.auth.user.UserProfile)
    176     */
    177    @Override
    178    public abstract void save( UserProfile profile ) throws WikiSecurityException;
    179
    180    /**
    181     * Validates the password for a given user. If the user does not exist in the user database, this method always returns
    182     * <code>false</code>. If the user exists, the supplied password is compared to the stored password. Note that if the stored password's
    183     * value starts with <code>{SHA}</code>, the supplied password is hashed prior to the comparison.
    184     *
    185     * @param loginName the user's login name
    186     * @param password the user's password (obtained from user input, e.g., a web form)
    187     * @return <code>true</code> if the supplied user password matches the stored password
    188     * @see org.apache.wiki.auth.user.UserDatabase#validatePassword(java.lang.String, java.lang.String)
    189     */
    190    @Override
    191    public boolean validatePassword( final String loginName, final String password ) {
    192        final String hashedPassword;
    193        try {
    194            final UserProfile profile = findByLoginName( loginName );
    195            String storedPassword = profile.getPassword();
    196            boolean verified = false;
    197
    198            // If the password is stored as SHA-256 or SSHA, verify the hash
    199            if( storedPassword.startsWith( SHA256_PREFIX ) || storedPassword.startsWith( SSHA_PREFIX ) ) {
    200                verified = CryptoUtil.verifySaltedPassword( password.getBytes( StandardCharsets.UTF_8 ), storedPassword );
    201            }
    202
    203            // Use older verification algorithm if password is stored as SHA
    204            if( storedPassword.startsWith( SHA_PREFIX ) ) {
    205                storedPassword = storedPassword.substring( SHA_PREFIX.length() );
    206                hashedPassword = getShaHash( password );
    207                verified = hashedPassword.equals( storedPassword );
    208            }
    209
    210            // If in the old format and password verified, upgrade the hash to SSHA
    211            if( verified && !storedPassword.startsWith( SHA256_PREFIX ) ) {
    212                profile.setPassword( password );
    213                save( profile );
    214            }
    215
    216            return verified;
    217        } catch( final NoSuchPrincipalException e ) {
    218        } catch( final NoSuchAlgorithmException e ) {
    219            LOG.error( "Unsupported algorithm: " + e.getMessage() );
    220        } catch( final WikiSecurityException e ) {
    221            LOG.error( "Could not upgrade SHA password to SSHA because profile could not be saved. Reason: " + e.getMessage(), e );
    222        }
    223        return false;
    224    }
    225
    226    /**
    227     * Generates a new random user identifier (uid) that is guaranteed to be unique.
    228     *
    229     * @param db The database for which the UID should be generated.
    230     * @return A random, unique UID.
    231     */
    232    protected static String generateUid( final UserDatabase db ) {
    233        // Keep generating UUIDs until we find one that doesn't collide
    234        String uid;
    235        boolean collision;
    236
    237        do {
    238            uid = UUID.randomUUID().toString();
    239            collision = true;
    240            try {
    241                db.findByUid( uid );
    242            } catch ( final NoSuchPrincipalException e ) {
    243                collision = false;
    244            }
    245        }
    246        while ( collision || uid == null );
    247        return uid;
    248    }
    249
    250    /**
    251     * 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
    252     * calculates a <em>salted</em> hash rather than a plain hash.
    253     *
    254     * @param text the text to hash
    255     * @return the result hash
    256     */
    257    protected String getHash( final String text ) {
    258        try {
    259            return CryptoUtil.getSaltedPassword( text.getBytes(StandardCharsets.UTF_8), SHA256_PREFIX );
    260        } catch( final NoSuchAlgorithmException e ) {
    261            LOG.error( "Error creating salted password hash: {}", e.getMessage() );
    262            return text;
    263        }
    264    }
    265
    266    /**
    267     * Private method that calculates the SHA-1 hash of a given <code>String</code>
    268     *
    269     * @param text the text to hash
    270     * @return the result hash
    271     * @deprecated this method is retained for backwards compatibility purposes; use {@link #getHash(String)} instead
    272     */
    273    @Deprecated
    274    String getShaHash(final String text ) {
    275        try {
    276            final MessageDigest md = MessageDigest.getInstance( "SHA" );
    277            md.update( text.getBytes( StandardCharsets.UTF_8 ) );
    278            final byte[] digestedBytes = md.digest();
    279            return ByteUtils.bytes2hex( digestedBytes );
    280        } catch( final NoSuchAlgorithmException e ) {
    281            LOG.error( "Error creating SHA password hash:" + e.getMessage() );
    282            return text;
    283        }
    284    }
    285
    286    /**
    287     * Parses a long integer from a supplied string, or returns 0 if not parsable.
    288     *
    289     * @param value the string to parse
    290     * @return the value parsed
    291     */
    292    protected long parseLong( final String value ) {
    293        if( NumberUtils.isParsable( value ) ) {
    294            return Long.parseLong( value );
    295        } else {
    296            return 0L;
    297        }
    298    }
    299
    300}