[java-opensaml] 11/15: Finish out ConcatKDF and PBKDF2 implementations.
Brent Putman
putmanb at georgetown.edu
Thu Jan 21 21:33:31 UTC 2021
This is an automated email from the git hooks/post-receive script.
putmanb pushed a commit to branch dev/OSJ-82
in repository java-opensaml.
View the commit online:
http://git.shibboleth.net/view/?p=java-opensaml.git;a=commit;h=938b4e25272cddbafa003a693aef0397c8a8104b
commit 938b4e25272cddbafa003a693aef0397c8a8104b
Author: Brent Putman <putmanb at georgetown.edu>
AuthorDate: Tue Dec 29 19:08:03 2020 -0500
Finish out ConcatKDF and PBKDF2 implementations.
---
.../opensaml/xmlsec/derivation/KeyDerivation.java | 4 +-
.../org/opensaml/xmlsec/encryption/KeyLength.java | 6 +-
.../opensaml/xmlsec/derivation/impl/ConcatKDF.java | 410 ++++++++++++-
.../opensaml/xmlsec/derivation/impl/PBKDF2.java | 345 ++++++++++-
.../xmlsec/derivation/impl/ConcatKDFTest.java | 498 ++++++++++++++++
.../xmlsec/derivation/impl/MockKeyDerivation.java | 10 -
.../xmlsec/derivation/impl/PBKDF2Test.java | 639 +++++++++++++++++++++
7 files changed, 1888 insertions(+), 24 deletions(-)
diff --git a/opensaml-xmlsec-api/src/main/java/org/opensaml/xmlsec/derivation/KeyDerivation.java b/opensaml-xmlsec-api/src/main/java/org/opensaml/xmlsec/derivation/KeyDerivation.java
index fa15b97f5..411e2abda 100644
--- a/opensaml-xmlsec-api/src/main/java/org/opensaml/xmlsec/derivation/KeyDerivation.java
+++ b/opensaml-xmlsec-api/src/main/java/org/opensaml/xmlsec/derivation/KeyDerivation.java
@@ -19,7 +19,7 @@ package org.opensaml.xmlsec.derivation;
import javax.crypto.SecretKey;
-import org.opensaml.xmlsec.agreement.XMLExpressableKeyAgreementParameter;
+import org.opensaml.xmlsec.agreement.KeyAgreementParameter;
/**
* Component which represents a specific key derivation algorithm, and supports deriving a new {@link SecretKey}
@@ -29,7 +29,7 @@ import org.opensaml.xmlsec.agreement.XMLExpressableKeyAgreementParameter;
* Sub-types will usually contain additional configurable property inputs to the derivation operation.
* </p>
*/
-public interface KeyDerivation extends XMLExpressableKeyAgreementParameter {
+public interface KeyDerivation extends KeyAgreementParameter {
/**
* The key derivation algorithm URI.
diff --git a/opensaml-xmlsec-api/src/main/java/org/opensaml/xmlsec/encryption/KeyLength.java b/opensaml-xmlsec-api/src/main/java/org/opensaml/xmlsec/encryption/KeyLength.java
index 65659dec1..84c20321b 100644
--- a/opensaml-xmlsec-api/src/main/java/org/opensaml/xmlsec/encryption/KeyLength.java
+++ b/opensaml-xmlsec-api/src/main/java/org/opensaml/xmlsec/encryption/KeyLength.java
@@ -23,7 +23,11 @@ import org.opensaml.core.xml.schema.XSInteger;
import org.opensaml.xmlsec.encryption.support.EncryptionConstants;
/**
- * XMLObject representing XML Encryption 1.1 IterationCount KeyLength element.
+ * XMLObject representing XML Encryption 1.1 KeyLength element.
+ *
+ * <p>
+ * Note: Value is in number of <b>bytes</b>.
+ * </p>
*/
public interface KeyLength extends XSInteger {
diff --git a/opensaml-xmlsec-impl/src/main/java/org/opensaml/xmlsec/derivation/impl/ConcatKDF.java b/opensaml-xmlsec-impl/src/main/java/org/opensaml/xmlsec/derivation/impl/ConcatKDF.java
index bac1f2660..cf006c323 100644
--- a/opensaml-xmlsec-impl/src/main/java/org/opensaml/xmlsec/derivation/impl/ConcatKDF.java
+++ b/opensaml-xmlsec-impl/src/main/java/org/opensaml/xmlsec/derivation/impl/ConcatKDF.java
@@ -18,36 +18,336 @@
package org.opensaml.xmlsec.derivation.impl;
import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import org.apache.commons.codec.DecoderException;
+import org.apache.commons.codec.binary.Hex;
+import org.bouncycastle.crypto.Digest;
+import org.bouncycastle.crypto.agreement.kdf.ConcatenationKDFGenerator;
+import org.bouncycastle.crypto.digests.RIPEMD160Digest;
+import org.bouncycastle.crypto.digests.SHA1Digest;
+import org.bouncycastle.crypto.digests.SHA224Digest;
+import org.bouncycastle.crypto.digests.SHA256Digest;
+import org.bouncycastle.crypto.digests.SHA384Digest;
+import org.bouncycastle.crypto.digests.SHA512Digest;
+import org.bouncycastle.crypto.params.KDFParameters;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.util.XMLObjectSupport;
import org.opensaml.xmlsec.agreement.CloneableKeyAgreementParameter;
+import org.opensaml.xmlsec.agreement.XMLExpressableKeyAgreementParameter;
+import org.opensaml.xmlsec.algorithm.AlgorithmDescriptor;
+import org.opensaml.xmlsec.algorithm.AlgorithmSupport;
+import org.opensaml.xmlsec.algorithm.DigestAlgorithm;
import org.opensaml.xmlsec.derivation.KeyDerivation;
import org.opensaml.xmlsec.derivation.KeyDerivationException;
import org.opensaml.xmlsec.encryption.ConcatKDFParams;
import org.opensaml.xmlsec.encryption.KeyDerivationMethod;
import org.opensaml.xmlsec.encryption.support.EncryptionConstants;
+import org.opensaml.xmlsec.signature.DigestMethod;
+import org.opensaml.xmlsec.signature.support.SignatureConstants;
+import com.google.common.primitives.Bytes;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
import net.shibboleth.utilities.java.support.component.AbstractInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
/**
* Implementation of ConcatKDF key derivation as defined in XML Encryption 1.1.
+ *
+ * <p>
+ * The following rules apply to the concatenation parameters:
+ * </p>
+ *
+ * <ul>
+ * <li>AlgorithmID</li>
+ * <li>PartyUInfo</li>
+ * <li>PartyVInfo</li>
+ * <li>SuppPubInfo</li>
+ * <li>SuppPrivInfo</li>
+ * </ul>
+ *
+ * <p>
+ * Configured parameter string values must conform to the XML <code>hexBinary</code> representation defined in
+ * XML Encryption 1.1, section 5.4.1, except in <b>unpadded</b> form, with number of padding bits not indicated.
+ * Per the recommendation in the XML Encryption specification, this implementation only supports whole byte
+ * (bye-aligned) values, not arbitrary length bit-strings as theoretically allowed in the NIST specification,
+ * so the # of padding bits for each parameter value in the XML representation must and will always be 0.
+ * This means the methods {@link #unpadParam(String, String)} and {@link #fromXMLObject(KeyDerivationMethod)}
+ * which consume external values from the XML representation will throw if the number of indicated padding bits
+ * is non-zero. Similarly {@link #buildXMLObject()} will always emit values which indicate 0 padding bits.
+ * </p>
+ *
*/
-public class ConcatKDF extends AbstractInitializableComponent implements KeyDerivation, CloneableKeyAgreementParameter {
+public class ConcatKDF extends AbstractInitializableComponent
+ implements KeyDerivation, XMLExpressableKeyAgreementParameter, CloneableKeyAgreementParameter {
+
+ /** Default digest method. */
+ public static final String DEFAULT_DIGEST_METHOD = EncryptionConstants.ALGO_ID_DIGEST_SHA256;
+
+ /** Digest method. */
+ @NonnullAfterInit private String digestMethod;
+
+ /** AlgorithmID. */
+ @Nullable private String algorithmID;
+
+ /** PartyUInfo. */
+ @Nullable private String partyUInfo;
+
+ /** PartyVInfo. */
+ @Nullable private String partyVInfo;
+
+ /** SuppPubInfo. */
+ @Nullable private String suppPubInfo;
+
+ /** SuppPrivInfo. */
+ @Nullable private String suppPrivInfo;
/** {@inheritDoc} */
public String getAlgorithm() {
return EncryptionConstants.ALGO_ID_KEYDERIVATION_CONCATKDF;
}
+
+ /**
+ * Get the digest method algorithm URI.
+ *
+ * @return the algorithm URI
+ */
+ @NonnullAfterInit public String getDigestMethod() {
+ return digestMethod;
+ }
+
+ /**
+ * Set the digest method algorithm URI.
+ *
+ * @param newDigestMethod the algorithm URI
+ */
+ public void setDigestMethod(@Nullable final String newDigestMethod) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ digestMethod = StringSupport.trimOrNull(newDigestMethod);
+ }
+
+ /**
+ * Get the AlgorithmID in its unpadded hex-encoded form.
+ *
+ * @return the AlgorithmID
+ */
+ @Nullable public String getAlgorithmID() {
+ return algorithmID;
+ }
+
+ /**
+ * Set the AlgorithmID in its unpadded hex-encoded form.
+ *
+ * @param newAlgorithmID the AlgorithmID
+ */
+ public void setAlgorithmID(@Nullable final String newAlgorithmID) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ algorithmID = StringSupport.trimOrNull(newAlgorithmID);
+ }
+
+ /**
+ * Get the PartyUInfo in its unpadded hex-encoded form.
+ *
+ * @return the PartyUInfo
+ */
+ @Nullable public String getPartyUInfo() {
+ return partyUInfo;
+ }
+
+ /**
+ * Set the PartyUInfo in its unpadded hex-encoded form.
+ *
+ * @param newPartyUInfo the PartyUInfo
+ */
+ public void setPartyUInfo(@Nullable final String newPartyUInfo) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ partyUInfo = StringSupport.trimOrNull(newPartyUInfo);
+ }
+
+ /**
+ * Get the PartyVInfo in its unpadded hex-encoded form.
+ *
+ * @return the PartyUInfo
+ */
+ @Nullable public String getPartyVInfo() {
+ return partyVInfo;
+ }
+
+ /**
+ * Set the PartyVInfo in its unpadded hex-encoded form.
+ *
+ * @param newPartyVInfo the PartyVInfo
+ */
+ public void setPartyVInfo(@Nullable final String newPartyVInfo) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ partyVInfo = StringSupport.trimOrNull(newPartyVInfo);
+ }
+
+ /**
+ * Get the SuppPubInfo in its unpadded hex-encoded form.
+ *
+ * @return the SuppPubInfo
+ */
+ @Nullable public String getSuppPubInfo() {
+ return suppPubInfo;
+ }
+
+ /**
+ * Set the SuppPubInfo in its unpadded hex-encoded form.
+ *
+ * @param newSuppPubInfo the SuppPubInfo
+ */
+ public void setSuppPubInfo(@Nullable final String newSuppPubInfo) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ suppPubInfo = StringSupport.trimOrNull(newSuppPubInfo);
+ }
+
+ /**
+ * Get the SuppPrivInfo in its unpadded hex-encoded form.
+ *
+ * @return the SuppPrivInfo
+ */
+ @Nullable public String getSuppPrivInfo() {
+ return suppPrivInfo;
+ }
+
+ /**
+ * Set the SuppPrivInfo in its unpadded hex-encoded form.
+ *
+ * @param newSuppPrivInfo the SuppPrivInfo
+ */
+ public void setSuppPrivInfo(@Nullable final String newSuppPrivInfo) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ suppPrivInfo = StringSupport.trimOrNull(newSuppPrivInfo);
+ }
+
+ /** {@inheritDoc} */
+ protected void doInitialize() throws ComponentInitializationException {
+ try {
+ decodeParam(algorithmID, "AlgorithmID");
+ decodeParam(partyUInfo, "PartyUInfo");
+ decodeParam(partyVInfo, "PartyVInfo");
+ decodeParam(suppPubInfo, "SuppPubInfo");
+ decodeParam(suppPrivInfo, "SuppPrivInfo");
+ } catch (final KeyDerivationException e) {
+ throw new ComponentInitializationException("Invalid ConcatKDF param value", e);
+ }
+
+ if (digestMethod == null) {
+ digestMethod = DEFAULT_DIGEST_METHOD;
+ } else {
+ final AlgorithmDescriptor descriptor = AlgorithmSupport.getGlobalAlgorithmRegistry().get(digestMethod);
+ if (descriptor == null) {
+ throw new ComponentInitializationException("Specified digest algorithm is unknown: " + digestMethod);
+ }
+ if (!DigestAlgorithm.class.isInstance(descriptor)) {
+ throw new ComponentInitializationException("Specified digest algorithm is not a digest algorithm: "
+ + digestMethod);
+ }
+ try {
+ // We don't store this off for later use b/c these appear to be non-thread-safe, per-use instances,
+ // so we get a new one each time in derive(...).
+ getDigestInstance(digestMethod);
+ } catch (final KeyDerivationException e) {
+ throw new ComponentInitializationException("Unable to obtain digest instance", e);
+ }
+ }
+ }
/** {@inheritDoc} */
public SecretKey derive(@Nonnull final byte[] secret, @Nonnull final String keyAlgorithm)
throws KeyDerivationException {
+ Constraint.isNotNull(secret, "Secret byte[] was null");
+ Constraint.isNotNull(keyAlgorithm, "Key algorithm was null");
+
+ final String jcaKeyAlgorithm = AlgorithmSupport.getKeyAlgorithm(keyAlgorithm);
+ if (jcaKeyAlgorithm == null) {
+ throw new KeyDerivationException("Could not determine JCA key algorithm from URI: " + keyAlgorithm);
+ }
+
+ final Integer jcaKeyLength = AlgorithmSupport.getKeyLength(keyAlgorithm);
+ if (jcaKeyLength == null) {
+ throw new KeyDerivationException("Could not determine JCA key length from URI: " + keyAlgorithm);
+ }
+
+ final byte[] otherInfo = Bytes.concat(
+ decodeParam(algorithmID, "AlgorithmID"),
+ decodeParam(partyUInfo, "PartyUInfo"),
+ decodeParam(partyVInfo, "PartyVInfo"),
+ decodeParam(suppPubInfo, "SuppPubInfo"),
+ decodeParam(suppPrivInfo, "SuppPrivInfo"));
+
+ final byte[] keyBytes = derive(secret, otherInfo, jcaKeyLength);
+
+ return new SecretKeySpec(keyBytes, jcaKeyAlgorithm);
+ }
+
+ /**
+ * Derive the key bytes.
+ *
+ * <p>
+ * This re-factored method mostly exists to facilitate unit testing using external test vectors
+ * which only specify the OtherInfo as an input, rather than its 5 constituent parts
+ * as defined in NIST SP 800-56A and XML Encryption 1.1.
+ * </p>
+ *
+ * @param secret the input secret from which to derive the key
+ * @param otherInfo the OtherInfo bit string as defined in NIST SP 800-56A
+ * @param keyLength the length of the derived key, in bits
+ *
+ * @return the derived key bytes
+ *
+ * @throws KeyDerivationException
+ */
+ protected byte[] derive(@Nonnull final byte[] secret, @Nonnull final byte[] otherInfo,
+ @Nonnull final Integer keyLength) throws KeyDerivationException {
+
+ final Digest digest = getDigestInstance(digestMethod);
- // TODO Auto-generated method stub
+ final ConcatenationKDFGenerator concatKDF = new ConcatenationKDFGenerator(digest);
+ final KDFParameters kdfParams = new KDFParameters(secret, otherInfo);
+ concatKDF.init(kdfParams);
- return null;
+ final int lengthInBytes = keyLength/8;
+
+ final byte[] keyBytes = new byte[lengthInBytes];
+ concatKDF.generateBytes(keyBytes, 0, lengthInBytes);
+
+ return keyBytes;
+ }
+
+ /**
+ * Get a new instance of the Bouncy Castle {@link Digest} for the specified digest algorithm URI.
+ *
+ * @param digestURI the digest algorithm URI
+ *
+ * @return a new corresponding instance of BC Digest
+ *
+ * @throws KeyDerivationException
+ */
+ @Nonnull protected Digest getDigestInstance(@Nonnull final String digestURI) throws KeyDerivationException {
+ switch(digestURI) {
+ case SignatureConstants.ALGO_ID_DIGEST_SHA1:
+ return new SHA1Digest();
+ case SignatureConstants.ALGO_ID_DIGEST_SHA224:
+ return new SHA224Digest();
+ case SignatureConstants.ALGO_ID_DIGEST_SHA256:
+ return new SHA256Digest();
+ case SignatureConstants.ALGO_ID_DIGEST_SHA384:
+ return new SHA384Digest();
+ case SignatureConstants.ALGO_ID_DIGEST_SHA512:
+ return new SHA512Digest();
+ case SignatureConstants.ALGO_ID_DIGEST_RIPEMD160:
+ return new RIPEMD160Digest();
+ default:
+ throw new KeyDerivationException("Specified digest algorithm is unsupported: " + digestURI);
+ }
}
/** {@inheritDoc} */
@@ -59,7 +359,16 @@ public class ConcatKDF extends AbstractInitializableComponent implements KeyDeri
final ConcatKDFParams params =
(ConcatKDFParams) XMLObjectSupport.buildXMLObject(ConcatKDFParams.DEFAULT_ELEMENT_NAME);
- //TODO populate params based on properties
+ final DigestMethod xmlDigestMethod =
+ (DigestMethod) XMLObjectSupport.buildXMLObject(DigestMethod.DEFAULT_ELEMENT_NAME);
+ xmlDigestMethod.setAlgorithm(digestMethod);
+ params.setDigestMethod(xmlDigestMethod);
+
+ params.setAlgorithmID(padParam(algorithmID));
+ params.setPartyUInfo(padParam(partyUInfo));
+ params.setPartyVInfo(padParam(partyVInfo));
+ params.setSuppPubInfo(padParam(suppPubInfo));
+ params.setSuppPrivInfo(padParam(suppPrivInfo));
method.getUnknownXMLObjects().add(params);
@@ -76,4 +385,97 @@ public class ConcatKDF extends AbstractInitializableComponent implements KeyDeri
}
}
+ /**
+ * Decode the specified concatenation parameter value for input to the derivation operation.
+ *
+ * @param value the value to process
+ * @param name the name of the value being processed, for diagnostic purposes
+ *
+ * @return the decoded value, which may be an empty array
+ *
+ * @throws KeyDerivationException
+ */
+ @Nonnull protected byte[] decodeParam(@Nullable final String value, @Nonnull final String name)
+ throws KeyDerivationException {
+
+ final String trimmed = StringSupport.trimOrNull(value);
+ if (trimmed == null) {
+ return new byte[]{};
+ }
+
+ byte[] decoded = null;
+ try {
+ decoded = Hex.decodeHex(trimmed);
+ } catch (final DecoderException e) {
+ throw new KeyDerivationException("ConcatKDF parameter was not valid hex-encoded value: " + name, e);
+ }
+
+ return decoded;
+ }
+
+ /**
+ * Pad the specified concatenation parameter value for output in the formed required by
+ * XML Encryption 1.1.
+ *
+ * <p>
+ * No syntactic validation is done on the input value. Since only whole byte-aligned values are not supported,
+ * this method merely pre-pends "00" to indicate 0 padding bits.
+ * </p>
+ *
+ * @param value the value to process
+ *
+ * @return the padded value, which may be null
+ *
+ * @throws KeyDerivationException
+ */
+ @Nullable protected static String padParam(@Nullable final String value) {
+
+ final String trimmed = StringSupport.trimOrNull(value);
+ if (trimmed == null) {
+ return null;
+ }
+
+ return "00" + trimmed;
+
+ }
+
+ /**
+ * Unpad the specified concatenation parameter value from the padded from required by XML Encryption 1.1
+ * for input to the derivation operation.
+ *
+ * <p>
+ * Since only whole byte-aligned values supported, this method required input values to begin with "00",
+ * indicating 0 padding bits.
+ * </p>
+ *
+ * @param value the value to process
+ * @param name the name of the value being processed, for diagnostic purposes
+ *
+ * @return the unpadded value, which may be null
+ *
+ * @throws KeyDerivationException if the input value is invalid
+ */
+ @Nullable protected static String unpadParam(@Nullable final String value, @Nullable final String name)
+ throws KeyDerivationException {
+
+ final String trimmed = StringSupport.trimOrNull(value);
+ if (trimmed == null) {
+ return null;
+ }
+
+ // We only support whole byte-aligned values, so # of padding bits must always be 0
+ if (!trimmed.startsWith("00")) {
+ throw new KeyDerivationException("ConcatKDF parameter was not a valid padded hexBinary value "
+ + "(non-byte-aligned): " + name);
+ }
+
+ // Minimum valid padded length would be 2 bytes (4 hex digits): 1 for # of padding bits, 1+ for data
+ if (trimmed.length() < 4) {
+ throw new KeyDerivationException("ConcatKDF parameter was not a valid padded hexBinary value (too short): "
+ + name);
+ }
+
+ return trimmed.substring(2);
+ }
+
}
diff --git a/opensaml-xmlsec-impl/src/main/java/org/opensaml/xmlsec/derivation/impl/PBKDF2.java b/opensaml-xmlsec-impl/src/main/java/org/opensaml/xmlsec/derivation/impl/PBKDF2.java
index be3dc09e8..a47c94bf4 100644
--- a/opensaml-xmlsec-impl/src/main/java/org/opensaml/xmlsec/derivation/impl/PBKDF2.java
+++ b/opensaml-xmlsec-impl/src/main/java/org/opensaml/xmlsec/derivation/impl/PBKDF2.java
@@ -17,37 +17,347 @@
package org.opensaml.xmlsec.derivation.impl;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+
import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.util.XMLObjectSupport;
import org.opensaml.xmlsec.agreement.CloneableKeyAgreementParameter;
+import org.opensaml.xmlsec.agreement.XMLExpressableKeyAgreementParameter;
+import org.opensaml.xmlsec.algorithm.AlgorithmDescriptor;
+import org.opensaml.xmlsec.algorithm.AlgorithmSupport;
+import org.opensaml.xmlsec.algorithm.MACAlgorithm;
import org.opensaml.xmlsec.derivation.KeyDerivation;
import org.opensaml.xmlsec.derivation.KeyDerivationException;
-import org.opensaml.xmlsec.encryption.ConcatKDFParams;
+import org.opensaml.xmlsec.encryption.IterationCount;
import org.opensaml.xmlsec.encryption.KeyDerivationMethod;
+import org.opensaml.xmlsec.encryption.KeyLength;
+import org.opensaml.xmlsec.encryption.PBKDF2Params;
+import org.opensaml.xmlsec.encryption.PRF;
+import org.opensaml.xmlsec.encryption.Salt;
+import org.opensaml.xmlsec.encryption.Specified;
import org.opensaml.xmlsec.encryption.support.EncryptionConstants;
+import org.opensaml.xmlsec.signature.support.SignatureConstants;
+
+import com.google.common.base.Charsets;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.codec.Base64Support;
+import net.shibboleth.utilities.java.support.codec.DecodingException;
+import net.shibboleth.utilities.java.support.codec.EncodingException;
import net.shibboleth.utilities.java.support.component.AbstractInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
/**
* Implementation of PBKDF2 key derivation as defined in XML Encryption 1.1.
*/
-public class PBKDF2 extends AbstractInitializableComponent implements KeyDerivation, CloneableKeyAgreementParameter {
+public class PBKDF2 extends AbstractInitializableComponent
+ implements KeyDerivation, XMLExpressableKeyAgreementParameter, CloneableKeyAgreementParameter {
+
+ /** Default PRF. */
+ public static final String DEFAULT_PRF = SignatureConstants.ALGO_ID_MAC_HMAC_SHA256;
+
+ /** Default iteration count. */
+ public static final Integer DEFAULT_ITERATION_COUNT = 2000;
+
+ /** Default length for generated salt, in bytes. */
+ public static final Integer DEFAULT_GENERATED_SALT_LENGTH = 8;
+
+ /** Base algorithm ID for PBKDF2 SecretKeyFactory. */
+ private static final String PBKDF2_JCA_ALGORITHM_BASE = "PBKDF2With";
+
+ /** Base64-encoded salt value. */
+ @Nullable private String salt;
+
+ /** Generated salt length, in bytes. */
+ @NonnullAfterInit private Integer generatedSaltLength;
+
+ /** SecureRandom generator for salt. */
+ @NonnullAfterInit private SecureRandom secureRandom;
+
+ /** Iteration count. */
+ @NonnullAfterInit private Integer iterationCount;
+
+ /** Key length, in <b>bits</b>. */
+ @Nullable private Integer keyLength;
+
+ /** Pseudo-random function algorithm. */
+ @NonnullAfterInit private String prf;
/** {@inheritDoc} */
public String getAlgorithm() {
return EncryptionConstants.ALGO_ID_KEYDERIVATION_PBKDF2;
}
+
+ /**
+ * Get the Base64-encoded salt value.
+ *
+ * @return the salt value
+ */
+ @Nullable public String getSalt() {
+ return salt;
+ }
+
+ /**
+ * Set the Base64-encoded salt value.
+ *
+ * @param value the salt
+ */
+ public void setSalt(@Nullable final String value) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ salt = StringSupport.trimOrNull(value);
+ }
+
+ /**
+ * Get the generated salt length, in bytes.
+ *
+ * @return the generated salt length, in bytes
+ */
+ @NonnullAfterInit public Integer getGeneratedSaltLength() {
+ return generatedSaltLength;
+ }
+
+ /**
+ * Set the generated salt length, in bytes.
+ *
+ * @param length
+ */
+ public void setGeneratedSaltLength(@Nullable final Integer length) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ generatedSaltLength = length;
+ }
+
+ /**
+ * Get the secure random generator.
+ *
+ * <p>
+ * Defaults to the platform default via <code>new SecureRandom()</code>
+ * </p>
+ *
+ * @return the secure random instance
+ */
+ @NonnullAfterInit public SecureRandom getRandom() {
+ return secureRandom;
+ }
+
+ /**
+ * Set the secure random generator.
+ *
+ * <p>
+ * Defaults to the platform default via <code>new SecureRandom()</code>
+ * </p>
+ *
+ * @param sr the secure random generator to set
+ */
+ public void setRandom(@Nullable final SecureRandom sr) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ secureRandom = sr;
+ }
+
+ /**
+ * Get the iteration count.
+ *
+ * @return the iteration count
+ */
+ @NonnullAfterInit public Integer getIterationCount() {
+ return iterationCount;
+ }
+
+ /**
+ * Set the iteration count.
+ *
+ * @param count
+ */
+ public void setIterationCount(@Nullable final Integer count) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ iterationCount = count;
+ }
+
+ /**
+ * Get the key length, in number of <b>bits</b>.
+ *
+ * <p>
+ * Note: KeyLength in expressed XML will be in <b>bytes</b>
+ * </p>
+ *
+ * @return the key length
+ */
+ @Nullable public Integer getKeyLength() {
+ return keyLength;
+ }
+
+ /**
+ * Set the key length, in number of <b>bits</b>.
+ *
+ * <p>
+ * Note: KeyLength in expressed XML will be in <b>bytes</b>
+ * </p>
+ *
+ * @param length
+ */
+ public void setKeyLength(@Nullable final Integer length) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ keyLength = length;
+ }
+
+ /**
+ * Get the pseudo-random function algorithm URI.
+ *
+ * @return the algorithm URI
+ */
+ @NonnullAfterInit public String getPRF() {
+ return prf;
+ }
+
+ /**
+ * Set the pseudo-random function algorithm URI.
+ *
+ * @param uri
+ */
+ public void setPRF(@Nullable final String uri) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ prf = StringSupport.trimOrNull(uri);
+ }
+
+ /** {@inheritDoc} */
+ // Checkstyle: CyclomaticComplexity OFF
+ protected void doInitialize() throws ComponentInitializationException {
+ if (salt != null) {
+ try {
+ Base64Support.decode(salt);
+ } catch (final DecodingException e) {
+ throw new ComponentInitializationException("Salt value was not valid Base64", e);
+ }
+ }
+
+ if (generatedSaltLength == null) {
+ generatedSaltLength = DEFAULT_GENERATED_SALT_LENGTH;
+ }
+
+ if (secureRandom == null) {
+ secureRandom = new SecureRandom();
+ }
+
+ if (iterationCount == null) {
+ iterationCount = DEFAULT_ITERATION_COUNT;
+ }
+
+ if (keyLength != null && keyLength % 8 != 0) {
+ throw new ComponentInitializationException("Specified key length in bits is not a multiple of 8");
+ }
+
+ if (prf == null) {
+ prf = DEFAULT_PRF;
+ } else {
+ final AlgorithmDescriptor descriptor = AlgorithmSupport.getGlobalAlgorithmRegistry().get(prf);
+ if (descriptor == null) {
+ throw new ComponentInitializationException("Specified PRF algorithm is unknown: " + prf);
+ }
+ if (!MACAlgorithm.class.isInstance(descriptor)) {
+ throw new ComponentInitializationException("Specified PRF algorithm is not a MAC algorithm: " + prf);
+ }
+ }
+ }
+ // Checkstyle: CyclomaticComplexity ON
/** {@inheritDoc} */
public SecretKey derive(@Nonnull final byte[] secret, @Nonnull final String keyAlgorithm)
throws KeyDerivationException {
+ Constraint.isNotNull(secret, "Secret byte[] was null");
+ Constraint.isNotNull(keyAlgorithm, "Key algorithm was null");
+
+ final String jcaKeyAlgorithm = AlgorithmSupport.getKeyAlgorithm(keyAlgorithm);
+ if (jcaKeyAlgorithm == null) {
+ throw new KeyDerivationException("Could not determine JCA key algorithm from URI: " + keyAlgorithm);
+ }
- // TODO Auto-generated method stub
+ final byte[] saltBytes = getEffectiveSalt();
- return null;
+ final Integer length = getEffectiveKeyLength(keyAlgorithm);
+
+ final String jcaPRF = AlgorithmSupport.getAlgorithmID(prf);
+
+ final char[] secretChars = new String(secret, Charsets.UTF_8).toCharArray();
+
+ try {
+ final PBEKeySpec spec = new PBEKeySpec(secretChars, saltBytes, iterationCount, length);
+ final SecretKeyFactory skf = SecretKeyFactory.getInstance(PBKDF2_JCA_ALGORITHM_BASE + jcaPRF);
+ return new SecretKeySpec(skf.generateSecret(spec).getEncoded(), jcaKeyAlgorithm);
+ } catch (final NoSuchAlgorithmException | InvalidKeySpecException e) {
+ throw new KeyDerivationException("Error generating SecretKey via PBKDF2", e);
+ }
+ }
+
+ /**
+ * Get the effective salt bytes to use.
+ *
+ * @return the salt bytes
+ *
+ * @throws KeyDerivationException
+ */
+ protected byte[] getEffectiveSalt() throws KeyDerivationException {
+ byte[] saltBytes = null;
+ if (salt == null) {
+ // Usually the originator/encrypting case. We generate and set it internally here so can emit in XML later.
+ saltBytes = new byte[generatedSaltLength];
+ secureRandom.nextBytes(saltBytes);
+ try {
+ salt = Base64Support.encode(saltBytes, false);
+ } catch (final EncodingException e) {
+ throw new KeyDerivationException("Error Base64-encoding generated salt", e);
+ }
+ } else {
+ // Usually the recipient/decrypting case, where value is parsed from the Salt XML Element.
+ try {
+ saltBytes = Base64Support.decode(salt);
+ } catch (final DecodingException e) {
+ // We already tested this during init so this shouldn't happen
+ throw new KeyDerivationException("Error Base64-decoding supplied salt", e);
+ }
+ }
+ return saltBytes;
+ }
+
+ /**
+ * Get the effective key length, in bits.
+ *
+ * @param keyAlgorithm the algorithm for which the derived key will be used
+ *
+ * @return the effective key length, in bits
+ *
+ * @throws KeyDerivationException
+ */
+ protected Integer getEffectiveKeyLength(@Nonnull final String keyAlgorithm) throws KeyDerivationException {
+ final Integer jcaKeyLength = AlgorithmSupport.getKeyLength(keyAlgorithm);
+ if (jcaKeyLength == null) {
+ throw new KeyDerivationException("Failed to determine key length for algorithm URI: " + keyAlgorithm);
+ }
+
+ if (keyLength == null) {
+ // Usually the originator/encrypting case. We set it internally here so can emit in XML later.
+ keyLength = jcaKeyLength;
+ } else {
+ // Usually the recipient/decrypting case, where value is parsed from the KeyLength XML Element.
+ // Validate that specified key length value matches that of the specified algorithm URI.
+ if (! keyLength.equals(jcaKeyLength)) {
+ throw new KeyDerivationException(String.format("Specified key length '%d' does not match URI: %s",
+ keyLength, keyAlgorithm));
+ }
+ }
+
+ return keyLength;
}
/** {@inheritDoc} */
@@ -56,10 +366,31 @@ public class PBKDF2 extends AbstractInitializableComponent implements KeyDerivat
(KeyDerivationMethod) XMLObjectSupport.buildXMLObject(KeyDerivationMethod.DEFAULT_ELEMENT_NAME);
method.setAlgorithm(getAlgorithm());
- final ConcatKDFParams params =
- (ConcatKDFParams) XMLObjectSupport.buildXMLObject(ConcatKDFParams.DEFAULT_ELEMENT_NAME);
+ final PBKDF2Params params =
+ (PBKDF2Params) XMLObjectSupport.buildXMLObject(PBKDF2Params.DEFAULT_ELEMENT_NAME);
+
+ //TODO do sanity checking on these - how to report out? Maybe method signature should have a thrown exception.
+
+ final Salt xmlSalt = (Salt) XMLObjectSupport.buildXMLObject(Salt.DEFAULT_ELEMENT_NAME);
+ final Specified specified = (Specified) XMLObjectSupport.buildXMLObject(Specified.DEFAULT_ELEMENT_NAME);
+ specified.setValue(salt);
+ xmlSalt.setSpecified(specified);
+ params.setSalt(xmlSalt);
+
+ final IterationCount xmlIterationcount =
+ (IterationCount) XMLObjectSupport.buildXMLObject(IterationCount.DEFAULT_ELEMENT_NAME);
+ xmlIterationcount.setValue(iterationCount);
+ params.setIterationCount(xmlIterationcount);
+
+ final KeyLength xmlKeyLength = (KeyLength) XMLObjectSupport.buildXMLObject(KeyLength.DEFAULT_ELEMENT_NAME);
+ // Note: We're tracking this in # of bits, but the XML element uses # of bytes.
+ // It's already validated to be an exact multiple of 8.
+ xmlKeyLength.setValue(keyLength / 8);
+ params.setKeyLength(xmlKeyLength);
- //TODO populate params based on properties
+ final PRF xmlPRF = (PRF) XMLObjectSupport.buildXMLObject(PRF.DEFAULT_ELEMENT_NAME);
+ xmlPRF.setAlgorithm(prf);
+ params.setPRF(xmlPRF);
method.getUnknownXMLObjects().add(params);
diff --git a/opensaml-xmlsec-impl/src/test/java/org/opensaml/xmlsec/derivation/impl/ConcatKDFTest.java b/opensaml-xmlsec-impl/src/test/java/org/opensaml/xmlsec/derivation/impl/ConcatKDFTest.java
new file mode 100644
index 000000000..87d8dc2f4
--- /dev/null
+++ b/opensaml-xmlsec-impl/src/test/java/org/opensaml/xmlsec/derivation/impl/ConcatKDFTest.java
@@ -0,0 +1,498 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.opensaml.xmlsec.derivation.impl;
+
+import javax.crypto.SecretKey;
+
+import org.apache.commons.codec.DecoderException;
+import org.apache.commons.codec.binary.Hex;
+import org.opensaml.core.testing.OpenSAMLInitBaseTestCase;
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.xmlsec.derivation.KeyDerivationException;
+import org.opensaml.xmlsec.encryption.ConcatKDFParams;
+import org.opensaml.xmlsec.encryption.KeyDerivationMethod;
+import org.opensaml.xmlsec.encryption.support.EncryptionConstants;
+import org.opensaml.xmlsec.signature.support.SignatureConstants;
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import net.shibboleth.utilities.java.support.codec.EncodingException;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+/**
+ *
+ */
+public class ConcatKDFTest extends OpenSAMLInitBaseTestCase {
+
+ @Test
+ public void defaultProperties() throws Exception {
+ ConcatKDF kdf = new ConcatKDF();
+ kdf.initialize();
+
+ Assert.assertEquals(kdf.getAlgorithm(), EncryptionConstants.ALGO_ID_KEYDERIVATION_CONCATKDF);
+
+ Assert.assertNull(kdf.getAlgorithmID());
+ Assert.assertNull(kdf.getPartyUInfo());
+ Assert.assertNull(kdf.getPartyVInfo());
+ Assert.assertNull(kdf.getSuppPubInfo());
+ Assert.assertNull(kdf.getSuppPrivInfo());
+ Assert.assertEquals(kdf.getDigestMethod(), SignatureConstants.ALGO_ID_DIGEST_SHA256);
+ }
+
+ @Test
+ public void explicitProperties() throws Exception {
+ ConcatKDF kdf = new ConcatKDF();
+ kdf.setAlgorithmID(" AA ");
+ kdf.setPartyUInfo(" BB ");
+ kdf.setPartyVInfo(" CC ");
+ kdf.setSuppPubInfo(" DD ");
+ kdf.setSuppPrivInfo(" EE ");
+ kdf.setDigestMethod(SignatureConstants.ALGO_ID_DIGEST_SHA512);
+ kdf.initialize();
+
+ Assert.assertEquals(kdf.getAlgorithm(), EncryptionConstants.ALGO_ID_KEYDERIVATION_CONCATKDF);
+
+ Assert.assertEquals(kdf.getAlgorithmID(), "AA");
+ Assert.assertEquals(kdf.getPartyUInfo(), "BB");
+ Assert.assertEquals(kdf.getPartyVInfo(), "CC");
+ Assert.assertEquals(kdf.getSuppPubInfo(), "DD");
+ Assert.assertEquals(kdf.getSuppPrivInfo(), "EE");
+ Assert.assertEquals(kdf.getDigestMethod(), SignatureConstants.ALGO_ID_DIGEST_SHA512);
+ }
+
+ @Test(expectedExceptions = ComponentInitializationException.class)
+ public void initNonDigestMethod() throws Exception {
+ ConcatKDF kdf = new ConcatKDF();
+ kdf.setDigestMethod(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1);
+ kdf.initialize();
+ }
+
+ @Test(expectedExceptions = ComponentInitializationException.class)
+ public void initUnsupportedDigest() throws Exception {
+ ConcatKDF kdf = new ConcatKDF();
+ kdf.setDigestMethod(SignatureConstants.ALGO_ID_DIGEST_NOT_RECOMMENDED_MD5);
+ kdf.initialize();
+ }
+
+ @Test(expectedExceptions = ComponentInitializationException.class)
+ public void initBadAlgorithmID() throws Exception {
+ ConcatKDF kdf = new ConcatKDF();
+ kdf.setAlgorithmID("INVALID");
+ kdf.initialize();
+ }
+
+ @Test(expectedExceptions = ComponentInitializationException.class)
+ public void initBadPartyUInfo() throws Exception {
+ ConcatKDF kdf = new ConcatKDF();
+ kdf.setPartyUInfo("INVALID");
+ kdf.initialize();
+ }
+
+ @Test(expectedExceptions = ComponentInitializationException.class)
+ public void initBadPartyVInfo() throws Exception {
+ ConcatKDF kdf = new ConcatKDF();
+ kdf.setPartyVInfo("INVALID");
+ kdf.initialize();
+ }
+
+ @Test(expectedExceptions = ComponentInitializationException.class)
+ public void initBadSuppPubInfo() throws Exception {
+ ConcatKDF kdf = new ConcatKDF();
+ kdf.setSuppPubInfo("INVALID");
+ kdf.initialize();
+ }
+
+ @Test(expectedExceptions = ComponentInitializationException.class)
+ public void initBadSuppPrivInfo() throws Exception {
+ ConcatKDF kdf = new ConcatKDF();
+ kdf.setSuppPrivInfo("INVALID");
+ kdf.initialize();
+ }
+
+ @Test
+ public void xmlGenerationSuccess() throws Exception {
+ ConcatKDF kdf = new ConcatKDF();
+ kdf.setAlgorithmID("AA");
+ kdf.setPartyUInfo("BB");
+ kdf.setPartyVInfo("CC");
+ kdf.setSuppPubInfo("DD");
+ kdf.setSuppPrivInfo("EE");
+ kdf.setDigestMethod(SignatureConstants.ALGO_ID_DIGEST_SHA512);
+ kdf.initialize();
+
+ XMLObject xmlObject = kdf.buildXMLObject();
+ Assert.assertNotNull(xmlObject);
+ Assert.assertTrue(KeyDerivationMethod.class.isInstance(xmlObject));
+
+ KeyDerivationMethod kdm = KeyDerivationMethod.class.cast(xmlObject);
+ Assert.assertEquals(kdm.getAlgorithm(), EncryptionConstants.ALGO_ID_KEYDERIVATION_CONCATKDF);
+ Assert.assertEquals(kdm.getUnknownXMLObjects().size(), 1);
+
+ ConcatKDFParams kdmParams = ConcatKDFParams.class.cast(kdm.getUnknownXMLObjects().get(0));
+
+ Assert.assertEquals(kdmParams.getAlgorithmID(), "00AA");
+ Assert.assertEquals(kdmParams.getPartyUInfo(), "00BB");
+ Assert.assertEquals(kdmParams.getPartyVInfo(), "00CC");
+ Assert.assertEquals(kdmParams.getSuppPubInfo(), "00DD");
+ Assert.assertEquals(kdmParams.getSuppPrivInfo(), "00EE");
+
+ Assert.assertNotNull(kdmParams.getDigestMethod());
+ Assert.assertEquals(kdmParams.getDigestMethod().getAlgorithm(), SignatureConstants.ALGO_ID_DIGEST_SHA512);
+ }
+
+ @Test
+ public void cloning() throws Exception {
+ ConcatKDF kdf = new ConcatKDF();
+ kdf.setAlgorithmID("AA");
+ kdf.setPartyUInfo("BB");
+ kdf.setPartyVInfo("CC");
+ kdf.setSuppPubInfo("DD");
+ kdf.setSuppPrivInfo("EE");
+ kdf.setDigestMethod(SignatureConstants.ALGO_ID_DIGEST_SHA512);
+ kdf.initialize();
+
+ Assert.assertEquals(kdf.getAlgorithm(), EncryptionConstants.ALGO_ID_KEYDERIVATION_CONCATKDF);
+
+ ConcatKDF cloned = kdf.clone();
+
+ Assert.assertEquals(cloned.getAlgorithmID(), "AA");
+ Assert.assertEquals(cloned.getPartyUInfo(), "BB");
+ Assert.assertEquals(cloned.getPartyVInfo(), "CC");
+ Assert.assertEquals(cloned.getSuppPubInfo(), "DD");
+ Assert.assertEquals(cloned.getSuppPrivInfo(), "EE");
+ Assert.assertEquals(cloned.getDigestMethod(), SignatureConstants.ALGO_ID_DIGEST_SHA512);
+ }
+
+ @Test
+ public void deriveWithDefaults() throws Exception {
+ ConcatKDF kdf = new ConcatKDF();
+ kdf.initialize();
+
+ byte[] secret = Hex.decodeHex("DEADBEEF");
+
+ SecretKey derivedKey = kdf.derive(secret, EncryptionConstants.ALGO_ID_BLOCKCIPHER_AES128_GCM);
+
+ Assert.assertNotNull(derivedKey);
+ Assert.assertEquals(derivedKey.getAlgorithm(), "AES");
+ Assert.assertEquals(derivedKey.getEncoded().length * 8, 128);
+ }
+
+ @Test
+ public void deriveWithExplicitProperties() throws Exception {
+ ConcatKDF kdf = new ConcatKDF();
+ kdf.setAlgorithmID("AA");
+ kdf.setPartyUInfo("BB");
+ kdf.setPartyVInfo("CC");
+ kdf.setSuppPubInfo("DD");
+ kdf.setSuppPrivInfo("EE");
+ kdf.initialize();
+
+ byte[] secret = Hex.decodeHex("DEADBEEF");
+
+ SecretKey derivedKey = kdf.derive(secret, EncryptionConstants.ALGO_ID_BLOCKCIPHER_AES128_GCM);
+
+ Assert.assertNotNull(derivedKey);
+ Assert.assertEquals(derivedKey.getAlgorithm(), "AES");
+ Assert.assertEquals(derivedKey.getEncoded().length * 8, 128);
+ }
+
+ @Test(expectedExceptions = KeyDerivationException.class)
+ public void unknownKeyAlgorithm() throws Exception {
+ ConcatKDF kdf = new ConcatKDF();
+ kdf.initialize();
+
+ byte[] secret = Hex.decodeHex("DEADBEEF");
+
+ kdf.derive(secret, "urn:test:InvalidKeyAlgorithm");
+ }
+
+ @Test(expectedExceptions = KeyDerivationException.class)
+ public void nonKeyLengthAlgorithm() throws Exception {
+ ConcatKDF kdf = new ConcatKDF();
+ kdf.initialize();
+
+ byte[] secret = Hex.decodeHex("DEADBEEF");
+
+ // Just use this as a stand-in for something which is KeySpecifiedAlgorithm but not KeyLengthSpecifiedAlgorithm
+ kdf.derive(secret, SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
+ }
+
+ @Test
+ public void decodeParam() throws Exception {
+ ConcatKDF kdf = new ConcatKDF();
+ kdf.initialize();
+
+ Assert.assertEquals(kdf.decodeParam(null, "test"), new byte[] {});
+ Assert.assertEquals(kdf.decodeParam(" ", "test"), new byte[] {});
+ Assert.assertEquals(kdf.decodeParam("00", "test"), new byte[] {0x00});
+ Assert.assertEquals(kdf.decodeParam("000000", "test"), new byte[] {0x00, 0x00, 0x00});
+ Assert.assertEquals(kdf.decodeParam("AB", "test"), new byte[] {(byte) 0xAB});
+ Assert.assertEquals(kdf.decodeParam("ABCD", "test"), new byte[] {(byte) 0xAB, (byte) 0xCD});
+ Assert.assertEquals(kdf.decodeParam("DEADBEEF", "test"), new byte[] {(byte) 0xDE, (byte) 0xAD, (byte) 0xBE, (byte) 0xEF});
+
+ try {
+ // invalid hex value
+ kdf.decodeParam("INVALID", "test");
+ Assert.fail("Invalid value should have failed");
+ } catch (KeyDerivationException e) {
+ //expected
+ }
+
+ try {
+ // invalid hex value
+ kdf.decodeParam("A", "test");
+ Assert.fail("Invalid value should have failed");
+ } catch (KeyDerivationException e) {
+ //expected
+ }
+
+ try {
+ // invalid hex value
+ kdf.decodeParam("ABC", "test");
+ Assert.fail("Invalid value should have failed");
+ } catch (KeyDerivationException e) {
+ //expected
+ }
+ }
+
+ @Test
+ public void padParam() throws Exception {
+ Assert.assertEquals(ConcatKDF.padParam(null), null);
+ Assert.assertEquals(ConcatKDF.padParam(" "), null);
+
+ Assert.assertEquals(ConcatKDF.padParam("AA"), "00AA");
+ Assert.assertEquals(ConcatKDF.padParam(" AABBCC "), "00AABBCC");
+ }
+
+ @Test
+ public void unpadParam() throws Exception {
+ Assert.assertEquals(ConcatKDF.unpadParam(null, "test"), null);
+ Assert.assertEquals(ConcatKDF.unpadParam(" ", "test"), null);
+
+ Assert.assertEquals(ConcatKDF.unpadParam("00AA", "test"), "AA");
+ Assert.assertEquals(ConcatKDF.unpadParam(" 00AABBCC ", "test"), "AABBCC");
+
+ try {
+ // Unsupported padding
+ ConcatKDF.unpadParam("01AA", "test");
+ Assert.fail("Invalid value should have failed");
+ } catch (KeyDerivationException eA ) {
+ //expected
+ }
+
+ try {
+ // Too short
+ ConcatKDF.unpadParam("00", "test");
+ Assert.fail("Invalid value should have failed");
+ } catch (KeyDerivationException eA ) {
+ //expected
+ }
+ try {
+ // Too short
+ ConcatKDF.unpadParam("00A", "test");
+ Assert.fail("Invalid value should have failed");
+ } catch (KeyDerivationException eA ) {
+ //expected
+ }
+ }
+
+
+ //
+ // Derivation tests using test vectors from external sources, and supporting code
+ //
+
+ @DataProvider(name = "testVectors")
+ public Object[][] testVectors() throws DecoderException, EncodingException {
+ return new Object[][] {
+
+ // Non-normative vectors.
+ // https://github.com/patrickfav/singlestep-kdf/wiki/NIST-SP-800-56C-Rev1:-Non-Official-Test-Vectors
+ // Nominally this is for a newer rev of the NIST spec, but the algorithm is the same.
+ // Just included a representative subset of these as of 2020-12-30.
+
+ // SHA-1 digest
+ new Object[] {
+ Hex.decodeHex("d09a6b1a472f930db4f5e6b967900744"),
+ Hex.decodeHex("b117255ab5f1b6b96fc434b0"),
+ 128,
+ SignatureConstants.ALGO_ID_DIGEST_SHA1,
+ Hex.decodeHex("b5a3c52e97ae6e8c5069954354eab3c7")},
+
+ new Object[] {
+ Hex.decodeHex("343666c0dd34b756e70f759f14c304f5"),
+ Hex.decodeHex("722b28448d7eab85491bce09"),
+ 128,
+ SignatureConstants.ALGO_ID_DIGEST_SHA1,
+ Hex.decodeHex("1003b650ddd3f0891a15166db5ec881d")},
+
+ new Object[] {
+ Hex.decodeHex("b84acf03ab08652dd7f82fa956933261"),
+ Hex.decodeHex("3d8773ec068c86053a918565"),
+ 128,
+ SignatureConstants.ALGO_ID_DIGEST_SHA1,
+ Hex.decodeHex("1635dcd1ce698f736831b4badb68ab2b")},
+
+ new Object[] {
+ Hex.decodeHex("8cc24ca3f1d1a8b34783780b79890430"),
+ Hex.decodeHex("f08d4f2d9a8e6d7105c0bc16"),
+ 128,
+ SignatureConstants.ALGO_ID_DIGEST_SHA1,
+ Hex.decodeHex("b8e716fb84a420aed4812cd76d9700ee")},
+
+ new Object[] {
+ Hex.decodeHex("ebe28edbae5a410b87a479243db3f690"),
+ Hex.decodeHex("e60dd8b28228ce5b9be74d3b"),
+ 128,
+ SignatureConstants.ALGO_ID_DIGEST_SHA1,
+ Hex.decodeHex("b4a23963e07f485382cb358a493daec1")},
+
+ new Object[] {
+ Hex.decodeHex("ebe28edbae5a410b87a479243db3f690"),
+ Hex.decodeHex("e60dd8b28228ce5b9be74d3b"),
+ 192,
+ SignatureConstants.ALGO_ID_DIGEST_SHA1,
+ Hex.decodeHex("b4a23963e07f485382cb358a493daec1759ac7043dbeac37")},
+
+ new Object[] {
+ Hex.decodeHex("ebe28edbae5a410b87a479243db3f690"),
+ Hex.decodeHex("e60dd8b28228ce5b9be74d3b"),
+ 256,
+ SignatureConstants.ALGO_ID_DIGEST_SHA1,
+ Hex.decodeHex("b4a23963e07f485382cb358a493daec1759ac7043dbeac37152c6ddf105031f0")},
+
+ // SHA-256 digest
+ new Object[] {
+ Hex.decodeHex("afc4e154498d4770aa8365f6903dc83b"),
+ Hex.decodeHex("662af20379b29d5ef813e655"),
+ 128,
+ SignatureConstants.ALGO_ID_DIGEST_SHA256,
+ Hex.decodeHex("f0b80d6ae4c1e19e2105a37024e35dc6")},
+
+ new Object[] {
+ Hex.decodeHex("a3ce8d61d699ad150e196a7ab6736a63"),
+ Hex.decodeHex("ce5cd95a44ee83a8fb83f34c"),
+ 128,
+ SignatureConstants.ALGO_ID_DIGEST_SHA256,
+ Hex.decodeHex("5db3455a22b65edfcfde3da3e8d724cd")},
+
+ new Object[] {
+ Hex.decodeHex("a9723e56045f0847fdd9c1c78781c8b7"),
+ Hex.decodeHex("e69b6005b78f7d42d0a8ed2a"),
+ 128,
+ SignatureConstants.ALGO_ID_DIGEST_SHA256,
+ Hex.decodeHex("ac3878b8cf357976f7fd8266923e1882")},
+
+ new Object[] {
+ Hex.decodeHex("a07a5e8df7ee1b2ce2a3d1348edfa8ab"),
+ Hex.decodeHex("e22a8ee34296dd39b56b31fb"),
+ 128,
+ SignatureConstants.ALGO_ID_DIGEST_SHA256,
+ Hex.decodeHex("70927d218b6d119268381e9930a4f256")},
+
+ new Object[] {
+ Hex.decodeHex("3f892bd8b84dae64a782a35f6eaa8f00"),
+ Hex.decodeHex("ec3f1cd873d28858a58cc39e"),
+ 128,
+ SignatureConstants.ALGO_ID_DIGEST_SHA256,
+ Hex.decodeHex("a7c0665298252531e0db37737a374651")},
+
+ new Object[] {
+ Hex.decodeHex("3f892bd8b84dae64a782a35f6eaa8f00"),
+ Hex.decodeHex("ec3f1cd873d28858a58cc39e"),
+ 192,
+ SignatureConstants.ALGO_ID_DIGEST_SHA256,
+ Hex.decodeHex("a7c0665298252531e0db37737a374651b368275f2048284d")},
+
+ new Object[] {
+ Hex.decodeHex("3f892bd8b84dae64a782a35f6eaa8f00"),
+ Hex.decodeHex("ec3f1cd873d28858a58cc39e"),
+ 256,
+ SignatureConstants.ALGO_ID_DIGEST_SHA256,
+ Hex.decodeHex("a7c0665298252531e0db37737a374651b368275f2048284d16a166c6d8a90a91")},
+
+ // SHA-256 digest
+ new Object[] {
+ Hex.decodeHex("108cf63318555c787fa578731dd4f037"),
+ Hex.decodeHex("53191b1dd3f94d83084d61d6"),
+ 128,
+ SignatureConstants.ALGO_ID_DIGEST_SHA512,
+ Hex.decodeHex("0ad475c1826da3007637970c8b92b993")},
+
+ new Object[] {
+ Hex.decodeHex("35fa6d42e65014f04bdd80ff1404ab27"),
+ Hex.decodeHex("506d9cfe967748d1e6f84bd9"),
+ 128,
+ SignatureConstants.ALGO_ID_DIGEST_SHA512,
+ Hex.decodeHex("16739821c3b13dee57e24c092211ddd6")},
+
+ new Object[] {
+ Hex.decodeHex("775e83546ce8b41a83656bd723d63c9e"),
+ Hex.decodeHex("514f4d06bf8c1646aeae28fa"),
+ 128,
+ SignatureConstants.ALGO_ID_DIGEST_SHA512,
+ Hex.decodeHex("0bce0e54a721367088495c0c4c0683f5")},
+
+ new Object[] {
+ Hex.decodeHex("03f1dea7561b885a5601c6e75e405140"),
+ Hex.decodeHex("1e366c4b697d20aa9a54d6f5"),
+ 128,
+ SignatureConstants.ALGO_ID_DIGEST_SHA512,
+ Hex.decodeHex("56a2ac8f0eb55fdc4d8a891664edfbdb")},
+
+ new Object[] {
+ Hex.decodeHex("e65b1905878b95f68b5535bd3b2b1013"),
+ Hex.decodeHex("830221b1730d9176f807d407"),
+ 128,
+ SignatureConstants.ALGO_ID_DIGEST_SHA512,
+ Hex.decodeHex("b8c44bdf0b85a64b6a51c12a06710e37")},
+
+ new Object[] {
+ Hex.decodeHex("e65b1905878b95f68b5535bd3b2b1013"),
+ Hex.decodeHex("830221b1730d9176f807d407"),
+ 192,
+ SignatureConstants.ALGO_ID_DIGEST_SHA512,
+ Hex.decodeHex("b8c44bdf0b85a64b6a51c12a06710e373d829bb1fda5b4e1")},
+
+ new Object[] {
+ Hex.decodeHex("e65b1905878b95f68b5535bd3b2b1013"),
+ Hex.decodeHex("830221b1730d9176f807d407"),
+ 256,
+ SignatureConstants.ALGO_ID_DIGEST_SHA512,
+ Hex.decodeHex("b8c44bdf0b85a64b6a51c12a06710e373d829bb1fda5b4e1a20795c6199594f6")},
+
+
+ };
+ }
+
+ @Test(dataProvider = "testVectors")
+ public void deriveTestVectors(byte[] secret, byte[] otherInfo, Integer keyLength, String digestMethod,
+ byte[] keyBytes) throws Exception {
+
+ ConcatKDF kdf = new ConcatKDF();
+ kdf.setDigestMethod(digestMethod);
+ kdf.initialize();
+
+ byte[] deriveKeyBytes = kdf.derive(secret, otherInfo, keyLength);
+
+ Assert.assertNotNull(deriveKeyBytes);
+ Assert.assertEquals(deriveKeyBytes.length * 8, keyLength.intValue());
+ Assert.assertEquals(deriveKeyBytes, keyBytes);
+ }
+
+}
diff --git a/opensaml-xmlsec-impl/src/test/java/org/opensaml/xmlsec/derivation/impl/MockKeyDerivation.java b/opensaml-xmlsec-impl/src/test/java/org/opensaml/xmlsec/derivation/impl/MockKeyDerivation.java
index 6050b3c06..fb4acbd17 100644
--- a/opensaml-xmlsec-impl/src/test/java/org/opensaml/xmlsec/derivation/impl/MockKeyDerivation.java
+++ b/opensaml-xmlsec-impl/src/test/java/org/opensaml/xmlsec/derivation/impl/MockKeyDerivation.java
@@ -19,26 +19,16 @@ package org.opensaml.xmlsec.derivation.impl;
import javax.crypto.SecretKey;
-import org.opensaml.core.xml.XMLObject;
-import org.opensaml.core.xml.util.XMLObjectSupport;
import org.opensaml.security.crypto.KeySupport;
import org.opensaml.xmlsec.algorithm.AlgorithmSupport;
import org.opensaml.xmlsec.derivation.KeyDerivation;
import org.opensaml.xmlsec.derivation.KeyDerivationException;
-import org.opensaml.xmlsec.encryption.KeyDerivationMethod;
/**
* Mock key derivation for testing.
*/
public class MockKeyDerivation implements KeyDerivation {
- /** {@inheritDoc} */
- public XMLObject buildXMLObject() {
- final KeyDerivationMethod method = (KeyDerivationMethod) XMLObjectSupport.buildXMLObject(KeyDerivationMethod.DEFAULT_ELEMENT_NAME);
- method.setAlgorithm(getAlgorithm());
- return method;
- }
-
/** {@inheritDoc} */
public String getAlgorithm() {
return "urn:test:MockKeyDerivation";
diff --git a/opensaml-xmlsec-impl/src/test/java/org/opensaml/xmlsec/derivation/impl/PBKDF2Test.java b/opensaml-xmlsec-impl/src/test/java/org/opensaml/xmlsec/derivation/impl/PBKDF2Test.java
new file mode 100644
index 000000000..438d95f1f
--- /dev/null
+++ b/opensaml-xmlsec-impl/src/test/java/org/opensaml/xmlsec/derivation/impl/PBKDF2Test.java
@@ -0,0 +1,639 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.opensaml.xmlsec.derivation.impl;
+
+import java.security.SecureRandom;
+
+import javax.crypto.SecretKey;
+
+import org.apache.commons.codec.DecoderException;
+import org.apache.commons.codec.binary.Hex;
+import org.opensaml.core.testing.OpenSAMLInitBaseTestCase;
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.xmlsec.algorithm.AlgorithmRegistry;
+import org.opensaml.xmlsec.algorithm.AlgorithmSupport;
+import org.opensaml.xmlsec.algorithm.BlockEncryptionAlgorithm;
+import org.opensaml.xmlsec.derivation.KeyDerivationException;
+import org.opensaml.xmlsec.encryption.KeyDerivationMethod;
+import org.opensaml.xmlsec.encryption.PBKDF2Params;
+import org.opensaml.xmlsec.encryption.support.EncryptionConstants;
+import org.opensaml.xmlsec.signature.support.SignatureConstants;
+import org.testng.Assert;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Charsets;
+
+import net.shibboleth.utilities.java.support.codec.Base64Support;
+import net.shibboleth.utilities.java.support.codec.EncodingException;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+/**
+ *
+ */
+public class PBKDF2Test extends OpenSAMLInitBaseTestCase {
+
+ @Test
+ public void defaultProperties() throws Exception {
+ PBKDF2 kdf = new PBKDF2();
+ kdf.initialize();
+
+ Assert.assertEquals(kdf.getAlgorithm(), EncryptionConstants.ALGO_ID_KEYDERIVATION_PBKDF2);
+
+ Assert.assertEquals(kdf.getGeneratedSaltLength(), PBKDF2.DEFAULT_GENERATED_SALT_LENGTH);
+ Assert.assertEquals(kdf.getIterationCount(), PBKDF2.DEFAULT_ITERATION_COUNT);
+ Assert.assertNull(kdf.getKeyLength());
+ Assert.assertEquals(kdf.getPRF(), PBKDF2.DEFAULT_PRF);
+ Assert.assertNotNull(kdf.getRandom());
+ Assert.assertNull(kdf.getSalt());
+ }
+
+ @Test
+ public void explicitProperties() throws Exception {
+ SecureRandom sr = new SecureRandom();
+
+ PBKDF2 kdf = new PBKDF2();
+ kdf.setGeneratedSaltLength(16);
+ kdf.setIterationCount(3000);
+ kdf.setKeyLength(256);
+ kdf.setPRF(SignatureConstants.ALGO_ID_MAC_HMAC_SHA512);
+ kdf.setRandom(sr);
+ kdf.setSalt("ABCDEFGH");
+ kdf.initialize();
+
+ Assert.assertEquals(kdf.getAlgorithm(), EncryptionConstants.ALGO_ID_KEYDERIVATION_PBKDF2);
+
+ Assert.assertEquals(kdf.getGeneratedSaltLength().intValue(), 16);
+ Assert.assertEquals(kdf.getIterationCount().intValue(), 3000);
+ Assert.assertEquals(kdf.getKeyLength().intValue(), 256);
+ Assert.assertEquals(kdf.getPRF(), SignatureConstants.ALGO_ID_MAC_HMAC_SHA512);
+ Assert.assertSame(kdf.getRandom(), sr);
+ Assert.assertEquals(kdf.getSalt(), "ABCDEFGH");
+ }
+
+ @Test(expectedExceptions = ComponentInitializationException.class)
+ public void initBadSalt() throws Exception {
+ PBKDF2 kdf = new PBKDF2();
+ kdf.setSalt("INVALID BASE64");
+ kdf.initialize();
+ }
+
+ @Test(expectedExceptions = ComponentInitializationException.class)
+ public void initBadKeyLength() throws Exception {
+ PBKDF2 kdf = new PBKDF2();
+ kdf.setKeyLength(129);
+ kdf.initialize();
+ }
+
+ @Test(expectedExceptions = ComponentInitializationException.class)
+ public void initBadPRF() throws Exception {
+ PBKDF2 kdf = new PBKDF2();
+ kdf.setPRF(EncryptionConstants.ALGO_ID_DIGEST_SHA256);
+ kdf.initialize();
+ }
+
+ @Test
+ public void xmlGenerationSuccess() throws Exception {
+ PBKDF2 kdf = new PBKDF2();
+ kdf.setIterationCount(3000);
+ kdf.setKeyLength(256);
+ kdf.setPRF(SignatureConstants.ALGO_ID_MAC_HMAC_SHA512);
+ kdf.setSalt("ABCDEFGH");
+ kdf.initialize();
+
+ XMLObject xmlObject = kdf.buildXMLObject();
+ Assert.assertNotNull(xmlObject);
+ Assert.assertTrue(KeyDerivationMethod.class.isInstance(xmlObject));
+
+ KeyDerivationMethod kdm = KeyDerivationMethod.class.cast(xmlObject);
+ Assert.assertEquals(kdm.getAlgorithm(), EncryptionConstants.ALGO_ID_KEYDERIVATION_PBKDF2);
+ Assert.assertEquals(kdm.getUnknownXMLObjects().size(), 1);
+
+ PBKDF2Params kdmParams = PBKDF2Params.class.cast(kdm.getUnknownXMLObjects().get(0));
+
+ Assert.assertNotNull(kdmParams.getIterationCount());
+ Assert.assertEquals(kdmParams.getIterationCount().getValue().intValue(), 3000);
+
+ Assert.assertNotNull(kdmParams.getKeyLength());
+ Assert.assertEquals(kdmParams.getKeyLength().getValue().intValue(), 32); // bytes = 256/8
+
+ Assert.assertNotNull(kdmParams.getPRF());
+ Assert.assertEquals(kdmParams.getPRF().getAlgorithm(), SignatureConstants.ALGO_ID_MAC_HMAC_SHA512);
+
+ Assert.assertNotNull(kdmParams.getSalt());
+ Assert.assertNotNull(kdmParams.getSalt().getSpecified());
+ Assert.assertEquals(kdmParams.getSalt().getSpecified().getValue(), "ABCDEFGH");
+ }
+
+ @Test
+ public void cloning() throws Exception {
+ SecureRandom sr = new SecureRandom();
+
+ PBKDF2 kdf = new PBKDF2();
+ kdf.setGeneratedSaltLength(16);
+ kdf.setIterationCount(3000);
+ kdf.setKeyLength(256);
+ kdf.setPRF(SignatureConstants.ALGO_ID_MAC_HMAC_SHA512);
+ kdf.setRandom(sr);
+ kdf.setSalt("ABCDEFGH");
+ kdf.initialize();
+
+ PBKDF2 cloned = kdf.clone();
+ Assert.assertNotSame(cloned, kdf);
+
+ Assert.assertNotNull(cloned);
+ Assert.assertEquals(cloned.getGeneratedSaltLength().intValue(), 16);
+ Assert.assertEquals(cloned.getIterationCount().intValue(), 3000);
+ Assert.assertEquals(cloned.getKeyLength().intValue(), 256);
+ Assert.assertEquals(cloned.getPRF(), SignatureConstants.ALGO_ID_MAC_HMAC_SHA512);
+ Assert.assertSame(cloned.getRandom(), sr);
+ Assert.assertEquals(cloned.getSalt(), "ABCDEFGH");
+ }
+
+ @Test
+ public void deriveWithDefaults() throws Exception {
+ PBKDF2 kdf = new PBKDF2();
+ kdf.initialize();
+
+ byte[] secret = Hex.decodeHex("DEADBEEF");
+
+ SecretKey derivedKey = kdf.derive(secret, EncryptionConstants.ALGO_ID_BLOCKCIPHER_AES128_GCM);
+
+ Assert.assertNotNull(derivedKey);
+ Assert.assertEquals(derivedKey.getAlgorithm(), "AES");
+ Assert.assertEquals(derivedKey.getEncoded().length * 8, 128);
+
+ // Salt and key length were dynamically generated, so sanity check the new property values
+ Assert.assertNotNull(kdf.getSalt());
+ Assert.assertEquals(Base64Support.decode(kdf.getSalt()).length, kdf.getGeneratedSaltLength().intValue());
+
+ Assert.assertNotNull(kdf.getKeyLength());
+ Assert.assertEquals(kdf.getKeyLength().intValue(), 128);
+
+ }
+
+ @Test
+ public void deriveWithExplicitProperties() throws Exception {
+ PBKDF2 kdf = new PBKDF2();
+ kdf.setIterationCount(3000);
+ kdf.setKeyLength(256);
+ kdf.setPRF(SignatureConstants.ALGO_ID_MAC_HMAC_SHA512);
+ kdf.setSalt("ABCDEFGH");
+ kdf.initialize();
+
+ byte[] secret = Hex.decodeHex("DEADBEEF");
+
+ SecretKey derivedKey = kdf.derive(secret, EncryptionConstants.ALGO_ID_BLOCKCIPHER_AES256_GCM);
+
+ Assert.assertNotNull(derivedKey);
+ Assert.assertEquals(derivedKey.getAlgorithm(), "AES");
+ Assert.assertEquals(derivedKey.getEncoded().length * 8, 256);
+ }
+
+ @Test(expectedExceptions = KeyDerivationException.class)
+ public void deriveWithKeyLengthMismatch() throws Exception {
+ PBKDF2 kdf = new PBKDF2();
+ kdf.setKeyLength(256);
+ kdf.initialize();
+
+ byte[] secret = Hex.decodeHex("DEADBEEF");
+
+ kdf.derive(secret, EncryptionConstants.ALGO_ID_BLOCKCIPHER_AES128_GCM);
+ }
+
+ @Test(expectedExceptions = KeyDerivationException.class)
+ public void unknownKeyAlgorithm() throws Exception {
+ PBKDF2 kdf = new PBKDF2();
+ kdf.initialize();
+
+ byte[] secret = Hex.decodeHex("DEADBEEF");
+
+ kdf.derive(secret, "urn:test:InvalidKeyAlgorithm");
+ }
+
+ @Test(expectedExceptions = KeyDerivationException.class)
+ public void nonKeyLengthAlgorithm() throws Exception {
+ PBKDF2 kdf = new PBKDF2();
+ kdf.initialize();
+
+ byte[] secret = Hex.decodeHex("DEADBEEF");
+
+ // Just use this as a stand-in for something which is KeySpecifiedAlgorithm but not KeyLengthSpecifiedAlgorithm
+ kdf.derive(secret, SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
+ }
+
+
+ //
+ // Derivation tests using test vectors from external sources, and supporting code
+ //
+
+ @BeforeClass
+ public void setupTestVectorAlgorithms() {
+ AlgorithmRegistry registry = AlgorithmSupport.getGlobalAlgorithmRegistry();
+ registry.register(new MockKeyAlgorithm128());
+ registry.register(new MockKeyAlgorithm160());
+ registry.register(new MockKeyAlgorithm200());
+ registry.register(new MockKeyAlgorithm256());
+ registry.register(new MockKeyAlgorithm320());
+ }
+
+ @AfterClass
+ public void teardownTestVectorAlgorithms() {
+ AlgorithmRegistry registry = AlgorithmSupport.getGlobalAlgorithmRegistry();
+ registry.deregister(new MockKeyAlgorithm128());
+ registry.deregister(new MockKeyAlgorithm160());
+ registry.deregister(new MockKeyAlgorithm200());
+ registry.register(new MockKeyAlgorithm256());
+ registry.deregister(new MockKeyAlgorithm320());
+ }
+
+ private String whitespace(String input) {
+ return input.replaceAll("\\s", "");
+ }
+
+ @DataProvider(name = "testVectors")
+ public Object[][] testVectors() throws DecoderException, EncodingException {
+ return new Object[][] {
+ // RFC 6070: https://tools.ietf.org/html/rfc6070
+ // These are older and use SHA-1-based PRF.
+ new Object[] {
+ "password".getBytes(Charsets.UTF_8),
+ "urn:test:MockKeyAlgorithm:160",
+ SignatureConstants.ALGO_ID_MAC_HMAC_SHA1,
+ Base64Support.encode("salt".getBytes(Charsets.UTF_8), false),
+ 1,
+ Hex.decodeHex(whitespace("0c 60 c8 0f 96 1f 0e 71" +
+ "f3 a9 b5 24 af 60 12 06" +
+ "2f e0 37 a6"))},
+
+ new Object[] {
+ "password".getBytes(Charsets.UTF_8),
+ "urn:test:MockKeyAlgorithm:160",
+ SignatureConstants.ALGO_ID_MAC_HMAC_SHA1,
+ Base64Support.encode("salt".getBytes(Charsets.UTF_8), false),
+ 2,
+ Hex.decodeHex(whitespace("ea 6c 01 4d c7 2d 6f 8c" +
+ "cd 1e d9 2a ce 1d 41 f0" +
+ "d8 de 89 57"))},
+
+ new Object[] {
+ "password".getBytes(Charsets.UTF_8),
+ "urn:test:MockKeyAlgorithm:160",
+ SignatureConstants.ALGO_ID_MAC_HMAC_SHA1,
+ Base64Support.encode("salt".getBytes(Charsets.UTF_8), false),
+ 4096,
+ Hex.decodeHex(whitespace("4b 00 79 01 b7 65 48 9a" +
+ "be ad 49 d9 26 f7 21 d0" +
+ "65 a4 29 c1 "))},
+
+ /* This takes a couple of minutes, so don't run in automated tests, etc.
+ new Object[] {
+ "password".getBytes(Charsets.UTF_8),
+ "urn:test:MockKeyAlgorithm:160",
+ SignatureConstants.ALGO_ID_MAC_HMAC_SHA1,
+ Base64Support.encode("salt".getBytes(Charsets.UTF_8), false),
+ 16777216,
+ Hex.decodeHex(whitespace("ee fe 3d 61 cd 4d a4 e4" +
+ "e9 94 5b 3d 6b a2 15 8c" +
+ "26 34 e9 84 "))},
+ */
+
+ new Object[] {
+ "passwordPASSWORDpassword".getBytes(Charsets.UTF_8),
+ "urn:test:MockKeyAlgorithm:200",
+ SignatureConstants.ALGO_ID_MAC_HMAC_SHA1,
+ Base64Support.encode("saltSALTsaltSALTsaltSALTsaltSALTsalt".getBytes(Charsets.UTF_8), false),
+ 4096,
+ Hex.decodeHex(whitespace("3d 2e ec 4f e4 1c 84 9b" +
+ "80 c8 d8 36 62 c0 e4 4a" +
+ "8b 29 1a 96 4c f2 f0 70" +
+ "38"))},
+
+ new Object[] {
+ // These are testing a literal ASCII NULL (0x00) in the secret and salt
+ "pass\u0000word".getBytes(Charsets.UTF_8),
+ "urn:test:MockKeyAlgorithm:128",
+ SignatureConstants.ALGO_ID_MAC_HMAC_SHA1,
+ Base64Support.encode("sa\u0000lt".getBytes(Charsets.UTF_8), false),
+ 4096,
+ Hex.decodeHex(whitespace("56 fa 6a a7 55 48 09 9d" +
+ "cc 37 d7 f0 34 25 e0 c3"))},
+
+
+ // Non-normative vectors for SHA-2 PRF.
+ // https://stackoverflow.com/questions/5130513/pbkdf2-hmac-sha2-test-vectors/5136918#5136918
+ new Object[] {
+ "password".getBytes(Charsets.UTF_8),
+ "urn:test:MockKeyAlgorithm:256",
+ SignatureConstants.ALGO_ID_MAC_HMAC_SHA256,
+ Base64Support.encode("salt".getBytes(Charsets.UTF_8), false),
+ 1,
+ Hex.decodeHex(whitespace("12 0f b6 cf fc f8 b3 2c" +
+ "43 e7 22 52 56 c4 f8 37" +
+ "a8 65 48 c9 2c cc 35 48" +
+ "08 05 98 7c b7 0b e1 7b"))},
+
+ new Object[] {
+ "password".getBytes(Charsets.UTF_8),
+ "urn:test:MockKeyAlgorithm:256",
+ SignatureConstants.ALGO_ID_MAC_HMAC_SHA256,
+ Base64Support.encode("salt".getBytes(Charsets.UTF_8), false),
+ 2,
+ Hex.decodeHex(whitespace("ae 4d 0c 95 af 6b 46 d3" +
+ "2d 0a df f9 28 f0 6d d0" +
+ "2a 30 3f 8e f3 c2 51 df" +
+ "d6 e2 d8 5a 95 47 4c 43"))},
+
+ new Object[] {
+ "password".getBytes(Charsets.UTF_8),
+ "urn:test:MockKeyAlgorithm:256",
+ SignatureConstants.ALGO_ID_MAC_HMAC_SHA256,
+ Base64Support.encode("salt".getBytes(Charsets.UTF_8), false),
+ 4096,
+ Hex.decodeHex(whitespace("c5 e4 78 d5 92 88 c8 41" +
+ "aa 53 0d b6 84 5c 4c 8d" +
+ "96 28 93 a0 01 ce 4e 11" +
+ "a4 96 38 73 aa 98 13 4a"))},
+
+ /* This takes a couple of minutes, so don't run in automated tests, etc.
+ new Object[] {
+ "password".getBytes(Charsets.UTF_8),
+ "urn:test:MockKeyAlgorithm:256",
+ SignatureConstants.ALGO_ID_MAC_HMAC_SHA256,
+ Base64Support.encode("salt".getBytes(Charsets.UTF_8), false),
+ 16777216,
+ Hex.decodeHex(whitespace("cf 81 c6 6f e8 cf c0 4d" +
+ "1f 31 ec b6 5d ab 40 89" +
+ "f7 f1 79 e8 9b 3b 0b cb" +
+ "17 ad 10 e3 ac 6e ba 46"))},
+ */
+
+ new Object[] {
+ "passwordPASSWORDpassword".getBytes(Charsets.UTF_8),
+ "urn:test:MockKeyAlgorithm:320",
+ SignatureConstants.ALGO_ID_MAC_HMAC_SHA256,
+ Base64Support.encode("saltSALTsaltSALTsaltSALTsaltSALTsalt".getBytes(Charsets.UTF_8), false),
+ 4096,
+ Hex.decodeHex(whitespace("34 8c 89 db cb d3 2b 2f" +
+ "32 d8 14 b8 11 6e 84 cf" +
+ "2b 17 34 7e bc 18 00 18" +
+ "1c 4e 2a 1f b8 dd 53 e1" +
+ "c6 35 51 8c 7d ac 47 e9"))},
+
+ new Object[] {
+ // These are testing a literal ASCII NULL (0x00) in the secret and salt
+ "pass\u0000word".getBytes(Charsets.UTF_8),
+ "urn:test:MockKeyAlgorithm:128",
+ SignatureConstants.ALGO_ID_MAC_HMAC_SHA256,
+ Base64Support.encode("sa\u0000lt".getBytes(Charsets.UTF_8), false),
+ 4096,
+ Hex.decodeHex(whitespace("89 b6 9d 05 16 f8 29 89" +
+ "3c 69 62 26 65 0a 86 87"))},
+
+ };
+ }
+
+ @Test(dataProvider = "testVectors")
+ public void deriveTestVectors(byte[] secret, String keyAlgorithm, String prfAlgorithm, String salt, Integer iterationCount,
+ byte[] keyBytes) throws Exception {
+
+ String jcaKeyAlgorithm = AlgorithmSupport.getKeyAlgorithm(keyAlgorithm);
+ Assert.assertNotNull(jcaKeyAlgorithm);
+ Integer jcaKeyLength = AlgorithmSupport.getKeyLength(keyAlgorithm);
+ Assert.assertNotNull(jcaKeyLength);
+
+ PBKDF2 kdf = new PBKDF2();
+ kdf.setIterationCount(iterationCount);
+ kdf.setKeyLength(jcaKeyLength);
+ kdf.setPRF(prfAlgorithm);
+ kdf.setSalt(salt);
+ kdf.initialize();
+
+ SecretKey derivedKey = kdf.derive(secret, keyAlgorithm);
+
+ Assert.assertNotNull(derivedKey);
+ Assert.assertEquals(derivedKey.getAlgorithm(), jcaKeyAlgorithm);
+ Assert.assertEquals(derivedKey.getEncoded().length * 8, jcaKeyLength.intValue());
+ Assert.assertEquals(derivedKey.getEncoded(), keyBytes);
+ }
+
+ // Mock key algorithm descriptors for test vectors
+
+ private class MockKeyAlgorithm128 implements BlockEncryptionAlgorithm {
+
+ /** {@inheritDoc} */
+ public String getKey() {
+ return "MockKey";
+ }
+
+ /** {@inheritDoc} */
+ public String getURI() {
+ return "urn:test:MockKeyAlgorithm:128";
+ }
+
+ /** {@inheritDoc} */
+ public Integer getKeyLength() {
+ // 16 bytes
+ return 128;
+ }
+
+ /** {@inheritDoc} */
+ public AlgorithmType getType() {
+ return AlgorithmType.BlockEncryption;
+ }
+
+ /** {@inheritDoc} */
+ public String getJCAAlgorithmID() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public String getCipherMode() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public String getPadding() {
+ return null;
+ }
+
+ }
+
+ private class MockKeyAlgorithm160 implements BlockEncryptionAlgorithm {
+
+ /** {@inheritDoc} */
+ public String getKey() {
+ return "MockKey";
+ }
+
+ /** {@inheritDoc} */
+ public String getURI() {
+ return "urn:test:MockKeyAlgorithm:160";
+ }
+
+ /** {@inheritDoc} */
+ public Integer getKeyLength() {
+ // 20 bytes
+ return 160;
+ }
+
+ /** {@inheritDoc} */
+ public AlgorithmType getType() {
+ return AlgorithmType.BlockEncryption;
+ }
+
+ /** {@inheritDoc} */
+ public String getJCAAlgorithmID() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public String getCipherMode() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public String getPadding() {
+ return null;
+ }
+
+ }
+
+ private class MockKeyAlgorithm200 implements BlockEncryptionAlgorithm {
+
+ /** {@inheritDoc} */
+ public String getKey() {
+ return "MockKey";
+ }
+
+ /** {@inheritDoc} */
+ public String getURI() {
+ return "urn:test:MockKeyAlgorithm:200";
+ }
+
+ /** {@inheritDoc} */
+ public Integer getKeyLength() {
+ // 25 bytes
+ return 200;
+ }
+
+ /** {@inheritDoc} */
+ public AlgorithmType getType() {
+ return AlgorithmType.BlockEncryption;
+ }
+
+ /** {@inheritDoc} */
+ public String getJCAAlgorithmID() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public String getCipherMode() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public String getPadding() {
+ return null;
+ }
+
+ }
+
+ private class MockKeyAlgorithm256 implements BlockEncryptionAlgorithm {
+
+ /** {@inheritDoc} */
+ public String getKey() {
+ return "MockKey";
+ }
+
+ /** {@inheritDoc} */
+ public String getURI() {
+ return "urn:test:MockKeyAlgorithm:256";
+ }
+
+ /** {@inheritDoc} */
+ public Integer getKeyLength() {
+ // 32 bytes
+ return 256;
+ }
+
+ /** {@inheritDoc} */
+ public AlgorithmType getType() {
+ return AlgorithmType.BlockEncryption;
+ }
+
+ /** {@inheritDoc} */
+ public String getJCAAlgorithmID() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public String getCipherMode() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public String getPadding() {
+ return null;
+ }
+
+ }
+
+ private class MockKeyAlgorithm320 implements BlockEncryptionAlgorithm {
+
+ /** {@inheritDoc} */
+ public String getKey() {
+ return "MockKey";
+ }
+
+ /** {@inheritDoc} */
+ public String getURI() {
+ return "urn:test:MockKeyAlgorithm:320";
+ }
+
+ /** {@inheritDoc} */
+ public Integer getKeyLength() {
+ // 40 bytes
+ return 320;
+ }
+
+ /** {@inheritDoc} */
+ public AlgorithmType getType() {
+ return AlgorithmType.BlockEncryption;
+ }
+
+ /** {@inheritDoc} */
+ public String getJCAAlgorithmID() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public String getCipherMode() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public String getPadding() {
+ return null;
+ }
+
+ }
+
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list