11 changed files with 54 additions and 426 deletions
@ -1,183 +0,0 @@
@@ -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 @@
@@ -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