[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