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