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}