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);
+ }
+
+}
+