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