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}