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