View Javadoc

1   /* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
2    *
3    * Licensed under the Apache License, Version 2.0 (the "License");
4    * you may not use this file except in compliance with the License.
5    * You may obtain a copy of the License at
6    *
7    *     http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  
16  package org.springframework.security.providers.ldap.authenticator;
17  
18  import org.springframework.security.providers.encoding.PasswordEncoder;
19  import org.springframework.security.providers.encoding.ShaPasswordEncoder;
20  
21  import org.apache.commons.codec.binary.Base64;
22  
23  import org.springframework.util.Assert;
24  
25  import java.io.UnsupportedEncodingException;
26  import java.security.MessageDigest;
27  
28  
29  /**
30   * A version of {@link ShaPasswordEncoder} which supports Ldap SHA and SSHA (salted-SHA) encodings. The values are
31   * base-64 encoded and have the label "{SHA}" (or "{SSHA}") prepended to the encoded hash. These can be made lower-case
32   * in the encoded password, if required, by setting the <tt>forceLowerCasePrefix</tt> property to true.
33   *
34   * Also supports plain text passwords, so can safely be used in cases when both encoded and non-encoded passwords are in
35   * use or when a null implementation is required.
36   *
37   * @author Luke Taylor
38   * @version $Id: LdapShaPasswordEncoder.java 2842 2008-04-01 14:32:31Z luke_t $
39   */
40  public class LdapShaPasswordEncoder implements PasswordEncoder {
41      //~ Static fields/initializers =====================================================================================
42  
43      /** The number of bytes in a SHA hash */
44      private static final int SHA_LENGTH = 20;
45      private static final String SSHA_PREFIX = "{SSHA}";
46      private static final String SSHA_PREFIX_LC = SSHA_PREFIX.toLowerCase();
47      private static final String SHA_PREFIX = "{SHA}";
48      private static final String SHA_PREFIX_LC = SHA_PREFIX.toLowerCase();
49  
50      //~ Instance fields ================================================================================================
51      private boolean forceLowerCasePrefix;
52  
53      //~ Constructors ===================================================================================================
54  
55      public LdapShaPasswordEncoder() {}
56  
57      //~ Methods ========================================================================================================
58  
59      private byte[] combineHashAndSalt(byte[] hash, byte[] salt) {
60          if (salt == null) {
61              return hash;
62          }
63  
64          byte[] hashAndSalt = new byte[hash.length + salt.length];
65          System.arraycopy(hash, 0, hashAndSalt, 0, hash.length);
66          System.arraycopy(salt, 0, hashAndSalt, hash.length, salt.length);
67  
68          return hashAndSalt;
69      }
70  
71      /**
72       * Calculates the hash of password (and salt bytes, if supplied) and returns a base64 encoded concatenation
73       * of the hash and salt, prefixed with {SHA} (or {SSHA} if salt was used).
74       *
75       * @param rawPass the password to be encoded.
76       * @param salt the salt. Must be a byte array or null.
77       *
78       * @return the encoded password in the specified format
79       *
80       */
81      public String encodePassword(String rawPass, Object salt) {
82          MessageDigest sha;
83  
84          try {
85              sha = MessageDigest.getInstance("SHA");
86              sha.update(rawPass.getBytes("UTF-8"));
87          } catch (java.security.NoSuchAlgorithmException e) {
88              throw new IllegalStateException("No SHA implementation available!");
89          } catch (UnsupportedEncodingException ue) {
90              throw new IllegalStateException("UTF-8 not supported!");
91          }
92  
93          if (salt != null) {
94              Assert.isInstanceOf(byte[].class, salt, "Salt value must be a byte array");
95              sha.update((byte[]) salt);
96          }
97  
98          byte[] hash = combineHashAndSalt(sha.digest(), (byte[]) salt);
99  
100         String prefix;
101 
102         if (salt == null) {
103             prefix = forceLowerCasePrefix ? SHA_PREFIX_LC : SHA_PREFIX;
104         } else {
105             prefix = forceLowerCasePrefix ? SSHA_PREFIX_LC : SSHA_PREFIX;
106         }
107 
108         return prefix + new String(Base64.encodeBase64(hash));
109     }
110 
111     private byte[] extractSalt(String encPass) {
112         String encPassNoLabel = encPass.substring(6);
113 
114         byte[] hashAndSalt = Base64.decodeBase64(encPassNoLabel.getBytes());
115         int saltLength = hashAndSalt.length - SHA_LENGTH;
116         byte[] salt = new byte[saltLength];
117         System.arraycopy(hashAndSalt, SHA_LENGTH, salt, 0, saltLength);
118 
119         return salt;
120     }
121 
122     /**
123      * Checks the validity of an unencoded password against an encoded one in the form
124      * "{SSHA}sQuQF8vj8Eg2Y1hPdh3bkQhCKQBgjhQI".
125      *
126      * @param encPass the actual SSHA or SHA encoded password
127      * @param rawPass unencoded password to be verified.
128      * @param salt ignored. If the format is SSHA the salt bytes will be extracted from the encoded password.
129      *
130      * @return true if they match (independent of the case of the prefix).
131      */
132     public boolean isPasswordValid(final String encPass, final String rawPass, Object salt) {
133         String prefix = extractPrefix(encPass);
134         
135         if (prefix == null) {
136             return encPass.equals(rawPass);
137         }
138 
139         if (prefix.equals(SSHA_PREFIX) || prefix.equals(SSHA_PREFIX_LC)) {
140             salt = extractSalt(encPass);
141         } else if (!prefix.equals(SHA_PREFIX) && !prefix.equals(SHA_PREFIX_LC)) {
142             throw new IllegalArgumentException("Unsupported password prefix '" + prefix + "'");
143         } else {
144             // Standard SHA
145             salt = null;
146         }
147 
148         int startOfHash = prefix.length() + 1;
149         
150         String encodedRawPass = encodePassword(rawPass, salt).substring(startOfHash);
151         
152         return encodedRawPass.equals(encPass.substring(startOfHash));
153     }
154     
155     /**
156      * Returns the hash prefix or null if there isn't one. 
157      */
158     private String extractPrefix(String encPass) {
159         if (!encPass.startsWith("{")) {
160             return null;
161         }
162 
163         int secondBrace = encPass.lastIndexOf('}');
164         
165         if (secondBrace < 0) {
166             throw new IllegalArgumentException("Couldn't find closing brace for SHA prefix");
167         }
168         
169         return encPass.substring(0, secondBrace + 1);
170     }
171 
172     public void setForceLowerCasePrefix(boolean forceLowerCasePrefix) {
173         this.forceLowerCasePrefix = forceLowerCasePrefix;
174     }
175 }