mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-06-24 04:52:16 +00:00
Add SCryptPasswordEncoder
Fixes gh-3702
This commit is contained in:
parent
e208bdb915
commit
7d02e259df
@ -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'
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-crypto</artifactId>
|
||||
<version>4.0.3.CI-SNAPSHOT</version>
|
||||
<version>4.1.0.BUILD-SNAPSHOT</version>
|
||||
<name>spring-security-crypto</name>
|
||||
<description>spring-security-crypto</description>
|
||||
<url>http://spring.io/spring-security</url>
|
||||
@ -30,12 +30,17 @@
|
||||
<developerConnection>scm:git:git://github.com/spring-projects/spring-security</developerConnection>
|
||||
<url>https://github.com/spring-projects/spring-security</url>
|
||||
</scm>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-snapshot</id>
|
||||
<url>https://repo.spring.io/snapshot</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-framework-bom</artifactId>
|
||||
<version>4.2.5.RELEASE</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>commons-logging</groupId>
|
||||
@ -44,6 +49,13 @@
|
||||
<scope>compile</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
<version>1.54</version>
|
||||
<scope>compile</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
@ -53,7 +65,13 @@
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.11</version>
|
||||
<version>4.12</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@ -80,17 +98,12 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-framework-bom</artifactId>
|
||||
<version>4.1.6.RELEASE</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-snapshot</id>
|
||||
<url>https://repo.spring.io/snapshot</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user