001: /* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
002: *
003: * Licensed under the Apache License, Version 2.0 (the "License");
004: * you may not use this file except in compliance with the License.
005: * You may obtain a copy of the License at
006: *
007: * http://www.apache.org/licenses/LICENSE-2.0
008: *
009: * Unless required by applicable law or agreed to in writing, software
010: * distributed under the License is distributed on an "AS IS" BASIS,
011: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012: * See the License for the specific language governing permissions and
013: * limitations under the License.
014: */
015:
016: package org.acegisecurity.providers.ldap.authenticator;
017:
018: import org.acegisecurity.ldap.LdapDataAccessException;
019:
020: import org.acegisecurity.providers.encoding.PasswordEncoder;
021: import org.acegisecurity.providers.encoding.ShaPasswordEncoder;
022:
023: import org.apache.commons.codec.binary.Base64;
024:
025: import org.springframework.util.Assert;
026:
027: import java.security.MessageDigest;
028:
029: /**
030: * A version of {@link ShaPasswordEncoder} which supports Ldap SHA and SSHA (salted-SHA) encodings. The values are
031: * base-64 encoded and have the label "{SHA}" (or "{SSHA}") prepended to the encoded hash. These can be made lower-case
032: * in the encoded password, if required, by setting the <tt>forceLowerCasePrefix</tt> property to true.
033: *
034: * @author Luke Taylor
035: * @version $Id: LdapShaPasswordEncoder.java 1958 2007-08-27 16:23:14Z luke_t $
036: */
037: public class LdapShaPasswordEncoder implements PasswordEncoder {
038: //~ Static fields/initializers =====================================================================================
039:
040: /** The number of bytes in a SHA hash */
041: private static final int SHA_LENGTH = 20;
042: private static final String SSHA_PREFIX = "{SSHA}";
043: private static final String SSHA_PREFIX_LC = SSHA_PREFIX
044: .toLowerCase();
045: private static final String SHA_PREFIX = "{SHA}";
046: private static final String SHA_PREFIX_LC = SHA_PREFIX
047: .toLowerCase();
048:
049: //~ Instance fields ================================================================================================
050: private boolean forceLowerCasePrefix;
051:
052: //~ Constructors ===================================================================================================
053:
054: public LdapShaPasswordEncoder() {
055: }
056:
057: //~ Methods ========================================================================================================
058:
059: private byte[] combineHashAndSalt(byte[] hash, byte[] salt) {
060: if (salt == null) {
061: return hash;
062: }
063:
064: byte[] hashAndSalt = new byte[hash.length + salt.length];
065: System.arraycopy(hash, 0, hashAndSalt, 0, hash.length);
066: System
067: .arraycopy(salt, 0, hashAndSalt, hash.length,
068: salt.length);
069:
070: return hashAndSalt;
071: }
072:
073: /**
074: * Calculates the hash of password (and salt bytes, if supplied) and returns a base64 encoded concatenation
075: * of the hash and salt, prefixed with {SHA} (or {SSHA} if salt was used).
076: *
077: * @param rawPass the password to be encoded.
078: * @param salt the salt. Must be a byte array or null.
079: *
080: * @return the encoded password in the specified format
081: *
082: */
083: public String encodePassword(String rawPass, Object salt) {
084: MessageDigest sha;
085:
086: try {
087: sha = MessageDigest.getInstance("SHA");
088: } catch (java.security.NoSuchAlgorithmException e) {
089: throw new LdapDataAccessException(
090: "No SHA implementation available!", e);
091: }
092:
093: sha.update(rawPass.getBytes());
094:
095: if (salt != null) {
096: Assert.isInstanceOf(byte[].class, salt,
097: "Salt value must be a byte array");
098: sha.update((byte[]) salt);
099: }
100:
101: byte[] hash = combineHashAndSalt(sha.digest(), (byte[]) salt);
102:
103: String prefix;
104:
105: if (salt == null) {
106: prefix = forceLowerCasePrefix ? SHA_PREFIX_LC : SHA_PREFIX;
107: } else {
108: prefix = forceLowerCasePrefix ? SSHA_PREFIX_LC
109: : SSHA_PREFIX;
110: }
111:
112: return prefix + new String(Base64.encodeBase64(hash));
113: }
114:
115: private byte[] extractSalt(String encPass) {
116: String encPassNoLabel = encPass.substring(6);
117:
118: byte[] hashAndSalt = Base64.decodeBase64(encPassNoLabel
119: .getBytes());
120: int saltLength = hashAndSalt.length - SHA_LENGTH;
121: byte[] salt = new byte[saltLength];
122: System.arraycopy(hashAndSalt, SHA_LENGTH, salt, 0, saltLength);
123:
124: return salt;
125: }
126:
127: /**
128: * Checks the validity of an unencoded password against an encoded one in the form
129: * "{SSHA}sQuQF8vj8Eg2Y1hPdh3bkQhCKQBgjhQI".
130: *
131: * @param encPass the actual SSHA or SHA encoded password
132: * @param rawPass unencoded password to be verified.
133: * @param salt ignored. If the format is SSHA the salt bytes will be extracted from the encoded password.
134: *
135: * @return true if they match (independent of the case of the prefix).
136: */
137: public boolean isPasswordValid(String encPass, String rawPass,
138: Object salt) {
139: String encPassWithoutPrefix;
140:
141: if (encPass.startsWith(SSHA_PREFIX)
142: || encPass.startsWith(SSHA_PREFIX_LC)) {
143: encPassWithoutPrefix = encPass.substring(6);
144: salt = extractSalt(encPass);
145: } else {
146: encPassWithoutPrefix = encPass.substring(5);
147: salt = null;
148: }
149:
150: // Compare the encoded passwords without the prefix
151: return encodePassword(rawPass, salt).endsWith(
152: encPassWithoutPrefix);
153: }
154:
155: public void setForceLowerCasePrefix(boolean forceLowerCasePrefix) {
156: this.forceLowerCasePrefix = forceLowerCasePrefix;
157: }
158: }
|