/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed 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.wildfly.security.sasl.digest;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.nio.charset.StandardCharsets;
import java.security.Provider;
import java.security.Security;
import java.util.HashMap;
import java.util.Map;

import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.wildfly.common.iteration.ByteIterator;
import org.wildfly.common.iteration.CodePointIterator;
import org.wildfly.security.password.interfaces.ClearPassword;
import org.wildfly.security.password.spec.ClearPasswordSpec;
import org.wildfly.security.sasl.test.SaslServerBuilder;
import org.wildfly.security.sasl.util.SaslMechanismInformation;

import mockit.Mock;
import mockit.MockUp;
import mockit.integration.junit4.JMockit;

/**
 * Test of server side of the Digest mechanism.
 * JMockit ensure same generated nonce in every test run.
 *
 * @author <a href="mailto:jkalina@redhat.com">Jan Kalina</a>
 */
@RunWith(JMockit.class)
public class CompatibilityServerTest {

    protected static final String REALM_PROPERTY = "com.sun.security.sasl.digest.realm";
    protected static final String QOP_PROPERTY = "javax.security.sasl.qop";

    private void mockNonce(final String nonce){
        new MockUp<DigestSaslServer>(){
            @Mock
            byte[] generateNonce(){
                return nonce.getBytes(StandardCharsets.UTF_8);
            }
        };
    }

    private static final Provider provider = WildFlyElytronSaslDigestProvider.getInstance();

    @BeforeClass
    public static void registerPasswordProvider() {
        Security.insertProviderAt(provider, 1);
    }

    @AfterClass
    public static void removePasswordProvider() {
        Security.removeProvider(provider.getName());
    }

    /**
     * Test communication by first example in RFC 2831 [page 18]
     */
    @Test
    public void testRfc2831example1() throws Exception {
        mockNonce("OA6MG9tEQGm2hh");

        SaslServer server =
                new SaslServerBuilder(DigestServerFactory.class, SaslMechanismInformation.Names.DIGEST_MD5)
                        .setUserName("chris")
                        .setPassword(ClearPassword.ALGORITHM_CLEAR, new ClearPasswordSpec("secret".toCharArray()))
                        .setProtocol("imap").setServerName("elwood.innosoft.com")
                        .addMechanismRealm("elwood.innosoft.com")
                        .build();
        assertFalse(server.isComplete());

        byte[] message1 = server.evaluateResponse(new byte[0]);
        assertEquals("realm=\"elwood.innosoft.com\",nonce=\"OA6MG9tEQGm2hh\",charset=utf-8,algorithm=md5-sess", new String(message1, "UTF-8"));
        assertFalse(server.isComplete());

        byte[] message2 = "charset=utf-8,username=\"chris\",realm=\"elwood.innosoft.com\",nonce=\"OA6MG9tEQGm2hh\",nc=00000001,cnonce=\"OA6MHXh6VqTrRk\",digest-uri=\"imap/elwood.innosoft.com\",response=d388dad90d4bbd760a152321f2143af7,qop=auth".getBytes(StandardCharsets.UTF_8);
        byte[] message3 = server.evaluateResponse(message2);
        assertEquals("rspauth=ea40f60335c427b5527b84dbabcdfffd", new String(message3, "UTF-8"));
        assertTrue(server.isComplete());
        assertEquals("chris", server.getAuthorizationID());
    }


    /**
     * Test communication by second example in RFC 2831 [page 19]
     */
    @Test
    public void testRfc2831example2() throws Exception {
        mockNonce("OA9BSXrbuRhWay");

        SaslServer server =
                new SaslServerBuilder(DigestServerFactory.class, SaslMechanismInformation.Names.DIGEST_MD5)
                        .setUserName("chris")
                        .setPassword(ClearPassword.ALGORITHM_CLEAR, new ClearPasswordSpec("secret".toCharArray()))
                        .setProtocol("acap").setServerName("elwood.innosoft.com")
                        .addMechanismRealm("elwood.innosoft.com")
                        .build();
        assertFalse(server.isComplete());

        byte[] message1 = server.evaluateResponse(new byte[0]);
        assertEquals("realm=\"elwood.innosoft.com\",nonce=\"OA9BSXrbuRhWay\",charset=utf-8,algorithm=md5-sess", new String(message1, "UTF-8"));
        assertFalse(server.isComplete());

        byte[] message2 = "charset=utf-8,username=\"chris\",realm=\"elwood.innosoft.com\",nonce=\"OA9BSXrbuRhWay\",nc=00000001,cnonce=\"OA9BSuZWMSpW8m\",digest-uri=\"acap/elwood.innosoft.com\",response=6084c6db3fede7352c551284490fd0fc,qop=auth".getBytes(StandardCharsets.UTF_8);
        byte[] message3 = server.evaluateResponse(message2);
        assertEquals("rspauth=2f0b3d7c3c2e486600ef710726aa2eae", new String(message3, "UTF-8"));
        assertTrue(server.isComplete());
        assertEquals("chris", server.getAuthorizationID());
    }

    private SaslServer testCommonInitSaslServer() throws Exception {
        mockNonce("OA9BSXrbuRhWay");

        Map<String, Object> serverProps = new HashMap<String, Object>();
        serverProps.put(REALM_PROPERTY, "elwood.innosoft.com");
        SaslServer server =
                new SaslServerBuilder(DigestServerFactory.class, SaslMechanismInformation.Names.DIGEST_MD5)
                        .setUserName("chris")
                        .setPassword(ClearPassword.ALGORITHM_CLEAR, new ClearPasswordSpec("secret".toCharArray()))
                        .setProtocol("acap").setServerName("elwood.innosoft.com")
                        .setProperties(serverProps)
                        .build();
        assertFalse(server.isComplete());

        byte[] message1 = server.evaluateResponse(new byte[0]);
        assertEquals("realm=\"elwood.innosoft.com\",nonce=\"OA9BSXrbuRhWay\",charset=utf-8,algorithm=md5-sess", new String(message1, "UTF-8"));
        assertFalse(server.isComplete());

        return server;
    }


    //********************************************** */
    /**
     * Test with authorization ID (authzid) of other user
     */
    @Test
    public void testUnauthorizedAuthorizationId() throws Exception {
        SaslServer server = testCommonInitSaslServer();

        byte[] message2 = "charset=utf-8,username=\"chris\",realm=\"elwood.innosoft.com\",nonce=\"OA9BSXrbuRhWay\",nc=00000001,cnonce=\"OA9BSuZWMSpW8m\",digest-uri=\"acap/elwood.innosoft.com\",maxbuf=65536,response=0d071450228e395e2c0999e02b6aa665,qop=auth,authzid=\"george\"".getBytes(StandardCharsets.UTF_8);

        try {
            server.evaluateResponse(message2);
            fail("Not throwed SaslException!");
        } catch (SaslException e) {}
        assertFalse(server.isComplete());
    }


    /**
     * Test with authorization ID (authzid) - authorized
     */
    @Test
    public void testAuthorizedAuthorizationId() throws Exception {
        mockNonce("OA9BSXrbuRhWay");

        SaslServer server =
                new SaslServerBuilder(DigestServerFactory.class, SaslMechanismInformation.Names.DIGEST_MD5)
                        .setUserName("chris")
                        .setPassword(ClearPassword.ALGORITHM_CLEAR, new ClearPasswordSpec("secret".toCharArray()))
                        .setProtocol("acap").setServerName("elwood.innosoft.com")
                        .addMechanismRealm("elwood.innosoft.com")
                        .build();
        assertFalse(server.isComplete());

        byte[] message1 = server.evaluateResponse(new byte[0]);
        assertEquals("realm=\"elwood.innosoft.com\",nonce=\"OA9BSXrbuRhWay\",charset=utf-8,algorithm=md5-sess", new String(message1, "UTF-8"));
        assertFalse(server.isComplete());

        byte[] message2 = "charset=utf-8,username=\"chris\",realm=\"elwood.innosoft.com\",nonce=\"OA9BSXrbuRhWay\",nc=00000001,cnonce=\"OA9BSuZWMSpW8m\",digest-uri=\"acap/elwood.innosoft.com\",response=aa4e81f1c6656350f7bce05d436665de,qop=auth,authzid=\"chris\"".getBytes(StandardCharsets.UTF_8);
        byte[] message3 = server.evaluateResponse(message2);

        assertEquals("rspauth=af3ca83a805d4cfa00675a17315475c4", new String(message3, "UTF-8"));
        assertTrue(server.isComplete());
        assertEquals("chris", server.getAuthorizationID());
    }


    /**
     * Test with authentication plus integrity protection (qop=auth-int)
     */
    @Test
    public void testQopAuthInt() throws Exception {
        mockNonce("OA9BSXrbuRhWay");

        Map<String, Object> serverProps = new HashMap<String, Object>();
        serverProps.put(QOP_PROPERTY, "auth-int");
        SaslServer server =
                new SaslServerBuilder(DigestServerFactory.class, SaslMechanismInformation.Names.DIGEST_MD5)
                        .setUserName("chris")
                        .setPassword(ClearPassword.ALGORITHM_CLEAR, new ClearPasswordSpec("secret".toCharArray()))
                        .setProtocol("acap").setServerName("elwood.innosoft.com")
                        .setProperties(serverProps)
                        .addMechanismRealm("elwood.innosoft.com")
                        .build();
        assertFalse(server.isComplete());

        byte[] message1 = server.evaluateResponse(new byte[0]);
        assertEquals("realm=\"elwood.innosoft.com\",nonce=\"OA9BSXrbuRhWay\",qop=\"auth-int\",charset=utf-8,algorithm=md5-sess", new String(message1, "UTF-8"));
        assertFalse(server.isComplete());

        byte[] message2 = "charset=utf-8,username=\"chris\",realm=\"elwood.innosoft.com\",nonce=\"OA9BSXrbuRhWay\",nc=00000001,cnonce=\"OA9BSuZWMSpW8m\",digest-uri=\"acap/elwood.innosoft.com\",maxbuf=65536,response=d8b17f55b410208c6ebb22f89f9d6cbb,qop=auth-int,authzid=\"chris\"".getBytes(StandardCharsets.UTF_8);
        byte[] message3 = server.evaluateResponse(message2);
        assertEquals("rspauth=7a8794654d6d6de607e9143d52b554a8", new String(message3, "UTF-8"));
        assertTrue(server.isComplete());
        assertEquals("chris", server.getAuthorizationID());

        byte[] incoming1 = CodePointIterator.ofString("1122334499191be7952a49d8549b000100000000").hexDecode().drain();
        byte[] incoming1unwrapped = server.unwrap(incoming1, 0, incoming1.length);
        assertEquals("11223344", ByteIterator.ofBytes(incoming1unwrapped).hexEncode().drainToString());

        byte[] outcoming1 = CodePointIterator.ofString("55667788").hexDecode().drain();
        byte[] outcoming1wrapped = server.wrap(outcoming1, 0, outcoming1.length);
        assertEquals("55667788cf5e02ad15987d9076b8000100000000", ByteIterator.ofBytes(outcoming1wrapped).hexEncode().drainToString());

        byte[] incoming2 = CodePointIterator.ofString("aabbcc7e845ed48b0474447543000100000001").hexDecode().drain();
        byte[] incoming2unwrapped = server.unwrap(incoming2, 0, incoming2.length);
        assertEquals("aabbcc", ByteIterator.ofBytes(incoming2unwrapped).hexEncode().drainToString());

        byte[] outcoming2 = new byte[0];
        byte[] outcoming2wrapped = server.wrap(outcoming2, 0, outcoming2.length);
        assertEquals("", ByteIterator.ofBytes(outcoming2wrapped).hexEncode().drainToString());

        // MAC not corresponds to message and sequence number
        byte[] incoming3 = CodePointIterator.ofString("0188034ce1b414194c1c822a55000100000002").hexDecode().drain();
        byte[] incoming3unwrapped = server.unwrap(incoming3, 0, incoming3.length);
        assertEquals("", ByteIterator.ofBytes(incoming3unwrapped).hexEncode().drainToString());

        // bad sequence number
        try {
            byte[] incoming4 = CodePointIterator.ofString("0102032cf12c67e4318ebd624e000100000003").hexDecode().drain();
            server.unwrap(incoming4, 0, incoming4.length);
            fail("Out of order sequencing SaslException expected!");
        } catch(SaslException e){}
    }


    /**
     * Test with authentication plus integrity and confidentiality protection (qop=auth-conf, cipher=default=3des)
     */
    @Test
    public void testQopAuthConf() throws Exception {
        mockNonce("OA9BSXrbuRhWay");

        Map<String, Object> serverProps = new HashMap<String, Object>();
        serverProps.put(QOP_PROPERTY, "auth-conf");
        SaslServer server =
                new SaslServerBuilder(DigestServerFactory.class, SaslMechanismInformation.Names.DIGEST_MD5)
                        .setUserName("chris")
                        .setPassword(ClearPassword.ALGORITHM_CLEAR, new ClearPasswordSpec("secret".toCharArray()))
                        .setProtocol("acap").setServerName("elwood.innosoft.com")
                        .setProperties(serverProps)
                        .addMechanismRealm("elwood.innosoft.com")
                        .build();
        assertFalse(server.isComplete());

        byte[] message1 = server.evaluateResponse(new byte[0]);
        assertEquals("realm=\"elwood.innosoft.com\",nonce=\"OA9BSXrbuRhWay\",qop=\"auth-conf\",charset=utf-8,cipher=\"3des,rc4,des,rc4-56,rc4-40\",algorithm=md5-sess", new String(message1, "UTF-8"));
        assertFalse(server.isComplete());

        byte[] message2 = "charset=utf-8,username=\"chris\",realm=\"elwood.innosoft.com\",nonce=\"OA9BSXrbuRhWay\",nc=00000001,cnonce=\"OA9BSuZWMSpW8m\",digest-uri=\"acap/elwood.innosoft.com\",maxbuf=65536,response=4520cf48234bb93b95548a25cd56601b,qop=auth-conf,cipher=\"3des\",authzid=\"chris\"".getBytes(StandardCharsets.UTF_8);
        byte[] message3 = server.evaluateResponse(message2);
        assertEquals("rspauth=a804fda66588e2d911bbacd1b1163bc1", new String(message3, "UTF-8"));
        assertTrue(server.isComplete());
        assertEquals("chris", server.getAuthorizationID());
        assertEquals("auth-conf", server.getNegotiatedProperty(Sasl.QOP));
        assertEquals("high", server.getNegotiatedProperty(Sasl.STRENGTH));

        byte[] incoming1 = CodePointIterator.ofString("13f7644f8c783501177522c1a455cb1f000100000000").hexDecode().drain();
        byte[] incoming1unwrapped = server.unwrap(incoming1, 0, incoming1.length);
        assertEquals("11223344", ByteIterator.ofBytes(incoming1unwrapped).hexEncode().drainToString());

        byte[] outcoming1 = CodePointIterator.ofString("55667788").hexDecode().drain();
        byte[] outcoming1wrapped = server.wrap(outcoming1, 0, outcoming1.length);
        assertEquals("93ce33409e0fe5187e07c16fc3041f64000100000000", ByteIterator.ofBytes(outcoming1wrapped).hexEncode().drainToString());

        byte[] incoming2 = CodePointIterator.ofString("ec426d9cd3276f22285ab5da8df8f26b000100000001").hexDecode().drain();
        byte[] incoming2unwrapped = server.unwrap(incoming2, 0, incoming2.length);
        assertEquals("aabbcc", ByteIterator.ofBytes(incoming2unwrapped).hexEncode().drainToString());

        byte[] outcoming2 = new byte[0];
        byte[] outcoming2wrapped = server.wrap(outcoming2, 0, outcoming2.length);
        assertEquals("", ByteIterator.ofBytes(outcoming2wrapped).hexEncode().drainToString());

        // MAC not corresponds to message and sequence number
        byte[] incoming3 = CodePointIterator.ofString("b0acad3c969d091251666f91070166f5000100000002").hexDecode().drain();
        byte[] incoming3unwrapped = server.unwrap(incoming3, 0, incoming3.length);
        assertEquals("", ByteIterator.ofBytes(incoming3unwrapped).hexEncode().drainToString());

        // bad sequence number
        try {
            byte[] incoming4 = CodePointIterator.ofString("2cfa9bced5b960763953c4f9838b7022000100000003").hexDecode().drain();
            server.unwrap(incoming4, 0, incoming4.length);
            fail("Out of order sequencing SaslException expected!");
        } catch(SaslException e){}
    }


    /**
     * Test with authentication plus integrity and confidentiality protection (qop=auth-conf, cipher=rc4)
     */
    @Test
    public void testQopAuthConfRc4() throws Exception {
        mockNonce("OA9BSXrbuRhWay");

        Map<String, Object> serverProps = new HashMap<String, Object>();
        serverProps.put(QOP_PROPERTY, "auth-conf");
        SaslServer server =
                new SaslServerBuilder(DigestServerFactory.class, SaslMechanismInformation.Names.DIGEST_MD5)
                        .setUserName("chris")
                        .setPassword(ClearPassword.ALGORITHM_CLEAR, new ClearPasswordSpec("secret".toCharArray()))
                        .setProtocol("acap").setServerName("elwood.innosoft.com")
                        .addMechanismRealm("elwood.innosoft.com")
                        .setProperties(serverProps)
                        .build();
        assertFalse(server.isComplete());

        byte[] message1 = server.evaluateResponse(new byte[0]);
        assertEquals("realm=\"elwood.innosoft.com\",nonce=\"OA9BSXrbuRhWay\",qop=\"auth-conf\",charset=utf-8,cipher=\"3des,rc4,des,rc4-56,rc4-40\",algorithm=md5-sess", new String(message1, "UTF-8"));
        assertFalse(server.isComplete());

        byte[] message2 = "charset=utf-8,username=\"chris\",realm=\"elwood.innosoft.com\",nonce=\"OA9BSXrbuRhWay\",nc=00000001,cnonce=\"OA9BSuZWMSpW8m\",digest-uri=\"acap/elwood.innosoft.com\",maxbuf=65536,response=4520cf48234bb93b95548a25cd56601b,qop=auth-conf,cipher=\"rc4\",authzid=\"chris\"".getBytes(StandardCharsets.UTF_8);
        byte[] message3 = server.evaluateResponse(message2);
        assertEquals("rspauth=a804fda66588e2d911bbacd1b1163bc1", new String(message3, "UTF-8"));
        assertTrue(server.isComplete());
        assertEquals("chris", server.getAuthorizationID());

        byte[] incoming1 = CodePointIterator.ofString("6a9328ca634e47c8d1ecc3c3f6e6000100000000").hexDecode().drain();
        byte[] incoming1unwrapped = server.unwrap(incoming1, 0, incoming1.length);
        assertEquals("11223344", ByteIterator.ofBytes(incoming1unwrapped).hexEncode().drainToString());

        byte[] outcoming1 = CodePointIterator.ofString("55667788").hexDecode().drain();
        byte[] outcoming1wrapped = server.wrap(outcoming1, 0, outcoming1.length);
        assertEquals("9fc7eb1c3c9e04b52df6e347a389000100000000", ByteIterator.ofBytes(outcoming1wrapped).hexEncode().drainToString());

        byte[] incoming2 = CodePointIterator.ofString("7e15b940fccbb58a5612f54da7000100000001").hexDecode().drain();
        byte[] incoming2unwrapped = server.unwrap(incoming2, 0, incoming2.length);
        assertEquals("aabbcc", ByteIterator.ofBytes(incoming2unwrapped).hexEncode().drainToString());

        byte[] outcoming2 = new byte[0];
        byte[] outcoming2wrapped = server.wrap(outcoming2, 0, outcoming2.length);
        assertEquals("", ByteIterator.ofBytes(outcoming2wrapped).hexEncode().drainToString());

        // bad message
        byte[] incoming3 = CodePointIterator.ofString("84468595614f4ac73fabe47cc4000100000002").hexDecode().drain();
        byte[] incoming3unwrapped = server.unwrap(incoming3, 0, incoming3.length);
        assertEquals("", ByteIterator.ofBytes(incoming3unwrapped).hexEncode().drainToString());

        // bad sequence number
        try {
            byte[] incoming4 = CodePointIterator.ofString("4a206df4178a9b7d091dec3527000100000003").hexDecode().drain();
            server.unwrap(incoming4, 0, incoming4.length);
            fail("Out of order sequencing SaslException expected!");
        } catch(SaslException e){}
    }


    /**
     * Test with authentication plus integrity and confidentiality protection (qop=auth-conf, cipher=des)
     */
    @Test
    public void testQopAuthConfDes() throws Exception {
        mockNonce("OA9BSXrbuRhWay");

        Map<String, Object> serverProps = new HashMap<String, Object>();
        serverProps.put(QOP_PROPERTY, "auth-conf");
        SaslServer server =
                new SaslServerBuilder(DigestServerFactory.class, SaslMechanismInformation.Names.DIGEST_MD5)
                        .setUserName("chris")
                        .setPassword(ClearPassword.ALGORITHM_CLEAR, new ClearPasswordSpec("secret".toCharArray()))
                        .setProtocol("acap").setServerName("elwood.innosoft.com")
                        .addMechanismRealm("elwood.innosoft.com")
                        .setProperties(serverProps)
                        .build();
        assertFalse(server.isComplete());

        byte[] message1 = server.evaluateResponse(new byte[0]);
        assertEquals("realm=\"elwood.innosoft.com\",nonce=\"OA9BSXrbuRhWay\",qop=\"auth-conf\",charset=utf-8,cipher=\"3des,rc4,des,rc4-56,rc4-40\",algorithm=md5-sess", new String(message1, "UTF-8"));
        assertFalse(server.isComplete());

        byte[] message2 = "charset=utf-8,username=\"chris\",realm=\"elwood.innosoft.com\",nonce=\"OA9BSXrbuRhWay\",nc=00000001,cnonce=\"OA9BSuZWMSpW8m\",digest-uri=\"acap/elwood.innosoft.com\",maxbuf=65536,response=4520cf48234bb93b95548a25cd56601b,qop=auth-conf,cipher=\"des\",authzid=\"chris\"".getBytes(StandardCharsets.UTF_8);
        byte[] message3 = server.evaluateResponse(message2);
        assertEquals("rspauth=a804fda66588e2d911bbacd1b1163bc1", new String(message3, "UTF-8"));
        assertTrue(server.isComplete());
        assertEquals("chris", server.getAuthorizationID());

        byte[] incoming1 = CodePointIterator.ofString("b2a12ba8ccd1030e7da4bac57a224197000100000000").hexDecode().drain();
        byte[] incoming1unwrapped = server.unwrap(incoming1, 0, incoming1.length);
        assertEquals("11223344", ByteIterator.ofBytes(incoming1unwrapped).hexEncode().drainToString());

        byte[] outcoming1 = CodePointIterator.ofString("55667788").hexDecode().drain();
        byte[] outcoming1wrapped = server.wrap(outcoming1, 0, outcoming1.length);
        assertEquals("8bc1267e71a769456f0c60f030e13f32000100000000", ByteIterator.ofBytes(outcoming1wrapped).hexEncode().drainToString());

        byte[] incoming2 = CodePointIterator.ofString("13144fc90ca65d3838d3547cca43e8ad000100000001").hexDecode().drain();
        byte[] incoming2unwrapped = server.unwrap(incoming2, 0, incoming2.length);
        assertEquals("aabbcc", ByteIterator.ofBytes(incoming2unwrapped).hexEncode().drainToString());

        byte[] outcoming2 = new byte[0];
        byte[] outcoming2wrapped = server.wrap(outcoming2, 0, outcoming2.length);
        assertEquals("", ByteIterator.ofBytes(outcoming2wrapped).hexEncode().drainToString());

        // bad message
        byte[] incoming3 = CodePointIterator.ofString("7022412985dbee1d261ecb8850486c6e000100000002").hexDecode().drain();
        byte[] incoming3unwrapped = server.unwrap(incoming3, 0, incoming3.length);
        assertEquals("", ByteIterator.ofBytes(incoming3unwrapped).hexEncode().drainToString());

        // bad sequence number
        try {
            byte[] incoming4 = CodePointIterator.ofString("3457275b036fa15042e41aeda83a563b000100000003").hexDecode().drain();
            server.unwrap(incoming4, 0, incoming4.length);
            fail("Out of order sequencing SaslException expected!");
        } catch(SaslException e){}
    }


    /**
     * Test with authentication plus integrity and confidentiality protection (qop=auth-conf, cipher=rc4-56)
     */
    @Test
    public void testQopAuthConfRc456() throws Exception {
        mockNonce("OA9BSXrbuRhWay");

        Map<String, Object> serverProps = new HashMap<String, Object>();
        serverProps.put(QOP_PROPERTY, "auth-conf");
        SaslServer server =
                new SaslServerBuilder(DigestServerFactory.class, SaslMechanismInformation.Names.DIGEST_MD5)
                        .setUserName("chris")
                        .setPassword(ClearPassword.ALGORITHM_CLEAR, new ClearPasswordSpec("secret".toCharArray()))
                        .setProtocol("acap").setServerName("elwood.innosoft.com")
                        .setProperties(serverProps)
                        .addMechanismRealm("elwood.innosoft.com")
                        .build();
        assertFalse(server.isComplete());

        byte[] message1 = server.evaluateResponse(new byte[0]);
        assertEquals("realm=\"elwood.innosoft.com\",nonce=\"OA9BSXrbuRhWay\",qop=\"auth-conf\",charset=utf-8,cipher=\"3des,rc4,des,rc4-56,rc4-40\",algorithm=md5-sess", new String(message1, "UTF-8"));
        assertFalse(server.isComplete());

        byte[] message2 = "charset=utf-8,username=\"chris\",realm=\"elwood.innosoft.com\",nonce=\"OA9BSXrbuRhWay\",nc=00000001,cnonce=\"OA9BSuZWMSpW8m\",digest-uri=\"acap/elwood.innosoft.com\",maxbuf=65536,response=4520cf48234bb93b95548a25cd56601b,qop=auth-conf,cipher=\"rc4-56\",authzid=\"chris\"".getBytes(StandardCharsets.UTF_8);
        byte[] message3 = server.evaluateResponse(message2);
        assertEquals("rspauth=a804fda66588e2d911bbacd1b1163bc1", new String(message3, "UTF-8"));
        assertTrue(server.isComplete());
        assertEquals("chris", server.getAuthorizationID());

        byte[] incoming1 = CodePointIterator.ofString("7a77c4b8b20208e502e5dc09bbfc000100000000").hexDecode().drain();
        byte[] incoming1unwrapped = server.unwrap(incoming1, 0, incoming1.length);
        assertEquals("11223344", ByteIterator.ofBytes(incoming1unwrapped).hexEncode().drainToString());

        byte[] outcoming1 = CodePointIterator.ofString("55667788").hexDecode().drain();
        byte[] outcoming1wrapped = server.wrap(outcoming1, 0, outcoming1.length);
        assertEquals("c10acbf737cdebf2298df53417bc000100000000", ByteIterator.ofBytes(outcoming1wrapped).hexEncode().drainToString());

        byte[] incoming2 = CodePointIterator.ofString("efcb8662925427788b0ffeab2c000100000001").hexDecode().drain();
        byte[] incoming2unwrapped = server.unwrap(incoming2, 0, incoming2.length);
        assertEquals("aabbcc", ByteIterator.ofBytes(incoming2unwrapped).hexEncode().drainToString());

        byte[] outcoming2 = new byte[0];
        byte[] outcoming2wrapped = server.wrap(outcoming2, 0, outcoming2.length);
        assertEquals("", ByteIterator.ofBytes(outcoming2wrapped).hexEncode().drainToString());

        // bad message
        byte[] incoming3 = CodePointIterator.ofString("03c8fa9cb28ecf4a99561e5ac3000100000002").hexDecode().drain();
        byte[] incoming3unwrapped = server.unwrap(incoming3, 0, incoming3.length);
        assertEquals("", ByteIterator.ofBytes(incoming3unwrapped).hexEncode().drainToString());

        // bad sequence number
        try {
            byte[] incoming4 = CodePointIterator.ofString("4daa261a6afb77f0b1d1d3d4eb000100000003").hexDecode().drain();
            server.unwrap(incoming4, 0, incoming4.length);
            fail("Out of order sequencing SaslException expected!");
        } catch(SaslException e){}
    }


    /**
     * Test with authentication plus integrity and confidentiality protection (qop=auth-conf, cipher=rc4-40)
     */
    @Test
    public void testQopAuthConfRc440() throws Exception {
        mockNonce("OA9BSXrbuRhWay");

        Map<String, Object> serverProps = new HashMap<String, Object>();
        serverProps.put(QOP_PROPERTY, "auth-conf");
        SaslServer server =
                new SaslServerBuilder(DigestServerFactory.class, SaslMechanismInformation.Names.DIGEST_MD5)
                        .setUserName("chris")
                        .setPassword(ClearPassword.ALGORITHM_CLEAR, new ClearPasswordSpec("secret".toCharArray()))
                        .setProtocol("acap").setServerName("elwood.innosoft.com")
                        .addMechanismRealm("elwood.innosoft.com")
                        .setProperties(serverProps)
                        .build();

        assertFalse(server.isComplete());

        byte[] message1 = server.evaluateResponse(new byte[0]);
        assertEquals("realm=\"elwood.innosoft.com\",nonce=\"OA9BSXrbuRhWay\",qop=\"auth-conf\",charset=utf-8,cipher=\"3des,rc4,des,rc4-56,rc4-40\",algorithm=md5-sess", new String(message1, "UTF-8"));
        assertFalse(server.isComplete());

        byte[] message2 = "charset=utf-8,username=\"chris\",realm=\"elwood.innosoft.com\",nonce=\"OA9BSXrbuRhWay\",nc=00000001,cnonce=\"OA9BSuZWMSpW8m\",digest-uri=\"acap/elwood.innosoft.com\",maxbuf=65536,response=4520cf48234bb93b95548a25cd56601b,qop=auth-conf,cipher=\"rc4-40\",authzid=\"chris\"".getBytes(StandardCharsets.UTF_8);
        byte[] message3 = server.evaluateResponse(message2);
        assertEquals("rspauth=a804fda66588e2d911bbacd1b1163bc1", new String(message3, "UTF-8"));
        assertTrue(server.isComplete());
        assertEquals("chris", server.getAuthorizationID());

        byte[] incoming1 = CodePointIterator.ofString("ed46c6b0d38acb719aad661f9625000100000000").hexDecode().drain();
        byte[] incoming1unwrapped = server.unwrap(incoming1, 0, incoming1.length);
        assertEquals("11223344", ByteIterator.ofBytes(incoming1unwrapped).hexEncode().drainToString());

        byte[] outcoming1 = CodePointIterator.ofString("55667788").hexDecode().drain();
        byte[] outcoming1wrapped = server.wrap(outcoming1, 0, outcoming1.length);
        assertEquals("44aca6145a89353d26258e524724000100000000", ByteIterator.ofBytes(outcoming1wrapped).hexEncode().drainToString());

        byte[] incoming2 = CodePointIterator.ofString("b7bdc8f08733182154289e7f3d000100000001").hexDecode().drain();
        byte[] incoming2unwrapped = server.unwrap(incoming2, 0, incoming2.length);
        assertEquals("aabbcc", ByteIterator.ofBytes(incoming2unwrapped).hexEncode().drainToString());

        byte[] outcoming2 = new byte[0];
        byte[] outcoming2wrapped = server.wrap(outcoming2, 0, outcoming2.length);
        assertEquals("", ByteIterator.ofBytes(outcoming2wrapped).hexEncode().drainToString());

        // bad message
        byte[] incoming3 = CodePointIterator.ofString("34968ede3148eb0d3affe15656000100000002").hexDecode().drain();
        byte[] incoming3unwrapped = server.unwrap(incoming3, 0, incoming3.length);
        assertEquals("", ByteIterator.ofBytes(incoming3unwrapped).hexEncode().drainToString());

        // bad sequence number
        try {
            byte[] incoming4 = CodePointIterator.ofString("8e497ee789076071cf3b5bb9e1000100000003").hexDecode().drain();
            server.unwrap(incoming4, 0, incoming4.length);
            fail("Out of order sequencing SaslException expected!");
        } catch(SaslException e){}
    }


    /**
     * Replay attack (different nonce than sent by server)
     */
    @Test
    public void testReplayAttack() throws Exception {
        SaslServer server = testCommonInitSaslServer();

        byte[] message2 = "charset=utf-8,username=\"chris\",realm=\"elwood.innosoft.com\",nonce=\"OA6MG9tEQGm2hh\",nc=00000001,cnonce=\"OA6MHXh6VqTrRk\",digest-uri=\"imap/elwood.innosoft.com\",response=d388dad90d4bbd760a152321f2143af7,qop=auth".getBytes(StandardCharsets.UTF_8);
        try{
            server.evaluateResponse(message2);
            fail("Not throwed SaslException!");
        } catch (SaslException e) {}
        assertFalse(server.isComplete());
    }


    /**
     * Bad response
     */
    @Test
    public void testBadResponse() throws Exception {
        SaslServer server = testCommonInitSaslServer();

        byte[] message2 = "charset=utf-8,username=\"chris\",realm=\"elwood.innosoft.com\",nonce=\"OA9BSXrbuRhWay\",nc=00000001,cnonce=\"OA9BSuZWMSpW8m\",digest-uri=\"acap/elwood.innosoft.com\",response=d388dad90d4bbd760a152321f2143af7,qop=auth".getBytes(StandardCharsets.UTF_8);
        try{
            server.evaluateResponse(message2);
            fail("Not throwed SaslException!");
        } catch (SaslException e) {}
        assertFalse(server.isComplete());
    }


    /**
     * More realms from server (realm="other-realm",realm="elwood.innosoft.com",realm="next-realm" -> elwood.innosoft.com)
     */
    @Test
    public void testMoreRealmsFromServer() throws Exception {
        mockNonce("OA9BSXrbuRhWay");

        Map<String, Object> serverProps = new HashMap<String, Object>();
        serverProps.put(REALM_PROPERTY, "other-realm elwood.innosoft.com next-realm");
        SaslServer server =
                new SaslServerBuilder(DigestServerFactory.class, SaslMechanismInformation.Names.DIGEST_MD5)
                        .setUserName("chris")
                        .setPassword(ClearPassword.ALGORITHM_CLEAR, new ClearPasswordSpec("secret".toCharArray()))
                        .setProtocol("acap").setServerName("elwood.innosoft.com")
                        .setProperties(serverProps)
                        .build();
        assertFalse(server.isComplete());

        byte[] message1 = server.evaluateResponse(new byte[0]);
        assertEquals("realm=\"other-realm\",realm=\"elwood.innosoft.com\",realm=\"next-realm\",nonce=\"OA9BSXrbuRhWay\",charset=utf-8,algorithm=md5-sess", new String(message1, "UTF-8"));
        assertFalse(server.isComplete());

        byte[] message2 = "charset=utf-8,username=\"chris\",realm=\"elwood.innosoft.com\",nonce=\"OA9BSXrbuRhWay\",nc=00000001,cnonce=\"OA9BSuZWMSpW8m\",digest-uri=\"acap/elwood.innosoft.com\",response=d388dad90d4bbd760a152321f2143af7,qop=auth".getBytes(StandardCharsets.UTF_8);
        try{
            server.evaluateResponse(message2);
            fail("Not throwed SaslException!");
        } catch (SaslException e) {}
        assertFalse(server.isComplete());

    }


    /**
     * Blank nonce from client (connection with naughty client)
     */
    @Test
    public void testBlankClientNonce() throws Exception {
        mockNonce("OA9BSXrbuRhWay");

        SaslServer server =
                new SaslServerBuilder(DigestServerFactory.class, SaslMechanismInformation.Names.DIGEST_MD5)
                        .setUserName("chris")
                        .setPassword(ClearPassword.ALGORITHM_CLEAR, new ClearPasswordSpec("secret".toCharArray()))
                        .setProtocol("acap").setServerName("elwood.innosoft.com")
                        .addMechanismRealm("other-realm")
                        .addMechanismRealm("elwood.innosoft.com")
                        .addMechanismRealm("next-realm")
                        .build();
        assertFalse(server.isComplete());

        byte[] message1 = server.evaluateResponse(new byte[0]);
        assertEquals("realm=\"other-realm\",realm=\"elwood.innosoft.com\",realm=\"next-realm\",nonce=\"OA9BSXrbuRhWay\",charset=utf-8,algorithm=md5-sess", new String(message1, "UTF-8"));
        assertFalse(server.isComplete());

        byte[] message2 = "charset=utf-8,username=\"chris\",realm=\"elwood.innosoft.com\",nonce=\"OA9BSXrbuRhWay\",nc=00000001,cnonce=\"\",digest-uri=\"acap/elwood.innosoft.com\",response=0ca21eafddf586f954909d2fd95b1ee7,qop=auth".getBytes(StandardCharsets.UTF_8);
        byte[] message3 = server.evaluateResponse(message2);
        assertEquals("rspauth=2bf631e48acb9863e9f5518ccc804b3b", new String(message3, "UTF-8"));
        assertTrue(server.isComplete());
        assertEquals("chris", server.getAuthorizationID());

    }


    /**
     * Test successful authentication with Unicode chars (UTF-8 encoding)
     */
    @Test
    public void testUtf8Charset() throws Exception {
        mockNonce("sn\u0438\u4F60\uD83C\uDCA1");

        Map<String, Object> serverProps = new HashMap<String, Object>();
        SaslServer server =
                new SaslServerBuilder(DigestServerFactory.class, SaslMechanismInformation.Names.DIGEST_MD5)
                        .setUserName("\u0438\u4F60\uD83C\uDCA1")
                        .setPassword(ClearPassword.ALGORITHM_CLEAR, new ClearPasswordSpec("\u0438\u4F60\uD83C\uDCA1".toCharArray()))
                        .setProtocol("\u0438\u4F60\uD83C\uDCA1").setServerName("realm.\u0438\u4F60\uD83C\uDCA1.com")
                        .setProperties(serverProps)
                        .addMechanismRealm("realm.\u0438\u4F60\uD83C\uDCA1.com")
                        .build();
        assertFalse(server.isComplete());

        byte[] message1 = server.evaluateResponse(new byte[0]);
        assertEquals("realm=\"realm.\u0438\u4F60\uD83C\uDCA1.com\",nonce=\"sn\u0438\u4F60\uD83C\uDCA1\",charset=utf-8,algorithm=md5-sess", new String(message1, "UTF-8"));
        assertFalse(server.isComplete());

        byte[] message2 = "charset=utf-8,username=\"\u0438\u4F60\uD83C\uDCA1\",realm=\"realm.\u0438\u4F60\uD83C\uDCA1.com\",nonce=\"sn\u0438\u4F60\uD83C\uDCA1\",nc=00000001,cnonce=\"cn\u0438\u4F60\uD83C\uDCA1\",digest-uri=\"\u0438\u4F60\uD83C\uDCA1/realm.\u0438\u4F60\uD83C\uDCA1.com\",maxbuf=65536,response=420939e06d2d748c157c5e33499b41a9,qop=auth".getBytes(StandardCharsets.UTF_8);
        byte[] message3 = server.evaluateResponse(message2);
        assertEquals("rspauth=9c4d137545617ba98c11aaea939b4381", new String(message3, "UTF-8"));
        assertTrue(server.isComplete());
        assertEquals("\u0438\u4F60\uD83C\uDCA1", server.getAuthorizationID());
    }


    /**
     * More realms from server (realm="other-realm",realm="elwood.innosoft.com",realm="next-realm" -> elwood.innosoft.com)
     */
    @Test
    public void testMoreRealmsWithEscapedDelimiters() throws Exception {
        mockNonce("OA9BSXrbuRhWay");

        Map<String, Object> serverProps = new HashMap<String, Object>();
        serverProps.put(REALM_PROPERTY, "first\\ realm second\\\\\\ realm \\ with\\ spaces\\  \\ ");
        SaslServer server =
                new SaslServerBuilder(DigestServerFactory.class, SaslMechanismInformation.Names.DIGEST_MD5)
                        .setUserName("chris")
                        .setPassword(ClearPassword.ALGORITHM_CLEAR, new ClearPasswordSpec("secret".toCharArray()))
                        .setProtocol("protocol name").setServerName("server name")
                        .addMechanismRealm("first realm")
                        .addMechanismRealm("second\\ realm")
                        .addMechanismRealm(" with spaces ")
                        .addMechanismRealm(" ")
                        .setProperties(serverProps)
                        .build();
        assertFalse(server.isComplete());

        byte[] message1 = server.evaluateResponse(new byte[0]);
        assertEquals("realm=\"first realm\",realm=\"second\\\\ realm\",realm=\" with spaces \",realm=\" \",nonce=\"OA9BSXrbuRhWay\",charset=utf-8,algorithm=md5-sess", new String(message1, "UTF-8"));
        assertFalse(server.isComplete());

        byte[] message2 = "charset=utf-8,username=\"chris\",realm=\"first realm\",nonce=\"OA9BSXrbuRhWay\",nc=00000001,cnonce=\"OA6MHXh6VqTrRk\",digest-uri=\"protocol name/server name\",maxbuf=65536,response=bf3dd710ee08b05c663456975c156075,qop=auth".getBytes(StandardCharsets.UTF_8);
        byte[] message3 = server.evaluateResponse(message2);
        assertEquals("rspauth=05a18aff49b22e373bb91af7396ce345", new String(message3, "UTF-8"));
        assertTrue(server.isComplete());
        assertEquals("chris", server.getAuthorizationID());
    }

    @Test
    public void testIncorrectResponseField() throws Exception {
        mockNonce("OA6MG9tEQGm2hh");

        SaslServer server =
                new SaslServerBuilder(DigestServerFactory.class, SaslMechanismInformation.Names.DIGEST_MD5)
                        .setUserName("chris")
                        .setPassword(ClearPassword.ALGORITHM_CLEAR, new ClearPasswordSpec("secret".toCharArray()))
                        .setProtocol("imap").setServerName("elwood.innosoft.com")
                        .addMechanismRealm("elwood.innosoft.com")
                        .build();
        assertFalse(server.isComplete());

        byte[] message = server.evaluateResponse(new byte[0]);
        assertEquals("realm=\"elwood.innosoft.com\",nonce=\"OA6MG9tEQGm2hh\",charset=utf-8,algorithm=md5-sess", new String(message, "UTF-8"));
        assertFalse(server.isComplete());

        byte[] invalidMessage = "charset=utf-8,username=\"chris\",realm=\"elwood.innosoft.com\",nonce=\"OA6MG9tEQGm2hh\",nc=00000001,cnonce=\"OA6MHXh6VqTrRk\",digest-uri=\"imap/elwood.innosoft.com\",response=incorrectResponse,qop=auth".getBytes(StandardCharsets.UTF_8);
        try {
            server.evaluateResponse(invalidMessage);
        } catch (SaslException e) {
            assertTrue(e.getMessage().contains("invalid proof"));
        }
    }
}
