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}