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