diff --git a/crypto/crypto.gradle b/crypto/crypto.gradle index 9690c4ff14..8ae1cac76f 100644 --- a/crypto/crypto.gradle +++ b/crypto/crypto.gradle @@ -10,3 +10,8 @@ configure(project.tasks.withType(Test)) { exclude '**/EncryptorsTests.class' } } + +dependencies { + optional 'org.bouncycastle:bcprov-jdk15on:1.54' + testCompile 'org.assertj:assertj-core:3.3.0' +} \ No newline at end of file diff --git a/crypto/pom.xml b/crypto/pom.xml index 5944135dcd..b22fc073c1 100644 --- a/crypto/pom.xml +++ b/crypto/pom.xml @@ -1,105 +1,118 @@ - - - 4.0.0 - org.springframework.security - spring-security-crypto - 4.0.3.CI-SNAPSHOT - spring-security-crypto - spring-security-crypto - http://spring.io/spring-security - - spring.io - http://spring.io/ - - - - The Apache Software License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - repo - - - - - rwinch - Rob Winch - rwinch@gopivotal.com - - - - scm:git:git://github.com/spring-projects/spring-security - scm:git:git://github.com/spring-projects/spring-security - https://github.com/spring-projects/spring-security - - - - spring-snapshot - https://repo.spring.io/snapshot - - - - - commons-logging - commons-logging - 1.2 - compile - true - - - ch.qos.logback - logback-classic - 1.1.2 - test - - - junit - junit - 4.11 - test - - - org.easytesting - fest-assert - 1.4 - test - - - org.mockito - mockito-core - 1.10.19 - test - - - org.slf4j - jcl-over-slf4j - 1.7.7 - test - - - org.springframework - spring-test - test - - - - - - org.springframework - spring-framework-bom - 4.1.6.RELEASE - pom - import - - - - - - - maven-compiler-plugin - - 1.7 - 1.7 - - - - - + + + 4.0.0 + org.springframework.security + spring-security-crypto + 4.1.0.BUILD-SNAPSHOT + spring-security-crypto + spring-security-crypto + http://spring.io/spring-security + + spring.io + http://spring.io/ + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + rwinch + Rob Winch + rwinch@gopivotal.com + + + + scm:git:git://github.com/spring-projects/spring-security + scm:git:git://github.com/spring-projects/spring-security + https://github.com/spring-projects/spring-security + + + + + org.springframework + spring-framework-bom + 4.2.5.RELEASE + pom + import + + + + + + commons-logging + commons-logging + 1.2 + compile + true + + + org.bouncycastle + bcprov-jdk15on + 1.54 + compile + true + + + ch.qos.logback + logback-classic + 1.1.2 + test + + + junit + junit + 4.12 + test + + + org.assertj + assertj-core + 3.3.0 + test + + + org.easytesting + fest-assert + 1.4 + test + + + org.mockito + mockito-core + 1.10.19 + test + + + org.slf4j + jcl-over-slf4j + 1.7.7 + test + + + org.springframework + spring-test + test + + + + + spring-snapshot + https://repo.spring.io/snapshot + + + + + + maven-compiler-plugin + + 1.7 + 1.7 + + + + + diff --git a/crypto/src/main/java/org/springframework/security/crypto/scrypt/SCryptPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/scrypt/SCryptPasswordEncoder.java new file mode 100644 index 0000000000..9c33234ce8 --- /dev/null +++ b/crypto/src/main/java/org/springframework/security/crypto/scrypt/SCryptPasswordEncoder.java @@ -0,0 +1,150 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * 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.crypto.scrypt; + +import java.util.Base64; +import java.util.Base64.Decoder; +import java.util.Base64.Encoder; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.security.crypto.codec.Utf8; +import org.springframework.security.crypto.keygen.BytesKeyGenerator; +import org.springframework.security.crypto.keygen.KeyGenerators; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.bouncycastle.crypto.generators.SCrypt; + + + +/** + * Implementation of PasswordEncoder that uses the SCrypt hashing function. Clients + * can optionally supply a cpu cost parameter, a memory cost parameter and a parallelization parameter. + * + * @author Shazin Sadakath + * + */ +public class SCryptPasswordEncoder implements PasswordEncoder { + + private final Log logger = LogFactory.getLog(getClass()); + + private final int cpuCost; + + private final int memoryCost; + + private final int parallelization; + + private final int keyLength; + + private final BytesKeyGenerator saltGenerator; + + public SCryptPasswordEncoder() { + this(16384, 8, 1, 32, 64); + } + + /** + * @param cpu cost of the algorithm. must be power of 2 greater than 1 + * @param memory cost of the algorithm + * @param parallelization of the algorithm + * @param key length for the algorithm + * @param salt length + */ + public SCryptPasswordEncoder(int cpuCost, int memoryCost, int parallelization, int keyLength, int saltLength) { + if (cpuCost <= 1) { + throw new IllegalArgumentException("Cpu cost parameter must be > 1."); + } + if (memoryCost == 1 && cpuCost > 65536) { + throw new IllegalArgumentException("Cpu cost parameter must be > 1 and < 65536."); + } + if (memoryCost < 1) { + throw new IllegalArgumentException("Memory cost must be >= 1."); + } + int maxParallel = Integer.MAX_VALUE / (128 * memoryCost * 8); + if (parallelization < 1 || parallelization > maxParallel) { + throw new IllegalArgumentException("Parallelisation parameter p must be >= 1 and <= " + maxParallel + + " (based on block size r of " + memoryCost + ")"); + } + if (keyLength < 1 || keyLength > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Key length must be >= 1 and <= "+Integer.MAX_VALUE); + } + if (saltLength < 1 || saltLength > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Salt length must be >= 1 and <= "+Integer.MAX_VALUE); + } + + this.cpuCost = cpuCost; + this.memoryCost = memoryCost; + this.parallelization = parallelization; + this.keyLength = keyLength; + this.saltGenerator = KeyGenerators.secureRandom(saltLength); + } + + @Override + public String encode(CharSequence rawPassword) { + return digest(rawPassword, saltGenerator.generateKey()); + } + + @Override + public boolean matches(CharSequence rawPassword, String encodedPassword) { + if(encodedPassword == null || encodedPassword.length() < keyLength) { + logger.warn("Empty encoded password"); + return false; + } + return decodeAndCheckMatches(rawPassword, encodedPassword); + } + + private boolean decodeAndCheckMatches(CharSequence rawPassword, String encodedPassword) { + String[] parts = encodedPassword.split("\\$"); + + if (parts.length != 4) { + return false; + } + + Decoder decoder = Base64.getDecoder(); + long params = Long.parseLong(parts[1], 16); + byte[] salt = decoder.decode(parts[2]); + byte[] derived = decoder.decode(parts[3]); + + int cpuCost = (int) Math.pow(2, params >> 16 & 0xffff); + int memoryCost = (int) params >> 8 & 0xff; + int parallelization = (int) params & 0xff; + + byte[] generated = SCrypt.generate(Utf8.encode(rawPassword), salt, cpuCost, memoryCost, parallelization, keyLength); + + if (derived.length != generated.length) { + return false; + } + + int result = 0; + for (int i = 0; i < derived.length; i++) { + result |= derived[i] ^ generated[i]; + } + return result == 0; + } + + private String digest(CharSequence rawPassword, byte[] salt) { + byte[] derived = SCrypt.generate(Utf8.encode(rawPassword), salt, cpuCost, memoryCost, parallelization, 32); + + String params = Long.toString(((int) (Math.log(cpuCost) / Math.log(2)) << 16L) | memoryCost << 8 | parallelization, 16); + Encoder encoder = Base64.getEncoder(); + + StringBuilder sb = new StringBuilder((salt.length + derived.length) * 2); + sb.append("$").append(params).append('$'); + sb.append(encoder.encodeToString(salt)).append('$'); + sb.append(encoder.encodeToString(derived)); + + return sb.toString(); + } + +} diff --git a/crypto/src/test/java/org/springframework/security/crypto/scrypt/SCryptPasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/scrypt/SCryptPasswordEncoderTests.java new file mode 100644 index 0000000000..8e24afc455 --- /dev/null +++ b/crypto/src/test/java/org/springframework/security/crypto/scrypt/SCryptPasswordEncoderTests.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * 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.crypto.scrypt; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.Test; + +/** + * @author Shazin Sadakath + * + */ +public class SCryptPasswordEncoderTests { + + @Test + public void matches() { + SCryptPasswordEncoder encoder = new SCryptPasswordEncoder(); + String result = encoder.encode("password"); + assertThat(result).isNotEqualTo("password"); + assertThat(encoder.matches("password", result)).isTrue(); + } + + @Test + public void unicode() { + SCryptPasswordEncoder encoder = new SCryptPasswordEncoder(); + String result = encoder.encode("passw\u9292rd"); + assertThat(encoder.matches("pass\u9292\u9292rd", result)).isFalse(); + assertThat(encoder.matches("passw\u9292rd", result)).isTrue(); + } + + @Test + public void notMatches() { + SCryptPasswordEncoder encoder = new SCryptPasswordEncoder(); + String result = encoder.encode("password"); + assertThat(encoder.matches("bogus", result)).isFalse(); + } + + @Test + public void customParameters() { + SCryptPasswordEncoder encoder = new SCryptPasswordEncoder(512, 8, 4, 32, 16); + String result = encoder.encode("password"); + assertThat(result).isNotEqualTo("password"); + assertThat(encoder.matches("password", result)).isTrue(); + } + + @Test + public void differentPasswordHashes() { + SCryptPasswordEncoder encoder = new SCryptPasswordEncoder(); + String password = "secret"; + assertThat(encoder.encode(password)).isNotEqualTo(encoder.encode(password)); + } + + @Test + public void samePasswordWithDifferentParams() { + SCryptPasswordEncoder oldEncoder = new SCryptPasswordEncoder(512, 8, 4, 64, 16); + SCryptPasswordEncoder newEncoder = new SCryptPasswordEncoder(); + + String password = "secret"; + String oldEncodedPassword = oldEncoder.encode(password); + assertThat(newEncoder.matches(password, oldEncodedPassword)).isTrue(); + } + + @Test + public void doesntMatchNullEncodedValue() { + SCryptPasswordEncoder encoder = new SCryptPasswordEncoder(); + assertThat(encoder.matches("password", null)).isFalse(); + } + + @Test + public void doesntMatchEmptyEncodedValue() { + SCryptPasswordEncoder encoder = new SCryptPasswordEncoder(); + assertThat(encoder.matches("password", "")).isFalse(); + } + + @Test + public void doesntMatchBogusEncodedValue() { + SCryptPasswordEncoder encoder = new SCryptPasswordEncoder(); + assertThat(encoder.matches("password", "012345678901234567890123456789")).isFalse(); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidCpuCostParameter() { + new SCryptPasswordEncoder(Integer.MIN_VALUE, 16, 2, 32, 16); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidMemoryCostParameter() { + new SCryptPasswordEncoder(2, Integer.MAX_VALUE, 2, 32, 16); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidParallelizationParameter() { + new SCryptPasswordEncoder(2, 8, Integer.MAX_VALUE, 32, 16); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidSaltLengthParameter() { + new SCryptPasswordEncoder(2, 8, 1, 16, -1); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidKeyLengthParameter() { + new SCryptPasswordEncoder(2, 8, 1, -1, 16); + } + +} +