KEYCLOAK-13983 Include algorithm parameters

Include suggestions made by @mposolda to enable more generic
usage
This commit is contained in:
Elisabeth Schulz 2020-08-04 10:13:08 +02:00 committed by Marek Posolda
parent 396fec19a8
commit 9143bc748f
5 changed files with 60 additions and 50 deletions

View file

@ -15,11 +15,15 @@ public class PasswordCredentialModel extends CredentialModel {
private final PasswordCredentialData credentialData; private final PasswordCredentialData credentialData;
private final PasswordSecretData secretData; private final PasswordSecretData secretData;
public PasswordCredentialModel(PasswordCredentialData credentialData, PasswordSecretData secretData) { private PasswordCredentialModel(PasswordCredentialData credentialData, PasswordSecretData secretData) {
this.credentialData = credentialData; this.credentialData = credentialData;
this.secretData = secretData; this.secretData = secretData;
} }
public static PasswordCredentialModel createFromValues(PasswordCredentialData credentialData, PasswordSecretData secretData) {
return new PasswordCredentialModel(credentialData, secretData);
}
public static PasswordCredentialModel createFromValues(String algorithm, byte[] salt, int hashIterations, String encodedPassword){ public static PasswordCredentialModel createFromValues(String algorithm, byte[] salt, int hashIterations, String encodedPassword){
PasswordCredentialData credentialData = new PasswordCredentialData(hashIterations, algorithm); PasswordCredentialData credentialData = new PasswordCredentialData(hashIterations, algorithm);
PasswordSecretData secretData = new PasswordSecretData(encodedPassword, salt); PasswordSecretData secretData = new PasswordSecretData(encodedPassword, salt);

View file

@ -2,14 +2,16 @@ package org.keycloak.models.credential.dto;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import org.keycloak.common.util.MultivaluedHashMap;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.Map; import java.util.Map;
public class PasswordCredentialData { public class PasswordCredentialData {
private final int hashIterations; private final int hashIterations;
private final String algorithm; private final String algorithm;
private final Map<String, String> algorithmData; private final MultivaluedHashMap<String, String> additionalParameters;
/** /**
* Creator for standard algorithms (no algorithm tuning beyond hash iterations) * Creator for standard algorithms (no algorithm tuning beyond hash iterations)
@ -24,13 +26,13 @@ public class PasswordCredentialData {
* Creator for custom algorithms (algorithm with tuning parameters beyond simple has iterations) * Creator for custom algorithms (algorithm with tuning parameters beyond simple has iterations)
* @param hashIterations iterations * @param hashIterations iterations
* @param algorithm algorithm id * @param algorithm algorithm id
* @param algorithmData additional tuning parameters * @param additionalParameters additional tuning parameters
*/ */
@JsonCreator @JsonCreator
public PasswordCredentialData(@JsonProperty("hashIterations") int hashIterations, @JsonProperty("algorithm") String algorithm, @JsonProperty("algorithmData") Map<String, String> algorithmData) { public PasswordCredentialData(@JsonProperty("hashIterations") int hashIterations, @JsonProperty("algorithm") String algorithm, @JsonProperty("algorithmData") Map<String, List<String>> additionalParameters) {
this.hashIterations = hashIterations; this.hashIterations = hashIterations;
this.algorithm = algorithm; this.algorithm = algorithm;
this.algorithmData = algorithmData == null ? Collections.emptyMap() : Collections.unmodifiableMap(algorithmData); this.additionalParameters = new MultivaluedHashMap<>(additionalParameters == null ? Collections.emptyMap() : additionalParameters);
} }
@ -44,11 +46,11 @@ public class PasswordCredentialData {
} }
/** /**
* Returns an immutable map of algorithm-specific settings. These settings may include additional * Returns a map of algorithm-specific settings. These settings may include additional
* parameters such as Bcrypt memory-tuning parameters * parameters such as Bcrypt memory-tuning parameters. It should be used immutably.
* @return algorithm data * @return algorithm data
*/ */
public Map<String, String> getAlgorithmData() { public MultivaluedHashMap<String, String> getAdditionalParameters() {
return algorithmData; return additionalParameters;
} }
} }

View file

@ -2,12 +2,14 @@ package org.keycloak.models.credential.dto;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.Map; import java.util.Map;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.util.Base64; import org.keycloak.common.util.Base64;
import org.keycloak.common.util.MultivaluedHashMap;
public class PasswordSecretData { public class PasswordSecretData {
@ -15,18 +17,18 @@ public class PasswordSecretData {
private final String value; private final String value;
private final byte[] salt; private final byte[] salt;
private final Map<String, String> algorithmData; private final MultivaluedHashMap<String, String> additionalParameters;
/** /**
* Creator with the option to provide customized secret data (multiple salt values, chiefly) * Creator with the option to provide customized secret data (multiple salt values, chiefly)
* @param value hash value * @param value hash value
* @param salt salt value * @param salt salt value
* @param algorithmData additional data required by the algorithm * @param additionalParameters additional data required by the algorithm
* @throws IOException invalid base64 in salt value * @throws IOException invalid base64 in salt value
*/ */
@JsonCreator @JsonCreator
public PasswordSecretData(@JsonProperty("value") String value, @JsonProperty("salt") String salt, @JsonProperty("algorithmData") Map<String, String> algorithmData) throws IOException { public PasswordSecretData(@JsonProperty("value") String value, @JsonProperty("salt") String salt, @JsonProperty("algorithmData") Map<String, List<String>> additionalParameters) throws IOException {
this.algorithmData = algorithmData == null ? Collections.emptyMap() : Collections.unmodifiableMap(algorithmData); this.additionalParameters = new MultivaluedHashMap<>(additionalParameters == null ? Collections.emptyMap() : additionalParameters);
if (salt == null || "__SALT__".equals(salt)) { if (salt == null || "__SALT__".equals(salt)) {
this.value = value; this.value = value;
@ -46,7 +48,7 @@ public class PasswordSecretData {
public PasswordSecretData(String value, byte[] salt) { public PasswordSecretData(String value, byte[] salt) {
this.value = value; this.value = value;
this.salt = salt; this.salt = salt;
this.algorithmData = Collections.emptyMap(); this.additionalParameters = new MultivaluedHashMap<>();
} }
public String getValue() { public String getValue() {
@ -57,7 +59,7 @@ public class PasswordSecretData {
return salt; return salt;
} }
public Map<String, String> getAlgorithmData() { public MultivaluedHashMap<String, String> getAdditionalParameters() {
return algorithmData; return additionalParameters;
} }
} }

View file

@ -2,12 +2,13 @@ package org.keycloak.models.credential;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test; import org.junit.Test;
import org.keycloak.credential.CredentialModel;
import org.keycloak.models.credential.dto.PasswordCredentialData; import org.keycloak.models.credential.dto.PasswordCredentialData;
import org.keycloak.models.credential.dto.PasswordSecretData; import org.keycloak.models.credential.dto.PasswordSecretData;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Array;
import java.util.Arrays; import java.util.Arrays;
import java.util.Base64;
import java.util.Collections; import java.util.Collections;
import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.CoreMatchers.*;
@ -17,6 +18,28 @@ public class CredentialModelTest {
private ObjectMapper mapper = new ObjectMapper(); private ObjectMapper mapper = new ObjectMapper();
@Test
public void canDeserializeMinimalJson() {
CredentialModel model = new CredentialModel();
model.setCredentialData("{\"hashIterations\": 10000, \"algorithm\": \"custom\"}");
model.setSecretData("{\"value\": \"the value\", \"salt\": \"saltValu\"}");
PasswordCredentialModel decoded = PasswordCredentialModel.createFromCredentialModel(model);
assertThat(decoded, notNullValue());
assertThat(decoded.getPasswordCredentialData(), notNullValue());
assertThat(decoded.getPasswordCredentialData().getAlgorithm(), equalTo("custom"));
assertThat(decoded.getPasswordCredentialData().getHashIterations(), equalTo(10000));
assertThat(decoded.getPasswordCredentialData().getAdditionalParameters(), equalTo(Collections.emptyMap()));
assertThat(decoded.getPasswordSecretData(), notNullValue());
assertThat(decoded.getPasswordSecretData().getValue(), equalTo("the value"));
assertThat(decoded.getPasswordSecretData().getSalt(), notNullValue());
String base64Salt = Base64.getEncoder().encodeToString(decoded.getPasswordSecretData().getSalt());
assertThat(base64Salt, equalTo("saltValu"));
assertThat(decoded.getPasswordSecretData().getAdditionalParameters(), equalTo(Collections.emptyMap()));
}
@Test @Test
public void canCreateDefaultCredentialModel() { public void canCreateDefaultCredentialModel() {
PasswordCredentialModel model = PasswordCredentialModel.createFromValues("pbkdf2", new byte[32], 1000, "secretValue"); PasswordCredentialModel model = PasswordCredentialModel.createFromValues("pbkdf2", new byte[32], 1000, "secretValue");
@ -24,27 +47,27 @@ public class CredentialModelTest {
assertThat(model.getPasswordCredentialData(), notNullValue()); assertThat(model.getPasswordCredentialData(), notNullValue());
assertThat(model.getPasswordCredentialData().getAlgorithm(), equalTo("pbkdf2")); assertThat(model.getPasswordCredentialData().getAlgorithm(), equalTo("pbkdf2"));
assertThat(model.getPasswordCredentialData().getHashIterations(), equalTo(1000)); assertThat(model.getPasswordCredentialData().getHashIterations(), equalTo(1000));
assertThat(model.getPasswordCredentialData().getAlgorithmData(), equalTo(Collections.emptyMap())); assertThat(model.getPasswordCredentialData().getAdditionalParameters(), equalTo(Collections.emptyMap()));
assertThat(model.getPasswordSecretData(), notNullValue()); assertThat(model.getPasswordSecretData(), notNullValue());
assertThat(model.getPasswordSecretData().getAlgorithmData(), equalTo(Collections.emptyMap())); assertThat(model.getPasswordSecretData().getAdditionalParameters(), equalTo(Collections.emptyMap()));
assertThat(model.getPasswordSecretData().getValue(), equalTo("secretValue")); assertThat(model.getPasswordSecretData().getValue(), equalTo("secretValue"));
assertThat(Arrays.equals(model.getPasswordSecretData().getSalt(), new byte[32]), is(true)); assertThat(Arrays.equals(model.getPasswordSecretData().getSalt(), new byte[32]), is(true));
} }
@Test @Test
public void canCreatedExtendedCredentialModel() throws IOException { public void canCreatedExtendedCredentialModel() throws IOException {
PasswordCredentialData credentialData = new PasswordCredentialData(1000, "bcrypt", Collections.singletonMap("cost", "18")); PasswordCredentialData credentialData = new PasswordCredentialData(1000, "bcrypt", Collections.singletonMap("cost", Collections.singletonList("18")));
PasswordSecretData secretData = new PasswordSecretData("secretValue", "AAAAAAAAAAAAAAAA", Collections.singletonMap("salt2", "BBBBBBBBBBBBBBBB")); PasswordSecretData secretData = new PasswordSecretData("secretValue", "AAAAAAAAAAAAAAAA", Collections.singletonMap("salt2", Collections.singletonList("BBBBBBBBBBBBBBBB")));
PasswordCredentialModel model = new PasswordCredentialModel(credentialData, secretData); PasswordCredentialModel model = PasswordCredentialModel.createFromValues(credentialData, secretData);
assertThat(model.getPasswordCredentialData(), notNullValue()); assertThat(model.getPasswordCredentialData(), notNullValue());
assertThat(model.getPasswordCredentialData().getAlgorithm(), equalTo("bcrypt")); assertThat(model.getPasswordCredentialData().getAlgorithm(), equalTo("bcrypt"));
assertThat(model.getPasswordCredentialData().getHashIterations(), equalTo(1000)); assertThat(model.getPasswordCredentialData().getHashIterations(), equalTo(1000));
assertThat(model.getPasswordCredentialData().getAlgorithmData(), equalTo(Collections.singletonMap("cost", "18"))); assertThat(model.getPasswordCredentialData().getAdditionalParameters(), equalTo(Collections.singletonMap("cost", Collections.singletonList("18"))));
assertThat(model.getPasswordSecretData(), notNullValue()); assertThat(model.getPasswordSecretData(), notNullValue());
assertThat(model.getPasswordSecretData().getAlgorithmData(), equalTo(Collections.singletonMap("salt2", "BBBBBBBBBBBBBBBB"))); assertThat(model.getPasswordSecretData().getAdditionalParameters(), equalTo(Collections.singletonMap("salt2", Collections.singletonList("BBBBBBBBBBBBBBBB"))));
assertThat(model.getPasswordSecretData().getValue(), equalTo("secretValue")); assertThat(model.getPasswordSecretData().getValue(), equalTo("secretValue"));
assertThat(Arrays.equals(model.getPasswordSecretData().getSalt(), new byte[12]), is(true)); assertThat(Arrays.equals(model.getPasswordSecretData().getSalt(), new byte[12]), is(true));
} }
@ -60,7 +83,7 @@ public class CredentialModelTest {
PasswordCredentialData pcdOriginal = model.getPasswordCredentialData(); PasswordCredentialData pcdOriginal = model.getPasswordCredentialData();
PasswordCredentialData pcdRoundtrip = mapper.readValue(mapper.writeValueAsString(pcdOriginal), PasswordCredentialData.class); PasswordCredentialData pcdRoundtrip = mapper.readValue(mapper.writeValueAsString(pcdOriginal), PasswordCredentialData.class);
assertThat(pcdRoundtrip.getAlgorithmData(), equalTo(pcdOriginal.getAlgorithmData())); assertThat(pcdRoundtrip.getAdditionalParameters(), equalTo(pcdOriginal.getAdditionalParameters()));
assertThat(pcdRoundtrip.getAlgorithm(), equalTo(pcdOriginal.getAlgorithm())); assertThat(pcdRoundtrip.getAlgorithm(), equalTo(pcdOriginal.getAlgorithm()));
assertThat(pcdRoundtrip.getHashIterations(), equalTo(pcdOriginal.getHashIterations())); assertThat(pcdRoundtrip.getHashIterations(), equalTo(pcdOriginal.getHashIterations()));
@ -69,14 +92,14 @@ public class CredentialModelTest {
assertThat(psdRoundtrip.getValue(), equalTo(psdOriginal.getValue())); assertThat(psdRoundtrip.getValue(), equalTo(psdOriginal.getValue()));
assertThat(psdRoundtrip.getSalt(), equalTo(psdOriginal.getSalt())); assertThat(psdRoundtrip.getSalt(), equalTo(psdOriginal.getSalt()));
assertThat(psdRoundtrip.getAlgorithmData(), equalTo(psdRoundtrip.getAlgorithmData())); assertThat(psdRoundtrip.getAdditionalParameters(), equalTo(psdRoundtrip.getAdditionalParameters()));
} }
@Test @Test
public void roudtripToJsonExtendedCredentialModel() throws IOException { public void roudtripToJsonExtendedCredentialModel() throws IOException {
PasswordCredentialData credentialData = new PasswordCredentialData(1000, "bcrypt", Collections.singletonMap("cost", "18")); PasswordCredentialData credentialData = new PasswordCredentialData(1000, "bcrypt", Collections.singletonMap("cost", Collections.singletonList("18")));
PasswordSecretData secretData = new PasswordSecretData("secretValue", "AAAAAAAAAAAAAAAA", Collections.singletonMap("salt2", "BBBBBBBBBBBBBBBB")); PasswordSecretData secretData = new PasswordSecretData("secretValue", "AAAAAAAAAAAAAAAA", Collections.singletonMap("salt2", Collections.singletonList("BBBBBBBBBBBBBBBB")));
PasswordCredentialModel model = new PasswordCredentialModel(credentialData, secretData); PasswordCredentialModel model = PasswordCredentialModel.createFromValues(credentialData, secretData);
roundTripAndVerify(model); roundTripAndVerify(model);
} }

View file

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="20px" height="20px" viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<style type="text/css">
.st0{clip-path:url(#SVGID_2_);fill:#F05133;}
</style>
<g>
<defs>
<rect id="SVGID_1_" x="0" y="0" width="20" height="20"/>
</defs>
<clipPath id="SVGID_2_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<path class="st0" d="M19.6,9.1l-8.7-8.7c-0.5-0.5-1.3-0.5-1.8,0L7.3,2.2l2.3,2.3c0.5-0.2,1.1-0.1,1.6,0.4c0.4,0.4,0.5,1,0.4,1.6
l2.2,2.2c0.5-0.2,1.2-0.1,1.6,0.4c0.6,0.6,0.6,1.6,0,2.2c-0.6,0.6-1.6,0.6-2.2,0c-0.5-0.5-0.6-1.1-0.3-1.7l-2.1-2.1v5.4
c0.1,0.1,0.3,0.2,0.4,0.3c0.6,0.6,0.6,1.6,0,2.2C10.5,16,9.6,16,9,15.4c-0.6-0.6-0.6-1.6,0-2.2c0.1-0.1,0.3-0.3,0.5-0.3V7.4
C9.3,7.3,9.1,7.2,9,7C8.5,6.6,8.4,5.9,8.6,5.3L6.4,3.1l-6,6c-0.5,0.5-0.5,1.3,0,1.8l8.7,8.7c0.5,0.5,1.3,0.5,1.8,0l8.7-8.7
C20.1,10.4,20.1,9.6,19.6,9.1"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB