11 changed files with 54 additions and 426 deletions
@ -1,183 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited |
|
||||||
* |
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|
||||||
* you may not use this file except in compliance with the License. |
|
||||||
* You may obtain a copy of the License at |
|
||||||
* |
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
* |
|
||||||
* Unless required by applicable law or agreed to in writing, software |
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, |
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
||||||
* See the License for the specific language governing permissions and |
|
||||||
* limitations under the License. |
|
||||||
*/ |
|
||||||
|
|
||||||
package org.springframework.security.authentication.encoding; |
|
||||||
|
|
||||||
import java.security.MessageDigest; |
|
||||||
import java.util.Base64; |
|
||||||
|
|
||||||
import org.springframework.security.crypto.codec.Utf8; |
|
||||||
import org.springframework.util.Assert; |
|
||||||
|
|
||||||
/** |
|
||||||
* A version of {@link ShaPasswordEncoder} which supports Ldap SHA and SSHA (salted-SHA) |
|
||||||
* encodings. The values are base-64 encoded and have the label "{SHA}" (or "{SSHA}") |
|
||||||
* prepended to the encoded hash. These can be made lower-case in the encoded password, if |
|
||||||
* required, by setting the <tt>forceLowerCasePrefix</tt> property to true. |
|
||||||
* |
|
||||||
* Also supports plain text passwords, so can safely be used in cases when both encoded |
|
||||||
* and non-encoded passwords are in use or when a null implementation is required. |
|
||||||
* |
|
||||||
* @author Luke Taylor |
|
||||||
*/ |
|
||||||
public class LdapShaPasswordEncoder implements PasswordEncoder { |
|
||||||
// ~ Static fields/initializers
|
|
||||||
// =====================================================================================
|
|
||||||
|
|
||||||
/** The number of bytes in a SHA hash */ |
|
||||||
private static final int SHA_LENGTH = 20; |
|
||||||
private static final String SSHA_PREFIX = "{SSHA}"; |
|
||||||
private static final String SSHA_PREFIX_LC = SSHA_PREFIX.toLowerCase(); |
|
||||||
private static final String SHA_PREFIX = "{SHA}"; |
|
||||||
private static final String SHA_PREFIX_LC = SHA_PREFIX.toLowerCase(); |
|
||||||
|
|
||||||
// ~ Instance fields
|
|
||||||
// ================================================================================================
|
|
||||||
private boolean forceLowerCasePrefix; |
|
||||||
|
|
||||||
// ~ Constructors
|
|
||||||
// ===================================================================================================
|
|
||||||
|
|
||||||
public LdapShaPasswordEncoder() { |
|
||||||
} |
|
||||||
|
|
||||||
// ~ Methods
|
|
||||||
// ========================================================================================================
|
|
||||||
|
|
||||||
private byte[] combineHashAndSalt(byte[] hash, byte[] salt) { |
|
||||||
if (salt == null) { |
|
||||||
return hash; |
|
||||||
} |
|
||||||
|
|
||||||
byte[] hashAndSalt = new byte[hash.length + salt.length]; |
|
||||||
System.arraycopy(hash, 0, hashAndSalt, 0, hash.length); |
|
||||||
System.arraycopy(salt, 0, hashAndSalt, hash.length, salt.length); |
|
||||||
|
|
||||||
return hashAndSalt; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Calculates the hash of password (and salt bytes, if supplied) and returns a base64 |
|
||||||
* encoded concatenation of the hash and salt, prefixed with {SHA} (or {SSHA} if salt |
|
||||||
* was used). |
|
||||||
* |
|
||||||
* @param rawPass the password to be encoded. |
|
||||||
* @param salt the salt. Must be a byte array or null. |
|
||||||
* |
|
||||||
* @return the encoded password in the specified format |
|
||||||
* |
|
||||||
*/ |
|
||||||
public String encodePassword(String rawPass, Object salt) { |
|
||||||
MessageDigest sha; |
|
||||||
|
|
||||||
try { |
|
||||||
sha = MessageDigest.getInstance("SHA"); |
|
||||||
sha.update(Utf8.encode(rawPass)); |
|
||||||
} |
|
||||||
catch (java.security.NoSuchAlgorithmException e) { |
|
||||||
throw new IllegalStateException("No SHA implementation available!"); |
|
||||||
} |
|
||||||
|
|
||||||
if (salt != null) { |
|
||||||
Assert.isInstanceOf(byte[].class, salt, "Salt value must be a byte array"); |
|
||||||
sha.update((byte[]) salt); |
|
||||||
} |
|
||||||
|
|
||||||
byte[] hash = combineHashAndSalt(sha.digest(), (byte[]) salt); |
|
||||||
|
|
||||||
String prefix; |
|
||||||
|
|
||||||
if (salt == null) { |
|
||||||
prefix = forceLowerCasePrefix ? SHA_PREFIX_LC : SHA_PREFIX; |
|
||||||
} |
|
||||||
else { |
|
||||||
prefix = forceLowerCasePrefix ? SSHA_PREFIX_LC : SSHA_PREFIX; |
|
||||||
} |
|
||||||
|
|
||||||
return prefix + Utf8.decode(Base64.getEncoder().encode(hash)); |
|
||||||
} |
|
||||||
|
|
||||||
private byte[] extractSalt(String encPass) { |
|
||||||
String encPassNoLabel = encPass.substring(6); |
|
||||||
|
|
||||||
byte[] hashAndSalt = Base64.getDecoder().decode(encPassNoLabel.getBytes()); |
|
||||||
int saltLength = hashAndSalt.length - SHA_LENGTH; |
|
||||||
byte[] salt = new byte[saltLength]; |
|
||||||
System.arraycopy(hashAndSalt, SHA_LENGTH, salt, 0, saltLength); |
|
||||||
|
|
||||||
return salt; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Checks the validity of an unencoded password against an encoded one in the form |
|
||||||
* "{SSHA}sQuQF8vj8Eg2Y1hPdh3bkQhCKQBgjhQI". |
|
||||||
* |
|
||||||
* @param encPass the actual SSHA or SHA encoded password |
|
||||||
* @param rawPass unencoded password to be verified. |
|
||||||
* @param salt ignored. If the format is SSHA the salt bytes will be extracted from |
|
||||||
* the encoded password. |
|
||||||
* |
|
||||||
* @return true if they match (independent of the case of the prefix). |
|
||||||
*/ |
|
||||||
public boolean isPasswordValid(final String encPass, final String rawPass, Object salt) { |
|
||||||
String prefix = extractPrefix(encPass); |
|
||||||
|
|
||||||
if (prefix == null) { |
|
||||||
return encPass.equals(rawPass); |
|
||||||
} |
|
||||||
|
|
||||||
if (prefix.equals(SSHA_PREFIX) || prefix.equals(SSHA_PREFIX_LC)) { |
|
||||||
salt = extractSalt(encPass); |
|
||||||
} |
|
||||||
else if (!prefix.equals(SHA_PREFIX) && !prefix.equals(SHA_PREFIX_LC)) { |
|
||||||
throw new IllegalArgumentException("Unsupported password prefix '" + prefix |
|
||||||
+ "'"); |
|
||||||
} |
|
||||||
else { |
|
||||||
// Standard SHA
|
|
||||||
salt = null; |
|
||||||
} |
|
||||||
|
|
||||||
int startOfHash = prefix.length(); |
|
||||||
|
|
||||||
String encodedRawPass = encodePassword(rawPass, salt).substring(startOfHash); |
|
||||||
|
|
||||||
return PasswordEncoderUtils |
|
||||||
.equals(encodedRawPass, encPass.substring(startOfHash)); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Returns the hash prefix or null if there isn't one. |
|
||||||
*/ |
|
||||||
private String extractPrefix(String encPass) { |
|
||||||
if (!encPass.startsWith("{")) { |
|
||||||
return null; |
|
||||||
} |
|
||||||
|
|
||||||
int secondBrace = encPass.lastIndexOf('}'); |
|
||||||
|
|
||||||
if (secondBrace < 0) { |
|
||||||
throw new IllegalArgumentException( |
|
||||||
"Couldn't find closing brace for SHA prefix"); |
|
||||||
} |
|
||||||
|
|
||||||
return encPass.substring(0, secondBrace + 1); |
|
||||||
} |
|
||||||
|
|
||||||
public void setForceLowerCasePrefix(boolean forceLowerCasePrefix) { |
|
||||||
this.forceLowerCasePrefix = forceLowerCasePrefix; |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,134 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited |
|
||||||
* |
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|
||||||
* you may not use this file except in compliance with the License. |
|
||||||
* You may obtain a copy of the License at |
|
||||||
* |
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
* |
|
||||||
* Unless required by applicable law or agreed to in writing, software |
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, |
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
||||||
* See the License for the specific language governing permissions and |
|
||||||
* limitations under the License. |
|
||||||
*/ |
|
||||||
|
|
||||||
package org.springframework.security.ldap.authentication; |
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.*; |
|
||||||
|
|
||||||
import org.junit.Before; |
|
||||||
import org.junit.Test; |
|
||||||
import org.springframework.security.authentication.encoding.LdapShaPasswordEncoder; |
|
||||||
|
|
||||||
/** |
|
||||||
* Tests {@link LdapShaPasswordEncoder}. |
|
||||||
* |
|
||||||
* @author Luke Taylor |
|
||||||
*/ |
|
||||||
public class LdapShaPasswordEncoderTests { |
|
||||||
// ~ Instance fields
|
|
||||||
// ================================================================================================
|
|
||||||
|
|
||||||
LdapShaPasswordEncoder sha; |
|
||||||
|
|
||||||
// ~ Methods
|
|
||||||
// ========================================================================================================
|
|
||||||
|
|
||||||
@Before |
|
||||||
public void setUp() throws Exception { |
|
||||||
sha = new LdapShaPasswordEncoder(); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void invalidPasswordFails() { |
|
||||||
assertThat(sha.isPasswordValid("{SHA}ddSFGmjXYPbZC+NXR2kCzBRjqiE=", |
|
||||||
"wrongpassword", null)).isFalse(); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void invalidSaltedPasswordFails() { |
|
||||||
assertThat(sha.isPasswordValid("{SSHA}25ro4PKC8jhQZ26jVsozhX/xaP0suHgX", |
|
||||||
"wrongpassword", null)).isFalse(); |
|
||||||
assertThat(sha.isPasswordValid("{SSHA}PQy2j+6n5ytA+YlAKkM8Fh4p6u2JxfVd", |
|
||||||
"wrongpassword", null)).isFalse(); |
|
||||||
} |
|
||||||
|
|
||||||
@Test(expected = IllegalArgumentException.class) |
|
||||||
public void nonByteArraySaltThrowsException() { |
|
||||||
sha.encodePassword("password", "AStringNotAByteArray"); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Test values generated by 'slappasswd -h {SHA} -s boabspasswurd' |
|
||||||
*/ |
|
||||||
@Test |
|
||||||
public void validPasswordSucceeds() { |
|
||||||
sha.setForceLowerCasePrefix(false); |
|
||||||
assertThat(sha.isPasswordValid("{SHA}ddSFGmjXYPbZC+NXR2kCzBRjqiE=", |
|
||||||
"boabspasswurd", null)).isTrue(); |
|
||||||
assertThat(sha.isPasswordValid("{sha}ddSFGmjXYPbZC+NXR2kCzBRjqiE=", |
|
||||||
"boabspasswurd", null)).isTrue(); |
|
||||||
sha.setForceLowerCasePrefix(true); |
|
||||||
assertThat(sha.isPasswordValid("{SHA}ddSFGmjXYPbZC+NXR2kCzBRjqiE=", |
|
||||||
"boabspasswurd", null)).isTrue(); |
|
||||||
assertThat(sha.isPasswordValid("{sha}ddSFGmjXYPbZC+NXR2kCzBRjqiE=", |
|
||||||
"boabspasswurd", null)).isTrue(); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Test values generated by 'slappasswd -s boabspasswurd' |
|
||||||
*/ |
|
||||||
@Test |
|
||||||
public void validSaltedPasswordSucceeds() { |
|
||||||
sha.setForceLowerCasePrefix(false); |
|
||||||
assertThat(sha.isPasswordValid("{SSHA}25ro4PKC8jhQZ26jVsozhX/xaP0suHgX", |
|
||||||
"boabspasswurd", null)).isTrue(); |
|
||||||
assertThat(sha.isPasswordValid("{ssha}PQy2j+6n5ytA+YlAKkM8Fh4p6u2JxfVd", |
|
||||||
"boabspasswurd", null)).isTrue(); |
|
||||||
sha.setForceLowerCasePrefix(true); |
|
||||||
assertThat(sha.isPasswordValid("{SSHA}25ro4PKC8jhQZ26jVsozhX/xaP0suHgX", |
|
||||||
"boabspasswurd", null)).isTrue(); |
|
||||||
assertThat(sha.isPasswordValid("{ssha}PQy2j+6n5ytA+YlAKkM8Fh4p6u2JxfVd", |
|
||||||
"boabspasswurd", null)).isTrue(); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
// SEC-1031
|
|
||||||
public void fullLengthOfHashIsUsedInComparison() throws Exception { |
|
||||||
// Change the first hash character from '2' to '3'
|
|
||||||
assertThat(sha.isPasswordValid("{SSHA}35ro4PKC8jhQZ26jVsozhX/xaP0suHgX", |
|
||||||
"boabspasswurd", null)).isFalse(); |
|
||||||
// Change the last hash character from 'X' to 'Y'
|
|
||||||
assertThat(sha.isPasswordValid("{SSHA}25ro4PKC8jhQZ26jVsozhX/xaP0suHgY", |
|
||||||
"boabspasswurd", null)).isFalse(); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void correctPrefixCaseIsUsed() { |
|
||||||
sha.setForceLowerCasePrefix(false); |
|
||||||
assertThat("{SHA}ddSFGmjXYPbZC+NXR2kCzBRjqiE=").isEqualTo( |
|
||||||
sha.encodePassword("boabspasswurd", null)); |
|
||||||
assertThat(sha.encodePassword("somepassword", "salt".getBytes()).startsWith( |
|
||||||
"{SSHA}")); |
|
||||||
|
|
||||||
sha.setForceLowerCasePrefix(true); |
|
||||||
assertThat("{sha}ddSFGmjXYPbZC+NXR2kCzBRjqiE=").isEqualTo( |
|
||||||
sha.encodePassword("boabspasswurd", null)); |
|
||||||
assertThat(sha.encodePassword("somepassword", "salt".getBytes()).startsWith( |
|
||||||
"{ssha}")); |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
@Test(expected = IllegalArgumentException.class) |
|
||||||
public void invalidPrefixIsRejected() { |
|
||||||
sha.isPasswordValid("{MD9}xxxxxxxxxx", "somepassword", null); |
|
||||||
} |
|
||||||
|
|
||||||
@Test(expected = IllegalArgumentException.class) |
|
||||||
public void malformedPrefixIsRejected() { |
|
||||||
// No right brace
|
|
||||||
sha.isPasswordValid("{SSHA25ro4PKC8jhQZ26jVsozhX/xaP0suHgX", "somepassword", null); |
|
||||||
} |
|
||||||
} |
|
||||||
Loading…
Reference in new issue