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:
parent
269a72d672
commit
396fec19a8
4 changed files with 140 additions and 3 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue