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}