KEYCLOAK-13983 Include algorithm parameters

Adds the capacity to add both public and secret algorithm
specific data to a PasswordCredentialModel. The default
way to create the models in maintained to minimize the change
impact for the default hash infrastructure.

Publishes the PasswordCredentialModel constructor to
ease in building such extended credential models.
This commit is contained in:
Elisabeth Schulz 2020-06-29 10:28:42 +02:00 committed by Marek Posolda
parent 269a72d672
commit 396fec19a8
4 changed files with 140 additions and 3 deletions

View file

@ -15,7 +15,7 @@ public class PasswordCredentialModel extends CredentialModel {
private final PasswordCredentialData credentialData;
private final PasswordSecretData secretData;
private PasswordCredentialModel(PasswordCredentialData credentialData, PasswordSecretData secretData) {
public PasswordCredentialModel(PasswordCredentialData credentialData, PasswordSecretData secretData) {
this.credentialData = credentialData;
this.secretData = secretData;
}

View file

@ -3,16 +3,38 @@ package org.keycloak.models.credential.dto;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Collections;
import java.util.Map;
public class PasswordCredentialData {
private final int hashIterations;
private final String algorithm;
private final Map<String, String> algorithmData;
/**
* Creator for standard algorithms (no algorithm tuning beyond hash iterations)
* @param hashIterations iterations
* @param algorithm algorithm id
*/
public PasswordCredentialData(int hashIterations, String algorithm) {
this(hashIterations, algorithm, Collections.emptyMap());
}
/**
* Creator for custom algorithms (algorithm with tuning parameters beyond simple has iterations)
* @param hashIterations iterations
* @param algorithm algorithm id
* @param algorithmData additional tuning parameters
*/
@JsonCreator
public PasswordCredentialData(@JsonProperty("hashIterations") int hashIterations, @JsonProperty("algorithm") String algorithm) {
public PasswordCredentialData(@JsonProperty("hashIterations") int hashIterations, @JsonProperty("algorithm") String algorithm, @JsonProperty("algorithmData") Map<String, String> algorithmData) {
this.hashIterations = hashIterations;
this.algorithm = algorithm;
this.algorithmData = algorithmData == null ? Collections.emptyMap() : Collections.unmodifiableMap(algorithmData);
}
public int getHashIterations() {
return hashIterations;
}
@ -20,4 +42,13 @@ public class PasswordCredentialData {
public String getAlgorithm() {
return algorithm;
}
/**
* Returns an immutable map of algorithm-specific settings. These settings may include additional
* parameters such as Bcrypt memory-tuning parameters
* @return algorithm data
*/
public Map<String, String> getAlgorithmData() {
return algorithmData;
}
}

View file

@ -1,6 +1,8 @@
package org.keycloak.models.credential.dto;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
@ -13,9 +15,19 @@ public class PasswordSecretData {
private final String value;
private final byte[] salt;
private final Map<String, String> algorithmData;
/**
* Creator with the option to provide customized secret data (multiple salt values, chiefly)
* @param value hash value
* @param salt salt value
* @param algorithmData additional data required by the algorithm
* @throws IOException invalid base64 in salt value
*/
@JsonCreator
public PasswordSecretData(@JsonProperty("value") String value, @JsonProperty("salt") String salt) throws IOException {
public PasswordSecretData(@JsonProperty("value") String value, @JsonProperty("salt") String salt, @JsonProperty("algorithmData") Map<String, String> algorithmData) throws IOException {
this.algorithmData = algorithmData == null ? Collections.emptyMap() : Collections.unmodifiableMap(algorithmData);
if (salt == null || "__SALT__".equals(salt)) {
this.value = value;
this.salt = null;
@ -26,9 +38,15 @@ public class PasswordSecretData {
}
}
/**
* Default creator (Secret consists only of a value and a single salt)
* @param value hash value
* @param salt salt
*/
public PasswordSecretData(String value, byte[] salt) {
this.value = value;
this.salt = salt;
this.algorithmData = Collections.emptyMap();
}
public String getValue() {
@ -38,4 +56,8 @@ public class PasswordSecretData {
public byte[] getSalt() {
return salt;
}
public Map<String, String> getAlgorithmData() {
return algorithmData;
}
}

View file

@ -0,0 +1,84 @@
package org.keycloak.models.credential;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
import org.keycloak.models.credential.dto.PasswordCredentialData;
import org.keycloak.models.credential.dto.PasswordSecretData;
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Collections;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
public class CredentialModelTest {
private ObjectMapper mapper = new ObjectMapper();
@Test
public void canCreateDefaultCredentialModel() {
PasswordCredentialModel model = PasswordCredentialModel.createFromValues("pbkdf2", new byte[32], 1000, "secretValue");
assertThat(model.getPasswordCredentialData(), notNullValue());
assertThat(model.getPasswordCredentialData().getAlgorithm(), equalTo("pbkdf2"));
assertThat(model.getPasswordCredentialData().getHashIterations(), equalTo(1000));
assertThat(model.getPasswordCredentialData().getAlgorithmData(), equalTo(Collections.emptyMap()));
assertThat(model.getPasswordSecretData(), notNullValue());
assertThat(model.getPasswordSecretData().getAlgorithmData(), equalTo(Collections.emptyMap()));
assertThat(model.getPasswordSecretData().getValue(), equalTo("secretValue"));
assertThat(Arrays.equals(model.getPasswordSecretData().getSalt(), new byte[32]), is(true));
}
@Test
public void canCreatedExtendedCredentialModel() throws IOException {
PasswordCredentialData credentialData = new PasswordCredentialData(1000, "bcrypt", Collections.singletonMap("cost", "18"));
PasswordSecretData secretData = new PasswordSecretData("secretValue", "AAAAAAAAAAAAAAAA", Collections.singletonMap("salt2", "BBBBBBBBBBBBBBBB"));
PasswordCredentialModel model = new PasswordCredentialModel(credentialData, secretData);
assertThat(model.getPasswordCredentialData(), notNullValue());
assertThat(model.getPasswordCredentialData().getAlgorithm(), equalTo("bcrypt"));
assertThat(model.getPasswordCredentialData().getHashIterations(), equalTo(1000));
assertThat(model.getPasswordCredentialData().getAlgorithmData(), equalTo(Collections.singletonMap("cost", "18")));
assertThat(model.getPasswordSecretData(), notNullValue());
assertThat(model.getPasswordSecretData().getAlgorithmData(), equalTo(Collections.singletonMap("salt2", "BBBBBBBBBBBBBBBB")));
assertThat(model.getPasswordSecretData().getValue(), equalTo("secretValue"));
assertThat(Arrays.equals(model.getPasswordSecretData().getSalt(), new byte[12]), is(true));
}
@Test
public void roundtripToJsonDefaultCredentialModel() throws IOException {
PasswordCredentialModel model = PasswordCredentialModel.createFromValues("pbkdf2", new byte[32], 1000, "secretValue");
roundTripAndVerify(model);
}
private void roundTripAndVerify(PasswordCredentialModel model) throws IOException {
PasswordCredentialData pcdOriginal = model.getPasswordCredentialData();
PasswordCredentialData pcdRoundtrip = mapper.readValue(mapper.writeValueAsString(pcdOriginal), PasswordCredentialData.class);
assertThat(pcdRoundtrip.getAlgorithmData(), equalTo(pcdOriginal.getAlgorithmData()));
assertThat(pcdRoundtrip.getAlgorithm(), equalTo(pcdOriginal.getAlgorithm()));
assertThat(pcdRoundtrip.getHashIterations(), equalTo(pcdOriginal.getHashIterations()));
PasswordSecretData psdOriginal = model.getPasswordSecretData();
PasswordSecretData psdRoundtrip = mapper.readValue(mapper.writeValueAsString(psdOriginal), PasswordSecretData.class);
assertThat(psdRoundtrip.getValue(), equalTo(psdOriginal.getValue()));
assertThat(psdRoundtrip.getSalt(), equalTo(psdOriginal.getSalt()));
assertThat(psdRoundtrip.getAlgorithmData(), equalTo(psdRoundtrip.getAlgorithmData()));
}
@Test
public void roudtripToJsonExtendedCredentialModel() throws IOException {
PasswordCredentialData credentialData = new PasswordCredentialData(1000, "bcrypt", Collections.singletonMap("cost", "18"));
PasswordSecretData secretData = new PasswordSecretData("secretValue", "AAAAAAAAAAAAAAAA", Collections.singletonMap("salt2", "BBBBBBBBBBBBBBBB"));
PasswordCredentialModel model = new PasswordCredentialModel(credentialData, secretData);
roundTripAndVerify(model);
}
}