Selaa lähdekoodia

add pwm-cr library

jrivard@gmail.com 6 vuotta sitten
vanhempi
commit
9b09aff44d
25 muutettua tiedostoa jossa 1855 lisäystä ja 6 poistoa
  1. 1 0
      pom.xml
  2. 174 0
      pwm-cr/pom.xml
  3. 415 0
      pwm-cr/src/main/java/password/pwm/cr/ChaiXmlResponseSetSerializer.java
  4. 27 0
      pwm-cr/src/main/java/password/pwm/cr/JsonStoredResponseSerializer.java
  5. 56 0
      pwm-cr/src/main/java/password/pwm/cr/StoredItemUtils.java
  6. 91 0
      pwm-cr/src/main/java/password/pwm/cr/api/ChallengeItemPolicy.java
  7. 46 0
      pwm-cr/src/main/java/password/pwm/cr/api/ChallengeSetPolicy.java
  8. 29 0
      pwm-cr/src/main/java/password/pwm/cr/api/QuestionSource.java
  9. 29 0
      pwm-cr/src/main/java/password/pwm/cr/api/ResponseLevel.java
  10. 38 0
      pwm-cr/src/main/java/password/pwm/cr/api/StoredChallengeItem.java
  11. 40 0
      pwm-cr/src/main/java/password/pwm/cr/api/StoredResponseItem.java
  12. 42 0
      pwm-cr/src/main/java/password/pwm/cr/api/StoredResponseSet.java
  13. 53 0
      pwm-cr/src/main/java/password/pwm/cr/hash/AbstractHashMachine.java
  14. 76 0
      pwm-cr/src/main/java/password/pwm/cr/hash/HashFactory.java
  15. 46 0
      pwm-cr/src/main/java/password/pwm/cr/hash/HashParameter.java
  16. 126 0
      pwm-cr/src/main/java/password/pwm/cr/hash/PBKDF2HashMachine.java
  17. 50 0
      pwm-cr/src/main/java/password/pwm/cr/hash/ResponseHashAlgorithm.java
  18. 32 0
      pwm-cr/src/main/java/password/pwm/cr/hash/ResponseHashMachine.java
  19. 28 0
      pwm-cr/src/main/java/password/pwm/cr/hash/ResponseHashMachineSpi.java
  20. 67 0
      pwm-cr/src/main/java/password/pwm/cr/hash/TextHashMachine.java
  21. 144 0
      pwm-cr/src/main/java/password/pwm/cr/hash/TypicalHashMachine.java
  22. 139 0
      pwm-cr/src/test/java/password/pwm/cr/ChaiXmlResponseSet1Test.java
  23. 53 0
      pwm-cr/src/test/java/password/pwm/cr/ChaiXmlResponseSetReaderTest.java
  24. 47 0
      pwm-cr/src/test/resources/password/pwm/cr/ChaiXmlResponseSet1.xml
  25. 6 6
      webapp/pom.xml

+ 1 - 0
pom.xml

@@ -36,6 +36,7 @@
 
     <modules>
         <module>client</module>
+        <module>pwm-cr</module>
         <module>server</module>
         <module>webapp</module>
         <module>onejar</module>

+ 174 - 0
pwm-cr/pom.xml

@@ -0,0 +1,174 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+    <parent>
+        <groupId>org.pwm-project</groupId>
+        <artifactId>pwm-parent</artifactId>
+        <version>1.8.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>pwm-cr</artifactId>
+    <packaging>jar</packaging>
+
+    <name>PWM Password Self Service: Challenge/Response JAR</name>
+
+    <description>Library for managing challenge/response security policies, stored data, and validation</description>
+
+    <properties>
+        <project.root.basedir>${project.basedir}/..</project.root.basedir>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+            <version>2.8.5</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jdom</groupId>
+            <artifactId>jdom2</artifactId>
+            <version>2.0.6</version>
+        </dependency>
+        <dependency>
+            <groupId>log4j</groupId>
+            <artifactId>log4j</artifactId>
+            <version>1.2.17</version>
+        </dependency>
+        <dependency>
+            <groupId>net.iharder</groupId>
+            <artifactId>base64</artifactId>
+            <version>2.3.9</version>
+        </dependency>
+        <dependency>
+            <groupId>org.bouncycastle</groupId>
+            <artifactId>bcpkix-jdk15on</artifactId>
+            <version>1.60</version>
+        </dependency>
+
+        <!-- Test dependencies -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.12</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+                <filtering>true</filtering>
+            </resource>
+            <resource>
+                <directory>src/main/java</directory> <!-- include the src in the main output jar -->
+                <targetPath>src</targetPath>
+            </resource>
+        </resources>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <version>3.1.0</version>
+                <configuration>
+                    <archive>
+                        <manifestEntries>
+                            <Implementation-Archive-Name>pwm.jar</Implementation-Archive-Name>
+                            <Implementation-Title>${project.name}</Implementation-Title>
+                            <Implementation-Version>${project.version}</Implementation-Version>
+                            <Implementation-Vendor>${project.organization.name}</Implementation-Vendor>
+                            <Implementation-URL>${project.organization.url}</Implementation-URL>
+                            <Implementation-Build-Java-Vendor>${java.vendor}</Implementation-Build-Java-Vendor>
+                            <Implementation-Build-Java-Version>${java.version}</Implementation-Build-Java-Version>
+                            <Implementation-Build>${build.number}</Implementation-Build>
+                            <Implementation-Build-Timestamp>${timestamp.iso}</Implementation-Build-Timestamp>
+                            <Implementation-Revision>${build.revision}</Implementation-Revision>
+                            <Implementation-Version-Display>v${project.version} b${build.number} r${build.revision}</Implementation-Version-Display>
+                        </manifestEntries>
+                    </archive>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-source-plugin</artifactId>
+                <version>3.0.1</version>
+                <configuration>
+                    <includePom>true</includePom>
+                    <archive>
+                        <manifestEntries>
+                            <Implementation-Archive-Name>pwm.source</Implementation-Archive-Name>
+                            <Implementation-Title>${project.name}</Implementation-Title>
+                            <Implementation-Version>${project.version}</Implementation-Version>
+                            <Implementation-Vendor>${project.organization.name}</Implementation-Vendor>
+                            <Implementation-URL>${project.organization.url}</Implementation-URL>
+                            <Implementation-Build-Java-Vendor>${java.vendor}</Implementation-Build-Java-Vendor>
+                            <Implementation-Build-Java-Version>${java.version}</Implementation-Build-Java-Version>
+                            <Implementation-Build>${build.number}</Implementation-Build>
+                            <Implementation-Build-Timestamp>${timestamp.iso}</Implementation-Build-Timestamp>
+                            <Implementation-Revision>${build.revision}</Implementation-Revision>
+                            <Implementation-Version-Display>v${project.version} b${build.number} r${build.revision}</Implementation-Version-Display>
+                        </manifestEntries>
+                    </archive>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <version>3.0.1</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>jar</goal>
+                        </goals>
+                        <configuration>
+                            <archive>
+                                <manifestEntries>
+                                    <Implementation-Archive-Name>pwm.javadoc</Implementation-Archive-Name>
+                                    <Implementation-Title>${project.name}</Implementation-Title>
+                                    <Implementation-Version>${project.version}</Implementation-Version>
+                                    <Implementation-Vendor>${project.organization.name}</Implementation-Vendor>
+                                    <Implementation-URL>${project.organization.url}</Implementation-URL>
+                                    <Implementation-Build-Java-Vendor>${java.vendor}</Implementation-Build-Java-Vendor>
+                                    <Implementation-Build-Java-Version>${java.version}</Implementation-Build-Java-Version>
+                                    <Implementation-Build>${build.number}</Implementation-Build>
+                                    <Implementation-Build-Timestamp>${timestamp.iso}</Implementation-Build-Timestamp>
+                                    <Implementation-Revision>${build.revision}</Implementation-Revision>
+                                    <Implementation-Version-Display>v${project.version} b${build.number} r${build.revision}</Implementation-Version-Display>
+                                </manifestEntries>
+                            </archive>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+    <profiles>
+    </profiles>
+
+    <distributionManagement>
+        <snapshotRepository>
+            <id>ossrh</id>
+            <url>https://oss.sonatype.org/content/repositories/snapshots</url>
+        </snapshotRepository>
+    </distributionManagement>
+
+    <developers>
+        <developer>
+            <name>Jason Rivard</name>
+            <email>https://github.com/jrivard</email>
+            <organization>LDAP Chai</organization>
+            <organizationUrl>https://github.com/ldapchai/</organizationUrl>
+        </developer>
+    </developers>
+
+    <scm>
+        <connection>scm:git:git@github.com:ldapchai/chaiCR.git</connection>
+        <developerConnection>scm:git:git@github.com:ldapchai/chaiCR.git</developerConnection>
+        <url>git@github.com:ldapchai/chaiCR.git</url>
+    </scm>
+
+</project>

+ 415 - 0
pwm-cr/src/main/java/password/pwm/cr/ChaiXmlResponseSetSerializer.java

@@ -0,0 +1,415 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.cr;
+
+import net.iharder.Base64;
+import org.jdom2.Attribute;
+import org.jdom2.DataConversionException;
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.JDOMException;
+import org.jdom2.Text;
+import org.jdom2.input.SAXBuilder;
+import org.jdom2.output.Format;
+import org.jdom2.output.XMLOutputter;
+import password.pwm.cr.api.StoredChallengeItem;
+import password.pwm.cr.api.StoredResponseItem;
+import password.pwm.cr.api.StoredResponseSet;
+import password.pwm.cr.api.ResponseLevel;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.text.ParseException;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+public class ChaiXmlResponseSetSerializer
+{
+
+    public enum Type
+    {
+        USER,
+        HELPDESK,
+    }
+
+    static final String SALT_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+
+    static final String XML_NODE_ROOT = "ResponseSet";
+    static final String XML_ATTRIBUTE_MIN_RANDOM_REQUIRED = "minRandomRequired";
+    static final String XML_ATTRIBUTE_LOCALE = "locale";
+
+
+    static final String XML_NODE_RESPONSE = "response";
+    static final String XML_NODE_HELPDESK_RESPONSE = "helpdesk-response";
+    static final String XML_NODE_CHALLENGE = "challenge";
+    static final String XML_NODE_ANSWER_VALUE = "answer";
+
+    static final String XML_ATTRIBUTE_VERSION = "version";
+    static final String XML_ATTRIBUTE_CHAI_VERSION = "chaiVersion";
+    static final String XML_ATTRIBUTE_ADMIN_DEFINED = "adminDefined";
+    static final String XML_ATTRIBUTE_REQUIRED = "required";
+    static final String XML_ATTRIBUTE_HASH_COUNT = "hashcount";
+    static final String XML_ATTRIBUTE_CONTENT_FORMAT = "format";
+    static final String XML_ATTRIBUTE_SALT = "salt";
+    static final String XNL_ATTRIBUTE_MIN_LENGTH = "minLength";
+    static final String XNL_ATTRIBUTE_MAX_LENGTH = "maxLength";
+    static final String XML_ATTRIBUTE_CASE_INSENSITIVE = "caseInsensitive";
+
+    // identifier from challenge set.
+    static final String XML_ATTRIBUTE_CHALLENGE_SET_IDENTIFER = "challengeSetID";
+    static final String XML_ATTRIBUTE_TIMESTAMP = "time";
+
+    static final String VALUE_VERSION = "pwmCR-1";
+
+
+    public StoredResponseSet read( final Reader input, final Type type )
+    {
+        final Map<Type, StoredResponseSet> values = read( input );
+        return values.get( type );
+    }
+
+    public Map<Type, StoredResponseSet> read( final Reader input )
+    {
+        if ( input == null )
+        {
+            throw new NullPointerException( "input can not be null" );
+        }
+        final List<StoredChallengeItem> crMap = new ArrayList<>();
+        final List<StoredChallengeItem> helpdeskCrMap = new ArrayList<>();
+        final int minRandRequired;
+        final Attribute localeAttr;
+        boolean caseInsensitive = false;
+        String csIdentifier = null;
+        Instant timestamp = null;
+
+        try
+        {
+            final SAXBuilder builder = new SAXBuilder();
+            final Document doc = builder.build( input );
+            final Element rootElement = doc.getRootElement();
+            minRandRequired = rootElement.getAttribute( XML_ATTRIBUTE_MIN_RANDOM_REQUIRED ).getIntValue();
+            localeAttr = rootElement.getAttribute( XML_ATTRIBUTE_LOCALE );
+
+            {
+                final Attribute caseAttr = rootElement.getAttribute( XML_ATTRIBUTE_CASE_INSENSITIVE );
+                if ( caseAttr != null && caseAttr.getBooleanValue() )
+                {
+                    caseInsensitive = true;
+                }
+            }
+
+            {
+                final Attribute csIdentiferAttr = rootElement.getAttribute( XML_ATTRIBUTE_CHALLENGE_SET_IDENTIFER );
+                if ( csIdentiferAttr != null )
+                {
+                    csIdentifier = csIdentiferAttr.getValue();
+                }
+            }
+
+            {
+                final Attribute timeAttr = rootElement.getAttribute( XML_ATTRIBUTE_TIMESTAMP );
+                if ( timeAttr != null )
+                {
+                    final String timeStr = timeAttr.getValue();
+                    try
+                    {
+                        timestamp = CrUtils.parseDateString( timeStr );
+                    }
+                    catch ( ParseException e )
+                    {
+                        throw new IllegalArgumentException( "unexpected error attempting to parse timestamp: " + e.getMessage() );
+                    }
+                }
+            }
+
+            for ( final Element loopResponseElement : rootElement.getChildren() )
+            {
+                final Type type = XML_NODE_HELPDESK_RESPONSE.equals( loopResponseElement.getName() )
+                        ? Type.HELPDESK
+                        : XML_NODE_RESPONSE.equals( loopResponseElement.getName() )
+                        ? Type.USER
+                        : null;
+                if ( type != null )
+                {
+                    final StoredResponseItem storedResponseItem = parseAnswerElement( loopResponseElement.getChild( XML_NODE_ANSWER_VALUE ) );
+                    if ( storedResponseItem != null )
+                    {
+                        final StoredChallengeItem storedChallengeItem = parseResponseElement( loopResponseElement, storedResponseItem );
+                        switch ( type )
+                        {
+                            case USER:
+                                crMap.add( storedChallengeItem );
+                                break;
+
+                            case HELPDESK:
+                                helpdeskCrMap.add( storedChallengeItem );
+                                break;
+
+                            default:
+                                throw new IllegalStateException( "unknown response type '" + type + "'" );
+
+                        }
+                    }
+                }
+            }
+        }
+        catch ( JDOMException | IOException | NullPointerException e )
+        {
+            throw new IllegalArgumentException( "error parsing stored response record: " + e.getMessage() );
+        }
+
+        final String strLocale = localeAttr != null ? localeAttr.getValue() : null;
+
+
+        final Map<Type, StoredResponseSet> returnMap = new HashMap<>();
+        {
+            final StoredResponseSet userResponseSet = StoredResponseSet.builder()
+                    .id( csIdentifier )
+                    .caseSensitive( !caseInsensitive )
+                    .minRandomsDuringResponse( minRandRequired )
+                    .storedChallengeItems( Collections.unmodifiableList( crMap ) )
+                    .locale( strLocale )
+                    .timestamp( timestamp )
+                    .build();
+            returnMap.put( Type.USER, userResponseSet );
+        }
+
+        {
+            final StoredResponseSet helpdeskStoredResponseSet = StoredResponseSet.builder()
+                    .id( csIdentifier )
+                    .caseSensitive( !caseInsensitive )
+                    .minRandomsDuringResponse( minRandRequired )
+                    .storedChallengeItems( Collections.unmodifiableList( helpdeskCrMap ) )
+                    .locale( strLocale )
+                    .timestamp( timestamp )
+                    .build();
+            returnMap.put( Type.HELPDESK, helpdeskStoredResponseSet );
+        }
+
+
+        return Collections.unmodifiableMap( returnMap );
+    }
+
+    private static String elementNameForType( final Type type )
+    {
+        switch ( type )
+        {
+            case USER:
+                return XML_NODE_RESPONSE;
+
+            case HELPDESK:
+                return XML_NODE_HELPDESK_RESPONSE;
+
+            default:
+                throw new IllegalArgumentException( "unknown type '" + type + "'" );
+        }
+    }
+
+    private static StoredChallengeItem parseResponseElement(
+            final Element responseElement,
+            final StoredResponseItem storedResponseItem
+    )
+
+            throws DataConversionException
+    {
+        /*
+        final boolean adminDefined = responseElement.getAttribute( XML_ATTRIBUTE_ADMIN_DEFINED ) != null
+                && responseElement.getAttribute( XML_ATTRIBUTE_ADMIN_DEFINED ).getBooleanValue();
+
+        final int minLength = responseElement.getAttribute( XNL_ATTRIBUTE_MIN_LENGTH ) == null
+                ? 0
+                : responseElement.getAttribute( XNL_ATTRIBUTE_MIN_LENGTH ).getIntValue();
+
+        final int maxLength = responseElement.getAttribute( XNL_ATTRIBUTE_MAX_LENGTH ) == null
+                ? 0
+                : responseElement.getAttribute( XNL_ATTRIBUTE_MAX_LENGTH ).getIntValue();
+
+                */
+
+        final boolean required = responseElement.getAttribute( XML_ATTRIBUTE_REQUIRED ) != null
+                && responseElement.getAttribute( XML_ATTRIBUTE_REQUIRED ).getBooleanValue();
+
+        final String challengeText = responseElement.getChild( XML_NODE_CHALLENGE ) == null
+                ? ""
+                : responseElement.getChild( XML_NODE_CHALLENGE ).getText();
+
+        return StoredChallengeItem.builder()
+                .responseLevel( required ? ResponseLevel.REQUIRED : ResponseLevel.RANDOM )
+                .questionText( challengeText )
+                .id( makeId( challengeText ) )
+                .answer( storedResponseItem )
+                .build();
+    }
+
+    private static StoredResponseItem parseAnswerElement( final Element element )
+    {
+        final String answerValue = element.getText();
+        final String salt = element.getAttribute( XML_ATTRIBUTE_SALT ) == null ? "" : element.getAttribute( XML_ATTRIBUTE_SALT ).getValue();
+        final String hashCount = element.getAttribute( XML_ATTRIBUTE_HASH_COUNT ) == null ? "1" : element.getAttribute( XML_ATTRIBUTE_HASH_COUNT ).getValue();
+        int saltCount = 1;
+        try
+        {
+            saltCount = Integer.parseInt( hashCount );
+        }
+        catch ( NumberFormatException e )
+        { /* noop */ }
+        final String formatStr = element.getAttributeValue( XML_ATTRIBUTE_CONTENT_FORMAT ) == null ? "" : element.getAttributeValue( XML_ATTRIBUTE_CONTENT_FORMAT );
+
+        return StoredResponseItem.builder()
+                .format( formatStr )
+                .salt( salt )
+                .hash( answerValue )
+                .iterations( saltCount )
+                .build();
+    }
+
+    private static String makeId(
+            final String questionText
+    )
+            throws IllegalStateException
+    {
+        final MessageDigest md;
+        try
+        {
+            md = MessageDigest.getInstance( "SHA1" );
+            final byte[] hashedBytes = md.digest( questionText.getBytes( StandardCharsets.UTF_8 ) );
+            return net.iharder.Base64.encodeBytes( hashedBytes, Base64.URL_SAFE );
+        }
+        catch ( NoSuchAlgorithmException | IOException e )
+        {
+            throw new IllegalStateException( "unable to load SHA1 message digest algorithm: " + e.getMessage() );
+        }
+    }
+
+
+    public void write( final Writer writer, final Map<Type, StoredResponseSet> responseSets ) throws IOException
+    {
+        final StoredResponseSet rs = responseSets.get( Type.USER );
+        if ( rs == null )
+        {
+            throw new IllegalArgumentException( "responseSet must contain user type responses" );
+        }
+
+        final Element rootElement = new Element( XML_NODE_ROOT );
+        rootElement.setAttribute( XML_ATTRIBUTE_MIN_RANDOM_REQUIRED, String.valueOf( rs.getMinRandomsDuringResponse() ) );
+        rootElement.setAttribute( XML_ATTRIBUTE_LOCALE, rs.getLocale().toString() );
+        rootElement.setAttribute( XML_ATTRIBUTE_VERSION, VALUE_VERSION );
+        rootElement.setAttribute( XML_ATTRIBUTE_CHAI_VERSION, VALUE_VERSION );
+
+        if ( !rs.isCaseSensitive() )
+        {
+            rootElement.setAttribute( XML_ATTRIBUTE_CASE_INSENSITIVE, "true" );
+        }
+
+        if ( rs.getId() != null )
+        {
+            rootElement.setAttribute( XML_ATTRIBUTE_CHALLENGE_SET_IDENTIFER, rs.getId() );
+        }
+
+        if ( rs.getTimestamp() != null )
+        {
+            rootElement.setAttribute( XML_ATTRIBUTE_TIMESTAMP, CrUtils.formatDateString( rs.getTimestamp() ) );
+        }
+
+        attachChallenges( rootElement, rs.getStoredChallengeItems(), Type.USER );
+        if ( responseSets.containsKey( Type.HELPDESK ) )
+        {
+            final List<StoredChallengeItem> helpdeskChallengeItems = responseSets.get( Type.HELPDESK ).getStoredChallengeItems();
+            attachChallenges( rootElement, helpdeskChallengeItems, Type.HELPDESK );
+        }
+
+
+        final Document doc = new Document( rootElement );
+        final XMLOutputter outputter = new XMLOutputter();
+        final Format format = Format.getRawFormat();
+        format.setTextMode( Format.TextMode.PRESERVE );
+        format.setLineSeparator( "" );
+        outputter.setFormat( format );
+        outputter.output( doc, writer );
+    }
+
+    private static void attachChallenges(
+            final Element parentElement,
+            final List<StoredChallengeItem> storedChallengeItems,
+            final Type type
+    )
+    {
+        if ( storedChallengeItems == null )
+        {
+            return;
+        }
+
+        if ( storedChallengeItems != null )
+        {
+            for ( final StoredChallengeItem storedChallengeItem : storedChallengeItems )
+            {
+                final StoredResponseItem storedResponseItem = storedChallengeItem.getAnswer();
+                final String responseElementName = elementNameForType( type );
+                final Element responseElement = challengeToXml( storedChallengeItem, storedResponseItem, responseElementName );
+                parentElement.addContent( responseElement );
+            }
+        }
+
+    }
+
+    private static Element challengeToXml(
+            final StoredChallengeItem loopChallenge,
+            final StoredResponseItem answer,
+            final String elementName
+    )
+    {
+        final Element responseElement = new Element( elementName );
+        responseElement.addContent( new Element( XML_NODE_CHALLENGE ).addContent( new Text( loopChallenge.getQuestionText() ) ) );
+        final Element answerElement = answerToXml( loopChallenge.getAnswer() );
+        responseElement.addContent( answerElement );
+        responseElement.setAttribute( XML_ATTRIBUTE_REQUIRED, Boolean.toString( loopChallenge.getResponseLevel() == ResponseLevel.REQUIRED ) );
+        return responseElement;
+    }
+
+    private static Element answerToXml( final StoredResponseItem storedResponseItem )
+    {
+        final Element answerElement = new Element( XML_NODE_ANSWER_VALUE );
+        answerElement.setText( storedResponseItem.getHash() );
+        if ( storedResponseItem.getSalt() != null && !storedResponseItem.getSalt().isEmpty() )
+        {
+            answerElement.setAttribute( XML_ATTRIBUTE_SALT, storedResponseItem.getSalt() );
+        }
+        answerElement.setAttribute( XML_ATTRIBUTE_CONTENT_FORMAT, storedResponseItem.getFormat() );
+        if ( storedResponseItem.getIterations() > 1 )
+        {
+            answerElement.setAttribute( XML_ATTRIBUTE_HASH_COUNT, String.valueOf( storedResponseItem.getIterations() ) );
+        }
+        return answerElement;
+    }
+
+
+}

+ 27 - 0
pwm-cr/src/main/java/password/pwm/cr/JsonStoredResponseSerializer.java

@@ -0,0 +1,27 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.cr;
+
+public class JsonStoredResponseSerializer
+{
+}

+ 56 - 0
pwm-cr/src/main/java/password/pwm/cr/StoredItemUtils.java

@@ -0,0 +1,56 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.cr;
+
+import password.pwm.cr.api.ResponseLevel;
+import password.pwm.cr.api.StoredChallengeItem;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public final class StoredItemUtils
+{
+
+    private StoredItemUtils( )
+    {
+    }
+
+    public static List<StoredChallengeItem> filterStoredChallenges(
+            final List<StoredChallengeItem> input,
+            final ResponseLevel responseLevel )
+    {
+        final List<StoredChallengeItem> returnList = new ArrayList<>();
+        if ( input != null )
+        {
+            for ( final StoredChallengeItem storedChallengeItem : input )
+            {
+                if ( storedChallengeItem.getResponseLevel() == responseLevel )
+                {
+                    returnList.add( storedChallengeItem );
+                }
+            }
+        }
+        return Collections.unmodifiableList( returnList );
+    }
+}

+ 91 - 0
pwm-cr/src/main/java/password/pwm/cr/api/ChallengeItemPolicy.java

@@ -0,0 +1,91 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.cr.api;
+
+import lombok.Builder;
+import lombok.Value;
+
+import java.io.Serializable;
+
+@Builder
+@Value
+public class ChallengeItemPolicy implements Serializable
+{
+    @Builder.Default
+    private final String questionText = "";
+
+    @Builder.Default
+    private int minLength = 1;
+
+    @Builder.Default
+    private int maxLength = 255;
+
+    @Builder.Default
+    private int maxQuestionCharsInAnswer = 0;
+
+    @Builder.Default
+    private boolean enforceWordList = false;
+
+    @Builder.Default
+    private QuestionSource questionSource = QuestionSource.ADMIN_DEFINED;
+
+    @Builder.Default
+    private ResponseLevel responseLevel = ResponseLevel.REQUIRED;
+
+    public void validate( ) throws IllegalArgumentException
+    {
+        if ( questionSource == null )
+        {
+            throw new IllegalArgumentException( "questionSource can not be null" );
+        }
+
+        if ( responseLevel == null )
+        {
+            throw new IllegalArgumentException( "responseLevel can not be null" );
+        }
+
+        if ( questionText == null || questionText.isEmpty() )
+        {
+            if ( questionSource == QuestionSource.ADMIN_DEFINED )
+            {
+                throw new IllegalArgumentException( "questionText is required when questionSource is "
+                        + QuestionSource.ADMIN_DEFINED.toString() );
+            }
+        }
+
+        if ( minLength < 1 )
+        {
+            throw new IllegalArgumentException( "minLength must be greater than zero" );
+        }
+
+        if ( maxLength < 1 )
+        {
+            throw new IllegalArgumentException( "maxLength must be greater than zero" );
+        }
+
+        if ( maxQuestionCharsInAnswer < 0 )
+        {
+            throw new IllegalArgumentException( "maxQuestionCharsInAnswer must be zero or greater" );
+        }
+    }
+}

+ 46 - 0
pwm-cr/src/main/java/password/pwm/cr/api/ChallengeSetPolicy.java

@@ -0,0 +1,46 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.cr.api;
+
+import lombok.Builder;
+import lombok.Value;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Value
+@Builder
+public class ChallengeSetPolicy implements Serializable
+{
+    private String id;
+
+    private String locale;
+
+    private List<ChallengeItemPolicy> challengeItemPolicies;
+
+    private int minRandomsDuringResponse;
+
+    private int minRandomsDuringSetup;
+
+    private boolean caseSensitive;
+}

+ 29 - 0
pwm-cr/src/main/java/password/pwm/cr/api/QuestionSource.java

@@ -0,0 +1,29 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.cr.api;
+
+public enum QuestionSource
+{
+    ADMIN_DEFINED,
+    USER_DEFINED,
+}

+ 29 - 0
pwm-cr/src/main/java/password/pwm/cr/api/ResponseLevel.java

@@ -0,0 +1,29 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.cr.api;
+
+public enum ResponseLevel
+{
+    REQUIRED,
+    RANDOM,
+}

+ 38 - 0
pwm-cr/src/main/java/password/pwm/cr/api/StoredChallengeItem.java

@@ -0,0 +1,38 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.cr.api;
+
+import lombok.Builder;
+import lombok.Value;
+
+import java.io.Serializable;
+
+@Value
+@Builder
+public class StoredChallengeItem implements Serializable
+{
+    private String id;
+    private String questionText;
+    private ResponseLevel responseLevel;
+    private StoredResponseItem answer;
+}

+ 40 - 0
pwm-cr/src/main/java/password/pwm/cr/api/StoredResponseItem.java

@@ -0,0 +1,40 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.cr.api;
+
+import lombok.Builder;
+import lombok.Value;
+
+import java.io.Serializable;
+
+@Value
+@Builder
+public class StoredResponseItem implements Serializable
+{
+    private String format;
+    private String hash;
+    private String salt;
+
+    /** Number of hash iterations. */
+    private final int iterations;
+}

+ 42 - 0
pwm-cr/src/main/java/password/pwm/cr/api/StoredResponseSet.java

@@ -0,0 +1,42 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.cr.api;
+
+import lombok.Builder;
+import lombok.Value;
+
+import java.io.Serializable;
+import java.time.Instant;
+import java.util.List;
+
+@Value
+@Builder
+public class StoredResponseSet implements Serializable
+{
+    private String id;
+    private String locale;
+    private List<StoredChallengeItem> storedChallengeItems;
+    private int minRandomsDuringResponse;
+    private boolean caseSensitive;
+    private Instant timestamp;
+}

+ 53 - 0
pwm-cr/src/main/java/password/pwm/cr/hash/AbstractHashMachine.java

@@ -0,0 +1,53 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.cr.hash;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public abstract class AbstractHashMachine implements ResponseHashMachineSpi
+{
+    private final Map<String, String> parameters = new HashMap<>();
+
+    public void init( final Map<String, String> parameters )
+    {
+
+    }
+
+    public Map<String, String> defaultParameters( )
+    {
+        return null;
+    }
+
+    Map<String, String> effectiveParameters( )
+    {
+        return Collections.unmodifiableMap( parameters );
+    }
+
+    protected boolean isCaseSensative( )
+    {
+        return effectiveParameters().containsKey( HashParameter.caseSensitive.toString() )
+                && Boolean.parseBoolean( effectiveParameters().get( HashParameter.caseSensitive.toString() ) );
+    }
+}

+ 76 - 0
pwm-cr/src/main/java/password/pwm/cr/hash/HashFactory.java

@@ -0,0 +1,76 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.cr.hash;
+
+import password.pwm.cr.api.StoredResponseItem;
+
+import java.util.Map;
+
+public class HashFactory
+{
+    StoredResponseItem responseItemForRawValue(
+            final String response,
+            final ResponseHashAlgorithm responseHashAlgorithm,
+            final Map<HashParameter, String> parameters
+
+    )
+    {
+        return null;
+    }
+
+    public static boolean testResponseItem(
+            final StoredResponseItem storedResponseItem,
+            final String answer
+    )
+    {
+        final ResponseHashMachine responseHashMachine = machineForStoredResponse( storedResponseItem );
+        return responseHashMachine.test( storedResponseItem, answer );
+    }
+
+
+    private static ResponseHashMachine machineForStoredResponse( final StoredResponseItem storedResponseItem )
+    {
+        final String algName = storedResponseItem.getFormat();
+        final ResponseHashAlgorithm alg;
+        try
+        {
+            alg = ResponseHashAlgorithm.valueOf( algName );
+        }
+        catch ( IllegalArgumentException e )
+        {
+            throw new IllegalArgumentException( "unknown format type '" + algName + "'" );
+        }
+        final Class algClass = alg.getImplementingClass();
+        final ResponseHashMachineSpi responseHashMachine;
+        try
+        {
+            responseHashMachine = ( ResponseHashMachineSpi ) algClass.newInstance();
+        }
+        catch ( Exception e )
+        {
+            throw new IllegalStateException( "unexpected error instantiating response hash machine spi class: " + e.getMessage() );
+        }
+        responseHashMachine.init( alg );
+        return responseHashMachine;
+    }
+}

+ 46 - 0
pwm-cr/src/main/java/password/pwm/cr/hash/HashParameter.java

@@ -0,0 +1,46 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.cr.hash;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public enum HashParameter
+{
+    iterations,
+    outputLength,
+    saltLength,
+    caseSensitive,;
+
+    static Map<String, String> untypedParamMap( final Map<HashParameter, String> parameters )
+    {
+        final Map<String, String> returnMap = new HashMap<>();
+        for ( final Map.Entry<HashParameter, String> entry : parameters.entrySet() )
+        {
+            final HashParameter key = entry.getKey();
+            final String value = entry.getValue();
+            returnMap.put( key.toString(), value );
+        }
+        return returnMap;
+    }
+}

+ 126 - 0
pwm-cr/src/main/java/password/pwm/cr/hash/PBKDF2HashMachine.java

@@ -0,0 +1,126 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.cr.hash;
+
+import net.iharder.Base64;
+import password.pwm.cr.api.StoredResponseItem;
+
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+class PBKDF2HashMachine extends AbstractHashMachine implements ResponseHashMachineSpi
+{
+
+    private ResponseHashAlgorithm responseHashAlgorithm;
+
+    PBKDF2HashMachine( )
+    {
+    }
+
+    public void init( final ResponseHashAlgorithm responseHashAlgorithm )
+    {
+        this.responseHashAlgorithm = responseHashAlgorithm;
+        switch ( responseHashAlgorithm )
+        {
+            case PBKDF2:
+            case PBKDF2_SHA256:
+            case PBKDF2_SHA512:
+                break;
+
+            default:
+                throw new IllegalArgumentException( "implementation does not support hash algorithm " + responseHashAlgorithm );
+        }
+    }
+
+    @Override
+    public Map<String, String> defaultParameters( )
+    {
+        final Map<String, String> map = new HashMap<>();
+        map.put( HashParameter.caseSensitive.toString(), String.valueOf( false ) );
+        return Collections.unmodifiableMap( map );
+    }
+
+    @Override
+    public StoredResponseItem generate( final String input )
+    {
+        //@todo
+        return null;
+    }
+
+    @Override
+    public boolean test( final StoredResponseItem hashedResponse, final String input )
+    {
+        final String newHash = hashValue( input, hashedResponse.getIterations(), hashedResponse.getSalt() );
+        return newHash.equals( hashedResponse.getHash() );
+    }
+
+    private String hashValue( final String input, final int iterations, final String salt )
+    {
+        try
+        {
+            final PBEKeySpec spec;
+            final SecretKeyFactory skf;
+            {
+                final String methodName;
+                final int keyLength;
+                switch ( responseHashAlgorithm )
+                {
+                    case PBKDF2:
+                        methodName = "PBKDF2WithHmacSHA1";
+                        keyLength = 64 * 8;
+                        break;
+
+                    case PBKDF2_SHA256:
+                        methodName = "PBKDF2WithHmacSHA256";
+                        keyLength = 128 * 8;
+                        break;
+
+                    case PBKDF2_SHA512:
+                        methodName = "PBKDF2WithHmacSHA512";
+                        keyLength = 192 * 8;
+                        break;
+
+                    default:
+                        throw new IllegalStateException( "formatType not supported: " + responseHashAlgorithm.toString() );
+
+                }
+
+                final char[] chars = input.toCharArray();
+                final byte[] saltBytes = salt.getBytes( "UTF-8" );
+
+                spec = new PBEKeySpec( chars, saltBytes, iterations, keyLength );
+                skf = SecretKeyFactory.getInstance( methodName );
+            }
+            final byte[] hash = skf.generateSecret( spec ).getEncoded();
+            return Base64.encodeBytes( hash );
+        }
+        catch ( Exception e )
+        {
+            throw new IllegalStateException( "unable to perform PBKDF2 hashing operation: " + e.getMessage() );
+        }
+    }
+
+}

+ 50 - 0
pwm-cr/src/main/java/password/pwm/cr/hash/ResponseHashAlgorithm.java

@@ -0,0 +1,50 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.cr.hash;
+
+public enum ResponseHashAlgorithm
+{
+    TEXT( TextHashMachine.class ),
+    MD5( TypicalHashMachine.class ),
+    SHA1( TypicalHashMachine.class ),
+    SHA1_SALT( TypicalHashMachine.class ),
+    SHA256_SALT( TypicalHashMachine.class ),
+    SHA512_SALT( TypicalHashMachine.class ),
+    //    BCRYPT(),
+//    SCRYPT(),
+    PBKDF2( PBKDF2HashMachine.class ),
+    PBKDF2_SHA256( PBKDF2HashMachine.class ),
+    PBKDF2_SHA512( PBKDF2HashMachine.class ),;
+
+    private final Class<? extends ResponseHashMachineSpi> implementingClass;
+
+    ResponseHashAlgorithm( final Class<? extends ResponseHashMachineSpi> responseHashMachineSpi )
+    {
+        this.implementingClass = responseHashMachineSpi;
+    }
+
+    public Class<? extends ResponseHashMachineSpi> getImplementingClass( )
+    {
+        return implementingClass;
+    }
+}

+ 32 - 0
pwm-cr/src/main/java/password/pwm/cr/hash/ResponseHashMachine.java

@@ -0,0 +1,32 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.cr.hash;
+
+import password.pwm.cr.api.StoredResponseItem;
+
+public interface ResponseHashMachine
+{
+    StoredResponseItem generate( String input );
+
+    boolean test( StoredResponseItem storedResponseItem, String input );
+}

+ 28 - 0
pwm-cr/src/main/java/password/pwm/cr/hash/ResponseHashMachineSpi.java

@@ -0,0 +1,28 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.cr.hash;
+
+public interface ResponseHashMachineSpi extends ResponseHashMachine
+{
+    void init( ResponseHashAlgorithm algorithm );
+}

+ 67 - 0
pwm-cr/src/main/java/password/pwm/cr/hash/TextHashMachine.java

@@ -0,0 +1,67 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.cr.hash;
+
+import password.pwm.cr.api.StoredResponseItem;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+class TextHashMachine extends AbstractHashMachine implements ResponseHashMachineSpi
+{
+
+    TextHashMachine( )
+    {
+    }
+
+    @Override
+    public void init( final ResponseHashAlgorithm algorithm )
+    {
+
+    }
+
+    @Override
+    public Map<String, String> defaultParameters( )
+    {
+        final Map<String, String> defaultParamMap = new HashMap<>();
+        defaultParamMap.put( HashParameter.caseSensitive.toString(), Boolean.toString( false ) );
+        return Collections.unmodifiableMap( defaultParamMap );
+    }
+
+    @Override
+    public StoredResponseItem generate( final String input )
+    {
+        return null;
+    }
+
+    @Override
+    public boolean test( final StoredResponseItem hash, final String input )
+    {
+        if ( input == null || hash == null )
+        {
+            return false;
+        }
+        return false;
+    }
+}

+ 144 - 0
pwm-cr/src/main/java/password/pwm/cr/hash/TypicalHashMachine.java

@@ -0,0 +1,144 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.cr.hash;
+
+import net.iharder.Base64;
+import password.pwm.cr.api.StoredResponseItem;
+
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class TypicalHashMachine extends AbstractHashMachine implements ResponseHashMachineSpi
+{
+
+    private static final Map<ResponseHashAlgorithm, String> SUPPORTED_FORMATS;
+
+    enum VERSION
+    {
+        // original version had bug where only one iteration was ever actually performed regardless of hashCount value
+        A,
+
+        // nominal working version
+        B,
+    }
+
+    static
+    {
+        final Map<ResponseHashAlgorithm, String> map = new HashMap<>();
+        map.put( ResponseHashAlgorithm.MD5, "MD5" );
+        map.put( ResponseHashAlgorithm.SHA1, "SHA1" );
+        map.put( ResponseHashAlgorithm.SHA1_SALT, "SHA1" );
+        map.put( ResponseHashAlgorithm.SHA256_SALT, "SHA-256" );
+        map.put( ResponseHashAlgorithm.SHA512_SALT, "SHA-512" );
+        SUPPORTED_FORMATS = Collections.unmodifiableMap( map );
+    }
+
+    private ResponseHashAlgorithm responseHashAlgorithm;
+
+    public TypicalHashMachine( )
+    {
+    }
+
+    public void init( final ResponseHashAlgorithm responseHashAlgorithm )
+    {
+        this.responseHashAlgorithm = responseHashAlgorithm;
+        if ( !SUPPORTED_FORMATS.containsKey( responseHashAlgorithm ) )
+        {
+            throw new IllegalArgumentException( "implementation does not support hash algorithm " + responseHashAlgorithm );
+        }
+    }
+
+    @Override
+    public Map<String, String> defaultParameters( )
+    {
+        final Map<String, String> map = new HashMap<>();
+        map.put( HashParameter.caseSensitive.toString(), String.valueOf( false ) );
+        return Collections.unmodifiableMap( map );
+    }
+
+    @Override
+    public StoredResponseItem generate( final String input )
+    {
+        //@todo
+        return null;
+    }
+
+    @Override
+    public boolean test( final StoredResponseItem hashedResponse, final String input )
+    {
+        final String newHash = doHash( input, hashedResponse.getIterations(), ResponseHashAlgorithm.SHA1_SALT, VERSION.B );
+        return newHash.equals( hashedResponse.getHash() );
+    }
+
+    static String doHash(
+            final String input,
+            final int hashCount,
+            final ResponseHashAlgorithm formatType,
+            final VERSION version
+    )
+            throws IllegalStateException
+    {
+        final String algorithm = SUPPORTED_FORMATS.get( formatType );
+        final MessageDigest md;
+        try
+        {
+            md = MessageDigest.getInstance( algorithm );
+        }
+        catch ( NoSuchAlgorithmException e )
+        {
+            throw new IllegalStateException( "unable to load " + algorithm + " message digest algorithm: " + e.getMessage() );
+        }
+
+
+        byte[] hashedBytes;
+        try
+        {
+            hashedBytes = input.getBytes( "UTF-8" );
+        }
+        catch ( UnsupportedEncodingException e )
+        {
+            throw new IllegalStateException( "unsupported UTF8 byte encoding: " + e.getMessage() );
+        }
+
+        switch ( version )
+        {
+            case A:
+                hashedBytes = md.digest( hashedBytes );
+                return Base64.encodeBytes( hashedBytes );
+
+            case B:
+                for ( int i = 0; i < hashCount; i++ )
+                {
+                    hashedBytes = md.digest( hashedBytes );
+                }
+                return Base64.encodeBytes( hashedBytes );
+
+            default:
+                throw new IllegalStateException( "unexpected version enum in hash method" );
+        }
+    }
+}

+ 139 - 0
pwm-cr/src/test/java/password/pwm/cr/ChaiXmlResponseSet1Test.java

@@ -0,0 +1,139 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.cr;
+
+import org.junit.Assert;
+import org.junit.Test;
+import password.pwm.cr.api.ResponseLevel;
+import password.pwm.cr.api.StoredChallengeItem;
+import password.pwm.cr.api.StoredResponseItem;
+import password.pwm.cr.api.StoredResponseSet;
+import password.pwm.cr.hash.HashFactory;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.Charset;
+
+public class ChaiXmlResponseSet1Test {
+
+    @Test
+    public void testReadingStoredChaiXmlChallengeSet() throws IOException {
+        /*
+        final Reader reader = readInputXmlFile();
+        StoredResponseSet storedResponseSet = new ChaiXmlResponseSetSerializer().read(reader, ChaiXmlResponseSetSerializer.Type.USER);
+
+        testUserResponseSetValidity(storedResponseSet);
+        */
+    }
+
+
+    @Test
+    public void testReadingStoredChaiHelpdeskXmlChallengeSet() throws IOException {
+        final Reader reader = readInputXmlFile();
+        StoredResponseSet storedResponseSet = new ChaiXmlResponseSetSerializer().read(reader, ChaiXmlResponseSetSerializer.Type.HELPDESK);
+
+        testHelpdeskResponseSetValidity(storedResponseSet);
+    }
+
+    @Test
+    public void testReadWriteRead() throws IOException {
+        /*
+        final ChaiXmlResponseSetSerializer chaiXmlResponseSetSerializer = new ChaiXmlResponseSetSerializer();
+
+
+        final Map<ChaiXmlResponseSetSerializer.Type,StoredResponseSet> firstResponsesRead;
+        {
+            final Reader reader = readInputXmlFile();
+            firstResponsesRead = chaiXmlResponseSetSerializer.read(reader);
+        }
+
+        final String firstResponsesWritten;
+        {
+            final StringWriter writer = new StringWriter();
+            new ChaiXmlResponseSetSerializer().write(writer, firstResponsesRead);
+            firstResponsesWritten = writer.toString();
+        }
+
+        final Map<ChaiXmlResponseSetSerializer.Type,StoredResponseSet> secondResponsesRead;
+        {
+            final Reader reader = new StringReader(firstResponsesWritten);
+            secondResponsesRead = chaiXmlResponseSetSerializer.read(reader);
+        }
+
+        testUserResponseSetValidity(secondResponsesRead.get(ChaiXmlResponseSetSerializer.Type.USER));
+        testHelpdeskResponseSetValidity(secondResponsesRead.get(ChaiXmlResponseSetSerializer.Type.HELPDESK));
+        */
+    }
+
+    private static Reader readInputXmlFile() {
+        return new InputStreamReader(ChaiXmlResponseSet1Test.class.getResourceAsStream("ChaiXmlResponseSet1.xml"), Charset.forName("UTF8"));
+    }
+
+
+    private void testUserResponseSetValidity(final StoredResponseSet storedResponseSet) {
+        Assert.assertEquals(4, storedResponseSet.getStoredChallengeItems().size());
+        Assert.assertEquals(4, StoredItemUtils.filterStoredChallenges(storedResponseSet.getStoredChallengeItems(), ResponseLevel.RANDOM).size());
+
+        for (final StoredChallengeItem storedChallengeItem : storedResponseSet.getStoredChallengeItems()) {
+            final String questionText = storedChallengeItem.getQuestionText();
+            if ("What is the name of the main character in your favorite book?".equals(questionText)) {
+                final StoredResponseItem storedResponseItem = storedChallengeItem.getAnswer();
+                Assert.assertTrue(HashFactory.testResponseItem(storedResponseItem, "book"));
+                Assert.assertFalse(HashFactory.testResponseItem(storedResponseItem, "wrong answer"));
+            }
+
+            if ("What is the name of your favorite teacher?".equals(questionText)) {
+                final StoredResponseItem storedResponseItem = storedChallengeItem.getAnswer();
+                Assert.assertTrue(HashFactory.testResponseItem(storedResponseItem, "teacher"));
+                Assert.assertFalse(HashFactory.testResponseItem(storedResponseItem, "wrong answer"));
+            }
+
+            if ("What was the name of your childhood best friend?".equals(questionText)) {
+                final StoredResponseItem storedResponseItem = storedChallengeItem.getAnswer();
+                Assert.assertTrue(HashFactory.testResponseItem(storedResponseItem, "friend"));
+                Assert.assertFalse(HashFactory.testResponseItem(storedResponseItem, "wrong answer"));
+            }
+
+            if ("What was your favorite show as a child?".equals(questionText)) {
+                final StoredResponseItem storedResponseItem = storedChallengeItem.getAnswer();
+                Assert.assertTrue(HashFactory.testResponseItem(storedResponseItem, "child"));
+                Assert.assertFalse(HashFactory.testResponseItem(storedResponseItem, "wrong answer"));
+            }
+        }
+
+    }
+
+    private void testHelpdeskResponseSetValidity(final StoredResponseSet storedResponseSet) {
+        Assert.assertEquals(2, storedResponseSet.getStoredChallengeItems().size());
+
+        for (final StoredChallengeItem storedChallengeItem : storedResponseSet.getStoredChallengeItems()) {
+            final String questionText = storedChallengeItem.getQuestionText();
+            if ("What is the name of the main character in your favorite book?".equals(questionText)) {
+                final StoredResponseItem storedResponseItem = storedChallengeItem.getAnswer();
+
+            }
+        }
+    }
+
+}

+ 53 - 0
pwm-cr/src/test/java/password/pwm/cr/ChaiXmlResponseSetReaderTest.java

@@ -0,0 +1,53 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.cr;
+
+import org.junit.Test;
+import password.pwm.cr.api.ChallengeItemPolicy;
+import password.pwm.cr.api.QuestionSource;
+import password.pwm.cr.api.ResponseLevel;
+
+
+public class ChaiXmlResponseSetReaderTest {
+
+    @Test(expected=IllegalArgumentException.class)
+    public void testBogusMaxLength() throws Exception {
+
+        ChallengeItemPolicy.builder()
+                .questionText("question 1!")
+                .maxLength(-3)
+                .build().validate();
+    }
+
+    @Test
+    public void testValidChallengeItemCreations() {
+        ChallengeItemPolicy.builder()
+                .questionText("question 1!")
+                .minLength(1)
+                .maxLength(10)
+                .questionSource(QuestionSource.ADMIN_DEFINED)
+                .responseLevel(ResponseLevel.REQUIRED)
+                .build();
+
+    }
+}

+ 47 - 0
pwm-cr/src/test/resources/password/pwm/cr/ChaiXmlResponseSet1.xml

@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2018 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
+<ResponseSet minRandomRequired="2" locale="en" version="2" chaiVersion="0.6.9-SNAPSHOT" caseInsensitive="true" challengeSetID="SSPR-defined [Version Missing]" time="2016-08-23 08:48:15 +0000">
+    <response adminDefined="true" required="false" minLength="4" maxLength="200">
+        <challenge>What is the name of the main character in your favorite book?</challenge>
+        <answer salt="WHc5dJydH8xBHoqpS1fsnEhHtETdjblt" format="PBKDF2" hashcount="100000">OYfp1MdBrysBfaYHu+KSOhieagPilStxSMMVSuIz8DgtygXI2yHWdHEh42FMhdRUjHRUS0PbdPpGhuptgXCBXQ==</answer>
+    </response>
+    <response adminDefined="true" required="false" minLength="4" maxLength="200">
+        <challenge>What is the name of your favorite teacher?</challenge>
+        <answer salt="vA4aGz6KhNKRcnMj2nSLzWgHgXw0LcRr" format="SHA1_SALT" hashcount="100000">B:Hm9U8bh2oXzqFnPif8wChoVosss=</answer>
+    </response>
+    <response adminDefined="true" required="false" minLength="4" maxLength="200">
+        <challenge>What was the name of your childhood best friend?</challenge>
+        <answer salt="hUOJI16WDk1bCrVtAhuURmQl5NhIn7XV" format="PBKDF2" hashcount="100000">FhZELpheB9JSAju8vpxwmEik7dvlV38d/iXPpalSw1g3i2lqZAgGt2ntv24K7OklzcR3HfoKHNMqIhKlwljovg==</answer>
+    </response>
+    <response adminDefined="true" required="false" minLength="4" maxLength="200">
+        <challenge>What was your favorite show as a child?</challenge>
+        <answer salt="hd0vlgkhBOJZizCpAm4Ip1gNO5JvZTcO" format="PBKDF2" hashcount="100000">OYs6l6CH8E0fhyNlp8cfzO1YATgFygsimw37ah+LJevNdCRpDe9eKrDlCXQEFDgqumrTOwHGTa56/PTEwptXpQ==</answer>
+    </response>
+    <helpdesk-response adminDefined="true" required="false" minLength="4" maxLength="200">
+        <challenge>Question 1</challenge>
+        <answer format="HELPDESK">H4sIAAAAAAAAAIvx-82rNP_i_ouVZwNn50a-BwBCsGs2EAAAAA==</answer>
+    </helpdesk-response>
+    <helpdesk-response adminDefined="true" required="false" minLength="4" maxLength="200">
+        <challenge>Question 2</challenge>
+        <answer format="HELPDESK">H4sIAAAAAAAAAIuQYQ59M3HZ5VvFk6_dNZjsAAAQvpfpEAAAAA==</answer>
+    </helpdesk-response>
+</ResponseSet>

+ 6 - 6
webapp/pom.xml

@@ -264,19 +264,19 @@
             <version>${project.version}</version>
         </dependency>
         <dependency>
-            <groupId>org.webjars.bower</groupId>
+            <groupId>org.webjars.npm</groupId>
             <artifactId>dojo</artifactId>
-            <version>1.13.0</version>
+            <version>1.14.0</version>
         </dependency>
         <dependency>
-            <groupId>org.webjars.bower</groupId>
+            <groupId>org.webjars.npm</groupId>
             <artifactId>dijit</artifactId>
-            <version>1.13.0</version>
+            <version>1.14.0</version>
         </dependency>
         <dependency>
-            <groupId>org.webjars.bower</groupId>
+            <groupId>org.webjars.npm</groupId>
             <artifactId>dojox</artifactId>
-            <version>1.13.0</version>
+            <version>1.14.0</version>
         </dependency>
         <dependency>
             <groupId>org.webjars.bower</groupId>