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     */
019    package org.apache.shiro.authc.credential;
020    
021    import org.apache.shiro.authc.AuthenticationInfo;
022    import org.apache.shiro.authc.AuthenticationToken;
023    import org.apache.shiro.crypto.hash.Hash;
024    
025    /**
026     * A {@link CredentialsMatcher} that employs best-practices comparisons for hashed text passwords.
027     * <p/>
028     * This implementation delegates to an internal {@link PasswordService} to perform the actual password
029     * comparison.  This class is essentially a bridge between the generic CredentialsMatcher interface and the
030     * more specific {@code PasswordService} component.
031     *
032     * @since 1.2
033     */
034    public class PasswordMatcher implements CredentialsMatcher {
035    
036        private PasswordService passwordService;
037    
038        public PasswordMatcher() {
039            this.passwordService = new DefaultPasswordService();
040        }
041    
042        public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
043    
044            PasswordService service = ensurePasswordService();
045    
046            Object submittedPassword = getSubmittedPassword(token);
047            Object storedCredentials = getStoredPassword(info);
048            assertStoredCredentialsType(storedCredentials);
049    
050            if (storedCredentials instanceof Hash) {
051                Hash hashedPassword = (Hash)storedCredentials;
052                HashingPasswordService hashingService = assertHashingPasswordService(service);
053                return hashingService.passwordsMatch(submittedPassword, hashedPassword);
054            }
055            //otherwise they are a String (asserted in the 'assertStoredCredentialsType' method call above):
056            String formatted = (String)storedCredentials;
057            return passwordService.passwordsMatch(submittedPassword, formatted);
058        }
059    
060        private HashingPasswordService assertHashingPasswordService(PasswordService service) {
061            if (service instanceof HashingPasswordService) {
062                return (HashingPasswordService) service;
063            }
064            String msg = "AuthenticationInfo's stored credentials are a Hash instance, but the " +
065                    "configured passwordService is not a " +
066                    HashingPasswordService.class.getName() + " instance.  This is required to perform Hash " +
067                    "object password comparisons.";
068            throw new IllegalStateException(msg);
069        }
070    
071        private PasswordService ensurePasswordService() {
072            PasswordService service = getPasswordService();
073            if (service == null) {
074                String msg = "Required PasswordService has not been configured.";
075                throw new IllegalStateException(msg);
076            }
077            return service;
078        }
079    
080        protected Object getSubmittedPassword(AuthenticationToken token) {
081            return token != null ? token.getCredentials() : null;
082        }
083    
084        private void assertStoredCredentialsType(Object credentials) {
085            if (credentials instanceof String || credentials instanceof Hash) {
086                return;
087            }
088    
089            String msg = "Stored account credentials are expected to be either a " +
090                    Hash.class.getName() + " instance or a formatted hash String.";
091            throw new IllegalArgumentException(msg);
092        }
093    
094        protected Object getStoredPassword(AuthenticationInfo storedAccountInfo) {
095            Object stored = storedAccountInfo != null ? storedAccountInfo.getCredentials() : null;
096            //fix for https://issues.apache.org/jira/browse/SHIRO-363
097            if (stored instanceof char[]) {
098                stored = new String((char[])stored);
099            }
100            return stored;
101        }
102    
103        public PasswordService getPasswordService() {
104            return passwordService;
105        }
106    
107        public void setPasswordService(PasswordService passwordService) {
108            this.passwordService = passwordService;
109        }
110    }