package gnu.crypto.sasl.srp;

// ----------------------------------------------------------------------------
// $Id: SRPServer.java,v 1.7 2005/10/06 04:24:18 rsdio Exp $
//
// Copyright (C) 2003 Free Software Foundation, Inc.
//
// This file is part of GNU Crypto.
//
// GNU Crypto 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, or (at your option)
// any later version.
//
// GNU Crypto 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; see the file COPYING.  If not, write to the
//
//    Free Software Foundation Inc.,
//    51 Franklin Street, Fifth Floor,
//    Boston, MA 02110-1301
//    USA
//
// Linking this library statically or dynamically with other modules is
// making a combined work based on this library.  Thus, the terms and
// conditions of the GNU General Public License cover the whole
// combination.
//
// As a special exception, the copyright holders of this library give
// you permission to link this library with independent modules to
// produce an executable, regardless of the license terms of these
// independent modules, and to copy and distribute the resulting
// executable under terms of your choice, provided that you also meet,
// for each linked independent module, the terms and conditions of the
// license of that module.  An independent module is a module which is
// not derived from or based on this library.  If you modify this
// library, you may extend this exception to your version of the
// library, but you are not obligated to do so.  If you do not wish to
// do so, delete this exception statement from your version.
// ----------------------------------------------------------------------------

import gnu.crypto.Registry;
import gnu.crypto.assembly.Direction;
import gnu.crypto.cipher.CipherFactory;
import gnu.crypto.cipher.IBlockCipher;
import gnu.crypto.key.IKeyAgreementParty;
import gnu.crypto.key.KeyAgreementFactory;
import gnu.crypto.key.KeyAgreementException;
import gnu.crypto.key.OutgoingMessage;
import gnu.crypto.key.IncomingMessage;
import gnu.crypto.key.srp6.SRP6KeyAgreement;
import gnu.crypto.sasl.IllegalMechanismStateException;
import gnu.crypto.sasl.InputBuffer;
import gnu.crypto.sasl.IntegrityException;
import gnu.crypto.sasl.OutputBuffer;
import gnu.crypto.sasl.ServerMechanism;
import gnu.crypto.util.PRNG;
import gnu.crypto.util.Util;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.HashMap;
import java.util.StringTokenizer;

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

/**
 * <p>The SASL-SRP server-side mechanism.</p>
 *
 * @version $Revision: 1.7 $
 */
public class SRPServer extends ServerMechanism implements SaslServer {

   // Debugging methods and variables
   // -------------------------------------------------------------------------

   private static final String NAME = "SRPServer";
//   private static final String ERROR = "ERROR";
   private static final String WARN =  " WARN";
   private static final String INFO =  " INFO";
   private static final String TRACE = "DEBUG";
   private static final boolean DEBUG = true;
   private static final int debuglevel = 3;
   private static final PrintWriter err = new PrintWriter(System.out, true);
   private static void debug(final String level, final Object obj) {
      err.println("["+level+"] "+NAME+": "+String.valueOf(obj));
   }

   // Constants and variables
   // -------------------------------------------------------------------------

   private String U = null; // client's username
   private BigInteger N, g, A, B;
   private byte[] s; // salt
   private byte[] cIV, sIV; // client+server IVs, when confidentiality is on
   private byte[] cn, sn; // client's and server's nonce
   private SRP srp; // SRP algorithm instance used by this server
   private byte[] sid; // session ID when re-used
   private int ttl = 360; // session time-to-live in seconds
   private byte[] cCB; // peer's channel binding'

   private String mandatory; // List of available options
   private String L = null;
   private String o;
   private String chosenIntegrityAlgorithm;
   private String chosenConfidentialityAlgorithm;
   private int rawSendSize = Registry.SASL_BUFFER_MAX_LIMIT;

   private byte[] K; // shared session key
   private boolean replayDetection = true; // whether Replay Detection is on
   private int inCounter = 0; // messages sequence numbers
   private int outCounter = 0;
   private IALG inMac, outMac; // if !null, use for integrity
   private CALG inCipher, outCipher; // if !null, use for confidentiality

   private IKeyAgreementParty serverHandler =
         KeyAgreementFactory.getPartyBInstance(Registry.SRP_SASL_KA);

   // Constructor(s)
   // -------------------------------------------------------------------------

   public SRPServer() {
      super(Registry.SASL_SRP_MECHANISM);
   }

   // Class methods
   // -------------------------------------------------------------------------

   // Instance methods
   // -------------------------------------------------------------------------

   // abstract methods implementation -----------------------------------------

   protected void initMechanism() throws SaslException {
      // TODO:
      // we must have a means to map a given username to a preferred
      // SRP hash algorithm; otherwise we end up using _always_ SHA.
      // for the time being get it from the mechanism properties map
      // and apply it for all users.
      final String mda = (String) properties.get(SRPRegistry.SRP_HASH);
      srp = SRP.instance(mda == null ? SRPRegistry.SRP_DEFAULT_DIGEST_NAME : mda);
   }

   protected void resetMechanism() throws SaslException {
      s = null;
      A = B = null;
      K = null;
      inMac = outMac = null;
      inCipher = outCipher = null;

      sid = null;
   }

   // javax.security.sasl.SaslServer interface implementation -----------------

   public byte[] evaluateResponse(final byte[] response) throws SaslException {
      switch (state) {
      case 0:
         if (response == null) {
            return null;
         }
         state++;
         return sendProtocolElements(response);
      case 1:
         if (!complete) {
            state++;
            return sendEvidence(response);
         }
         // else fall through
      default:
         throw new IllegalMechanismStateException("evaluateResponse()");
      }
   }

   protected byte[]
   engineUnwrap(final byte[] incoming, final int offset, final int len)
   throws SaslException {
//      if (DEBUG && debuglevel > 8) debug(TRACE, "==> engineUnwrap()");
//
//      if (inMac == null && inCipher == null) {
//         throw new IllegalStateException("connection is not protected");
//      }
//
//      if (DEBUG && debuglevel > 6) debug(TRACE, "Incoming buffer (before security): "+Util.dumpString(incoming, offset, len));
//
//      byte[] data = null;
//      try {
//         InputBuffer frameIn = InputBuffer.getInstance(incoming, offset, len);
//         data = frameIn.getEOS();
//         if (inMac != null) {
//            byte[] received_mac = frameIn.getOS();
//            if (DEBUG && debuglevel > 6) debug(TRACE, "Got C (received MAC): "+Util.dumpString(received_mac));
//            inMac.update(data);
//            if (replayDetection) {
//               inCounter++;
//               if (DEBUG && debuglevel > 6) debug(TRACE, "inCounter="+String.valueOf(inCounter));
//               inMac.update(new byte[] {
//                  (byte)(inCounter >>> 24),
//                  (byte)(inCounter >>> 16),
//                  (byte)(inCounter >>>  8),
//                  (byte) inCounter });
//            }
//            final byte[] computed_mac = inMac.doFinal();
//            if (DEBUG && debuglevel > 6) debug(TRACE, "Computed MAC: "+Util.dumpString(computed_mac));
//            if (!Arrays.equals(received_mac, computed_mac))
//               throw new IntegrityException("engineUnwrap()");
//         }
//         if (inCipher != null) {
//            data = inCipher.doFinal(data);
//         }
//       } catch (IOException x) {
//          if (x instanceof SaslException) {
//             throw (SaslException) x;
//          }
//          throw new SaslException("engineUnwrap()", x);
//       }
//
//      if (DEBUG && debuglevel > 6) debug(TRACE, "Incoming buffer (after security): "+Util.dumpString(data));
//      if (DEBUG && debuglevel > 8) debug(TRACE, "<== engineUnwrap()");
//      return data;

      if (DEBUG && debuglevel > 8) debug(TRACE, "==> engineUnwrap()");

      if (inMac == null && inCipher == null) {
         throw new IllegalStateException("connection is not protected");
      }

      if (DEBUG && debuglevel > 6) debug(TRACE, "Incoming buffer (before security): "+Util.dumpString(incoming, offset, len));

      // at this point one, or both, of confidentiality and integrity protection
      // services are active.

      final byte[] result;
      try {
         if (inMac != null) { // integrity bytes are at the end of the stream
            final int macBytesCount = inMac.length();
            final int payloadLength = len - macBytesCount;
            final byte[] received_mac = new byte[macBytesCount];
            System.arraycopy(incoming, offset + payloadLength, received_mac, 0, macBytesCount);
            if (DEBUG && debuglevel > 6) debug(TRACE, "Got C (received MAC): "+Util.dumpString(received_mac));
            inMac.update(incoming, offset, payloadLength);
            if (replayDetection) {
               inCounter++;
               if (DEBUG && debuglevel > 6) debug(TRACE, "inCounter="+String.valueOf(inCounter));
               inMac.update(new byte[] {
                  (byte)(inCounter >>> 24),
                  (byte)(inCounter >>> 16),
                  (byte)(inCounter >>>  8),
                  (byte) inCounter});
            }

            final byte[] computed_mac = inMac.doFinal();
            if (DEBUG && debuglevel > 6) debug(TRACE, "Computed MAC: "+Util.dumpString(computed_mac));
            if (!Arrays.equals(received_mac, computed_mac)) {
               throw new IntegrityException("engineUnwrap()");
            }

            // deal with the payload, which can be either plain or encrypted
            if (inCipher != null) {
               result = inCipher.doFinal(incoming, offset, payloadLength);
            } else {
               result = new byte[payloadLength];
               System.arraycopy(incoming, offset, result, 0, result.length);
            }
         } else { // no integrity protection; just confidentiality
//            if (inCipher != null) {
               result = inCipher.doFinal(incoming, offset, len);
//            } else {
//               result = new byte[len];
//               System.arraycopy(incoming, offset, result, 0, len);
//            }
         }
      } catch (IOException x) {
         if (x instanceof SaslException) {
            throw (SaslException) x;
         }
         throw new SaslException("engineUnwrap()", x);
      }
      if (DEBUG && debuglevel > 6) debug(TRACE, "Incoming buffer (after security): "+Util.dumpString(result));
      if (DEBUG && debuglevel > 8) debug(TRACE, "<== engineUnwrap()");
      return result;
   }

   protected byte[]
   engineWrap(final byte[] outgoing, final int offset, final int len)
   throws SaslException {
//      if (DEBUG && debuglevel > 8) debug(TRACE, "==> engineWrap()");
//
//      if (outMac == null && outCipher == null) {
//         throw new IllegalStateException("connection is not protected");
//      }
//
//      byte[] data = new byte[len];
//      System.arraycopy(outgoing, offset, data, 0, len);
//
//      if (DEBUG && debuglevel > 6) debug(TRACE, "Outgoing buffer (before security) (hex): "+Util.dumpString(data));
//      if (DEBUG && debuglevel > 6) debug(TRACE, "Outgoing buffer (before security) (str): \""+new String(data)+"\"");
//
//      final byte[] result;
//      try {
//         OutputBuffer frameOut = new OutputBuffer();
//         // Process the data
//         if (outCipher != null) {
//            data = outCipher.doFinal(data);
//            if (DEBUG && debuglevel > 6) debug(TRACE, "Encoding c (encrypted plaintext): "+Util.dumpString(data));
//         } else {
//            if (DEBUG && debuglevel > 6) debug(TRACE, "Encoding p (plaintext): "+Util.dumpString(data));
//         }
//         frameOut.setEOS(data);
//         if (outMac != null) {
//            outMac.update(data);
//            if (replayDetection) {
//               outCounter++;
//               if (DEBUG && debuglevel > 6) debug(TRACE, "outCounter="+String.valueOf(outCounter));
//               outMac.update(new byte[] {
//                  (byte)(outCounter >>> 24),
//                  (byte)(outCounter >>> 16),
//                  (byte)(outCounter >>>  8),
//                  (byte) outCounter});
//            }
//            byte[] C = outMac.doFinal();
//            frameOut.setOS(C);
//            if (DEBUG && debuglevel > 6) debug(TRACE, "Encoding C (integrity checksum): "+Util.dumpString(C));
//         }
//         result = frameOut.wrap();
//
//      } catch (IOException x) {
//         if (x instanceof SaslException) {
//            throw (SaslException) x;
//         }
//         throw new SaslException("engineWrap()", x);
//      }
//
//      if (DEBUG && debuglevel > 8) debug(TRACE, "<== engineWrap()");
//      return result;

      if (DEBUG && debuglevel > 8) debug(TRACE, "==> engineWrap()");

      if (outMac == null && outCipher == null) {
         throw new IllegalStateException("connection is not protected");
      }

      if (DEBUG && debuglevel > 6) debug(TRACE, "Outgoing buffer (before security) (hex): "+Util.dumpString(outgoing, offset, len));
      if (DEBUG && debuglevel > 6) debug(TRACE, "Outgoing buffer (before security) (str): \""+new String(outgoing, offset, len)+"\"");

      // at this point one, or both, of confidentiality and integrity protection
      // services are active.

      byte[] result;
      try {
         final ByteArrayOutputStream out = new ByteArrayOutputStream();
         if (outCipher != null) {
            result = outCipher.doFinal(outgoing, offset, len);
            if (DEBUG && debuglevel > 6) debug(TRACE, "Encoding c (encrypted plaintext): "+Util.dumpString(result));

            out.write(result);

            if (outMac != null) {
               outMac.update(result);
               if (replayDetection) {
                  outCounter++;
                  if (DEBUG && debuglevel > 6) debug(TRACE, "outCounter="+String.valueOf(outCounter));
                  outMac.update(new byte[] {
                     (byte)(outCounter >>> 24),
                     (byte)(outCounter >>> 16),
                     (byte)(outCounter >>>  8),
                     (byte) outCounter });
               }
               final byte[] C = outMac.doFinal();
               out.write(C);
               if (DEBUG && debuglevel > 6) debug(TRACE, "Encoding C (integrity checksum): "+Util.dumpString(C));
            } // else ciphertext only; do nothing
         } else { // no confidentiality; just integrity [+ replay detection]
            if (DEBUG && debuglevel > 6) debug(TRACE, "Encoding p (plaintext): "+Util.dumpString(outgoing, offset, len));

            out.write(outgoing, offset, len);

//            if (outMac != null) {
            outMac.update(outgoing, offset, len);
            if (replayDetection) {
               outCounter++;
               if (DEBUG && debuglevel > 6) debug(TRACE, "outCounter="+String.valueOf(outCounter));
               outMac.update(new byte[] {
                  (byte)(outCounter >>> 24),
                  (byte)(outCounter >>> 16),
                  (byte)(outCounter >>>  8),
                  (byte) outCounter });
            }
            final byte[] C = outMac.doFinal();
            out.write(C);
            if (DEBUG && debuglevel > 6) debug(TRACE, "Encoding C (integrity checksum): "+Util.dumpString(C));
//            } // else plaintext only; do nothing
         }

         result = out.toByteArray();

      } catch (IOException x) {
         if (x instanceof SaslException) {
            throw (SaslException) x;
         }
         throw new SaslException("engineWrap()", x);
      }

      if (DEBUG && debuglevel > 8) debug(TRACE, "<== engineWrap()");
      return result;
   }

   protected String getNegotiatedQOP() {
      if (inMac != null) {
         if (inCipher != null) {
            return Registry.QOP_AUTH_CONF;
         } else {
            return Registry.QOP_AUTH_INT;
         }
      }
      return Registry.QOP_AUTH;
   }

   protected String getNegotiatedStrength() {
      if (inMac != null) {
         if (inCipher != null) {
            return Registry.STRENGTH_HIGH;
         } else {
            return Registry.STRENGTH_MEDIUM;
         }
      }
      return Registry.STRENGTH_LOW;
   }

   protected String getNegotiatedRawSendSize() {
      return String.valueOf(rawSendSize);
   }

   protected String getReuse() {
      return Registry.REUSE_TRUE;
   }

   // other methods -----------------------------------------------------------

   private byte[] sendProtocolElements(final byte[] input) throws SaslException {
      if (DEBUG && debuglevel > 8) debug(TRACE, "==> sendProtocolElements()");
      if (DEBUG && debuglevel > 6) debug(TRACE, "C: "+Util.dumpString(input));

      // Client send U, I, sid, cn
      final InputBuffer frameIn = new InputBuffer(input);
      try {
         U = frameIn.getText(); // Extract username
         if (DEBUG && debuglevel > 6) debug(TRACE, "Got U (username): \""+U+"\"");
         authorizationID = frameIn.getText(); // Extract authorisation ID
         if (DEBUG && debuglevel > 6) debug(TRACE, "Got I (userid): \""+authorizationID+"\"");
         sid = frameIn.getEOS();
         if (DEBUG && debuglevel > 6) debug(TRACE, "Got sid (session ID): "+new String(sid));
         cn = frameIn.getOS();
         if (DEBUG && debuglevel > 6) debug(TRACE, "Got cn (client nonce): "+Util.dumpString(cn));
         cCB = frameIn.getEOS();
         if (DEBUG && debuglevel > 6) debug(TRACE, "Got cCB (client channel binding): "+Util.dumpString(cCB));
      } catch (IOException x) {
         if (x instanceof SaslException) {
            throw (SaslException) x;
         }
         throw new AuthenticationException("sendProtocolElements()", x);
      }

      // do/can we re-use?
      if (ServerStore.instance().isAlive(sid)) {
         final SecurityContext ctx = ServerStore.instance().restoreSession(sid);
         srp = SRP.instance(ctx.getMdName());
         K = ctx.getK();
         cIV = ctx.getClientIV();
         sIV = ctx.getServerIV();
         replayDetection = ctx.hasReplayDetection();
         inCounter = ctx.getInCounter();
         outCounter = ctx.getOutCounter();
         inMac = ctx.getInMac();
         outMac = ctx.getOutMac();
         inCipher = ctx.getInCipher();
         outCipher = ctx.getOutCipher();

         if (sn == null || sn.length != 16) {
            sn = new byte[16];
         }
         PRNG.nextBytes(sn);

         setupSecurityServices(false);

         final OutputBuffer frameOut = new OutputBuffer();
         try {
            frameOut.setScalar(1, 0xFF);
            frameOut.setOS(sn);
            frameOut.setEOS(channelBinding);
         } catch (IOException x) {
            if (x instanceof SaslException) {
               throw (SaslException) x;
            }
            throw new AuthenticationException("sendProtocolElements()", x);
         }
         final byte[] result = frameOut.encode();
         if (DEBUG && debuglevel > 8) debug(TRACE, "<== sendProtocolElements()");
         if (DEBUG && debuglevel > 2) debug(INFO, "Old session...");
         if (DEBUG && debuglevel > 2) debug(INFO, "S: "+Util.dumpString(result));
         if (DEBUG && debuglevel > 2) debug(INFO, "  sn = "+Util.dumpString(sn));
         if (DEBUG && debuglevel > 2) debug(INFO, " sCB = "+Util.dumpString(channelBinding));
         return result;
      } else { // new session
         authenticator.activate(properties);

         // -------------------------------------------------------------------
         final HashMap mapB = new HashMap();
//         mapB.put(SRP6KeyAgreement.HASH_FUNCTION,    srp.newDigest());
         mapB.put(SRP6KeyAgreement.HASH_FUNCTION,    srp.getAlgorithm());
         mapB.put(SRP6KeyAgreement.HOST_PASSWORD_DB, authenticator);

         try {
            serverHandler.init(mapB);
            OutgoingMessage out = new OutgoingMessage();
            out.writeString(U);
            IncomingMessage in = new IncomingMessage(out.toByteArray());
            out = serverHandler.processMessage(in);

            in = new IncomingMessage(out.toByteArray());
            N = in.readMPI();
            g = in.readMPI();
            s = in.readMPI().toByteArray();
            B = in.readMPI();
         } catch (KeyAgreementException x) {
            throw new SaslException("sendProtocolElements()", x);
         }
         // -------------------------------------------------------------------

         if (DEBUG && debuglevel > 6) debug(TRACE, "Encoding N (modulus): "+Util.dump(N));
         if (DEBUG && debuglevel > 6) debug(TRACE, "Encoding g (generator): "+Util.dump(g));
         if (DEBUG && debuglevel > 6) debug(TRACE, "Encoding s (client's salt): "+Util.dumpString(s));
         if (DEBUG && debuglevel > 6) debug(TRACE, "Encoding B (server ephemeral public key): "+Util.dump(B));

         // The server creates an options list (L), which consists of a
         // comma-separated list of option strings that specify the security
         // service options the server supports.
         L = createL();
         if (DEBUG && debuglevel > 6) debug(TRACE, "Encoding L (available options): \""+L+"\"");
         if (DEBUG && debuglevel > 6) debug(TRACE, "Encoding sIV (server IV): "+Util.dumpString(sIV));

         final OutputBuffer frameOut = new OutputBuffer();
         try {
            frameOut.setScalar(1, 0x00);
            frameOut.setMPI(N);
            frameOut.setMPI(g);
            frameOut.setOS(s);
            frameOut.setMPI(B);
            frameOut.setText(L);
         } catch (IOException x) {
            if (x instanceof SaslException) {
               throw (SaslException) x;
            }
            throw new AuthenticationException("sendProtocolElements()", x);
         }
         final byte[] result = frameOut.encode();
         if (DEBUG && debuglevel > 8) debug(TRACE, "<== sendProtocolElements()");
         if (DEBUG && debuglevel > 2) debug(INFO, "New session...");
         if (DEBUG && debuglevel > 2) debug(INFO, "S: "+Util.dumpString(result));
         if (DEBUG && debuglevel > 2) debug(INFO, "   N = 0x"+N.toString(16));
         if (DEBUG && debuglevel > 2) debug(INFO, "   g = 0x"+g.toString(16));
         if (DEBUG && debuglevel > 2) debug(INFO, "   s = "+Util.dumpString(s));
         if (DEBUG && debuglevel > 2) debug(INFO, "   B = 0x"+B.toString(16));
         if (DEBUG && debuglevel > 2) debug(INFO, "   L = "+L);
         return result;
      }
   }

   private byte[] sendEvidence(final byte[] input) throws SaslException {
      if (DEBUG && debuglevel > 8) debug(TRACE, "==> sendEvidence()");
      if (DEBUG && debuglevel > 6) debug(TRACE, "C: "+Util.dumpString(input));

      // Client send A, M1, o, cIV
      final InputBuffer frameIn = new InputBuffer(input);
      final byte[] M1;
      try {
         A = frameIn.getMPI(); // Extract client's ephemeral public key
         if (DEBUG && debuglevel > 6) debug(TRACE, "Got A (client ephemeral public key): "+Util.dump(A));
         M1 = frameIn.getOS(); // Extract evidence
         if (DEBUG && debuglevel > 6) debug(TRACE, "Got M1 (client evidence): "+Util.dumpString(M1));
         o = frameIn.getText(); // Extract client's options list
         if (DEBUG && debuglevel > 6) debug(TRACE, "Got o (client chosen options): \""+o+"\"");
         cIV = frameIn.getOS(); // Extract client's IV
         if (DEBUG && debuglevel > 6) debug(TRACE, "Got cIV (client IV): "+Util.dumpString(cIV));
      } catch (IOException x) {
         if (x instanceof SaslException) {
            throw (SaslException) x;
         }
         throw new AuthenticationException("sendEvidence()", x);
      }

      // Parse client's options and set security layer variables
      parseO(o);

      // ----------------------------------------------------------------------
      try {
         final OutgoingMessage out = new OutgoingMessage();
         out.writeMPI(A);
         final IncomingMessage in = new IncomingMessage(out.toByteArray());
         serverHandler.processMessage(in);
         K = serverHandler.getSharedSecret();
      } catch (KeyAgreementException x) {
         throw new SaslException("sendEvidence()", x);
      }
      // ----------------------------------------------------------------------

      if (DEBUG && debuglevel > 6) debug(TRACE, "K: "+Util.dumpString(K));

      final byte[] expected;
      try {
         expected = srp.generateM1(
               N, g, U, s, A, B, K, authorizationID, L, cn, cCB);
      } catch (UnsupportedEncodingException x) {
         throw new AuthenticationException("sendEvidence()", x);
      }

      // Verify client evidence
      if (!Arrays.equals(M1, expected)) {
         throw new AuthenticationException("M1 mismatch");
      }

      setupSecurityServices(true);

      final byte[] M2;
      try {
         M2 = srp.generateM2(
               A, M1, K, U, authorizationID, o, sid, ttl, cIV, sIV, channelBinding);
      } catch (UnsupportedEncodingException x) {
         throw new AuthenticationException("sendEvidence()", x);
      }

      final OutputBuffer frameOut = new OutputBuffer();
      try {
         frameOut.setOS(M2);
         frameOut.setOS(sIV);
         frameOut.setEOS(sid);
         frameOut.setScalar(4, ttl);
         frameOut.setEOS(channelBinding);
      } catch (IOException x) {
         if (x instanceof SaslException) {
            throw (SaslException) x;
         }
         throw new AuthenticationException("sendEvidence()", x);
      }
      final byte[] result = frameOut.encode();
      if (DEBUG && debuglevel > 2) debug(INFO, "S: "+Util.dumpString(result));
      if (DEBUG && debuglevel > 2) debug(INFO, "  M2 = "+Util.dumpString(M2));
      if (DEBUG && debuglevel > 2) debug(INFO, " sIV = "+Util.dumpString(sIV));
      if (DEBUG && debuglevel > 2) debug(INFO, " sid = "+new String(sid));
      if (DEBUG && debuglevel > 2) debug(INFO, " ttl = "+ttl);
      if (DEBUG && debuglevel > 2) debug(INFO, " sCB = "+Util.dumpString(channelBinding));

      if (DEBUG && debuglevel > 8) debug(TRACE, "<== sendEvidence()");
      return result;
   }

   private String createL() {
      if (DEBUG && debuglevel > 8) debug(TRACE, "==> createL()");

      String s = (String) properties.get(SRPRegistry.SRP_MANDATORY);
      if (s == null) {
         s = SRPRegistry.DEFAULT_MANDATORY;
      }
      if (!SRPRegistry.MANDATORY_NONE.equals(s)
            && !SRPRegistry.OPTION_REPLAY_DETECTION.equals(s)
            && !SRPRegistry.OPTION_INTEGRITY.equals(s)
            && !SRPRegistry.OPTION_CONFIDENTIALITY.equals(s)) {
         if (DEBUG && debuglevel > 4) debug(WARN, "Unrecognised mandatory option ("+s+"). Using default...");
         s = SRPRegistry.DEFAULT_MANDATORY;
      }

      mandatory = s;

      s = (String) properties.get(SRPRegistry.SRP_CONFIDENTIALITY);
      final boolean confidentiality = (s == null
            ? SRPRegistry.DEFAULT_CONFIDENTIALITY
            : Boolean.valueOf(s).booleanValue());

      s = (String) properties.get(SRPRegistry.SRP_INTEGRITY_PROTECTION);
      boolean integrity = (s == null
            ? SRPRegistry.DEFAULT_INTEGRITY
            : Boolean.valueOf(s).booleanValue());

      s = (String) properties.get(SRPRegistry.SRP_REPLAY_DETECTION);
      final boolean replayDetection =
         (s == null ? SRPRegistry.DEFAULT_REPLAY_DETECTION : Boolean.valueOf(s).booleanValue());

      final StringBuffer sb = new StringBuffer();
      sb.append(SRPRegistry.OPTION_SRP_DIGEST).append("=")
            .append(srp.getAlgorithm()).append(",");

      if (!SRPRegistry.MANDATORY_NONE.equals(mandatory)) {
         sb.append(SRPRegistry.OPTION_MANDATORY).append("=")
               .append(mandatory).append(",");
      }
      if (replayDetection) {
         sb.append(SRPRegistry.OPTION_REPLAY_DETECTION).append(",");
         // if replay detection is on then force integrity protection
         integrity = true;
      }

      int i;
      if (integrity) {
         for (i = 0; i < SRPRegistry.INTEGRITY_ALGORITHMS.length; i++) {
             sb.append(SRPRegistry.OPTION_INTEGRITY).append("=")
                   .append(SRPRegistry.INTEGRITY_ALGORITHMS[i]).append(",");
         }
      }

      if (confidentiality) {
         IBlockCipher cipher;
         for (i = 0; i < SRPRegistry.CONFIDENTIALITY_ALGORITHMS.length; i++) {
            cipher = CipherFactory.getInstance(SRPRegistry.CONFIDENTIALITY_ALGORITHMS[i]);
            if (cipher != null) {
               sb.append(SRPRegistry.OPTION_CONFIDENTIALITY).append("=")
                     .append(SRPRegistry.CONFIDENTIALITY_ALGORITHMS[i]).append(",");
            }
         }
      }

      final String result = sb
            .append(SRPRegistry.OPTION_MAX_BUFFER_SIZE).append("=")
            .append(Registry.SASL_BUFFER_MAX_LIMIT).toString();
      if (DEBUG && debuglevel > 8) debug(TRACE, "<== createL()");
      return result;
   }

   // Parse client's options and set security layer variables
   private void parseO(final String o) throws AuthenticationException {
      this.replayDetection = false;
      boolean integrity = false;
      boolean confidentiality = false;
      String option;
      int i;

      final StringTokenizer st = new StringTokenizer(o.toLowerCase(), ",");
      while (st.hasMoreTokens()) {
         option = st.nextToken();
         if (DEBUG && debuglevel > 6) debug(TRACE, "option: <"+option+">");
         if (option.equals(SRPRegistry.OPTION_REPLAY_DETECTION)) {
            replayDetection = true;
         } else if (option.startsWith(SRPRegistry.OPTION_INTEGRITY + "=")) {
            if (integrity) {
               throw new AuthenticationException("Only one integrity algorithm may be chosen");
            } else {
               option = option.substring(option.indexOf('=') + 1);
               if (DEBUG && debuglevel > 6) debug(TRACE, "algorithm: <"+option+">");
               for (i = 0; i < SRPRegistry.INTEGRITY_ALGORITHMS.length; i++) {
                  if (SRPRegistry.INTEGRITY_ALGORITHMS[i].equals(option)) {
                     chosenIntegrityAlgorithm = option;
                     integrity = true;
                     break;
                  }
               }
               if (!integrity) {
                  throw new AuthenticationException("Unknown integrity algorithm: "+option);
               }
            }
         } else if (option.startsWith(SRPRegistry.OPTION_CONFIDENTIALITY + "=")) {
            if (confidentiality) {
               throw new AuthenticationException("Only one confidentiality algorithm may be chosen");
            } else {
               option = option.substring(option.indexOf('=') + 1);
               if (DEBUG && debuglevel > 6) debug(TRACE, "algorithm: <"+option+">");
               for (i = 0; i < SRPRegistry.CONFIDENTIALITY_ALGORITHMS.length; i++) {
                  if (SRPRegistry.CONFIDENTIALITY_ALGORITHMS[i].equals(option)) {
                     chosenConfidentialityAlgorithm = option;
                     confidentiality = true;
                     break;
                  }
               }
               if (!confidentiality) {
                  throw new AuthenticationException("Unknown confidentiality algorithm: "+option);
               }
            }
         } else if (option.startsWith(SRPRegistry.OPTION_MAX_BUFFER_SIZE + "=")) {
            final String maxBufferSize = option.substring(option.indexOf('=') + 1);
            try {
               rawSendSize = Integer.parseInt(maxBufferSize);
               if (rawSendSize > Registry.SASL_BUFFER_MAX_LIMIT || rawSendSize < 1)
                  throw new AuthenticationException("Illegal value for 'maxbuffersize' option");
            } catch (NumberFormatException x) {
               throw new AuthenticationException(SRPRegistry.OPTION_MAX_BUFFER_SIZE
                     + "=" + String.valueOf(maxBufferSize), x);
            }
         }
      }

      // check if client did the right thing
      if (replayDetection) {
         if (!integrity) {
            throw new AuthenticationException("Missing integrity protection algorithm "
               +"but replay detection is chosen");
         }
      }
      if (mandatory.equals(SRPRegistry.OPTION_REPLAY_DETECTION)) {
         if (!replayDetection) {
            throw new AuthenticationException(
                  "Replay detection is mandatory but was not chosen");
         }
      }
      if (mandatory.equals(SRPRegistry.OPTION_INTEGRITY)) {
         if (!integrity) {
            throw new AuthenticationException(
                  "Integrity protection is mandatory but was not chosen");
         }
      }
      if (mandatory.equals(SRPRegistry.OPTION_CONFIDENTIALITY)) {
         if (!confidentiality) {
            throw new AuthenticationException("Confidentiality is mandatory but was not chosen");
         }
      }

      int blockSize = 0;
      if (chosenConfidentialityAlgorithm != null) {
         final IBlockCipher cipher =
               CipherFactory.getInstance(chosenConfidentialityAlgorithm);
         if (cipher != null) {
            blockSize = cipher.defaultBlockSize();
         } else { // should not happen
            throw new AuthenticationException("Confidentiality algorithm ("
                  +chosenConfidentialityAlgorithm+") not available");
         }
      }

      sIV = new byte[blockSize];
      if (blockSize > 0) {
         PRNG.nextBytes(sIV);
      }
   }

   private void setupSecurityServices(final boolean newSession) throws SaslException {
      complete = true; // signal end of authentication phase
      if (newSession) {
         outCounter = inCounter = 0;
         // instantiate cipher if confidentiality protection filter is active
         if (chosenConfidentialityAlgorithm != null) {
            if (DEBUG && debuglevel > 2) debug(INFO, "Activating confidentiality protection filter");
            inCipher =  CALG.getInstance(chosenConfidentialityAlgorithm);
            outCipher = CALG.getInstance(chosenConfidentialityAlgorithm);
          }
         // instantiate hmacs if integrity protection filter is active
         if (chosenIntegrityAlgorithm != null) {
            if (DEBUG && debuglevel > 2) debug(INFO, "Activating integrity protection filter");
            inMac =  IALG.getInstance(chosenIntegrityAlgorithm);
            outMac = IALG.getInstance(chosenIntegrityAlgorithm);
         }

         // generate a new sid if at least integrity is used
         sid = (inMac != null ? ServerStore.getNewSessionID() : new byte[0]);
      } else { // same session new keys
         K = srp.generateKn(K, cn, sn);
      }

      final KDF kdf = KDF.getInstance(K);

      // initialise in/out ciphers if confidentaility protection is used
      if (inCipher != null) {
         outCipher.init(kdf, sIV, Direction.FORWARD);
         inCipher.init(kdf, cIV, Direction.REVERSED);
       }
      // initialise in/out macs if integrity protection is used
      if (inMac != null) {
         outMac.init(kdf);
         inMac.init(kdf);
      }

      if (sid != null && sid.length != 0) { // update the security context and save in map
         if (DEBUG && debuglevel > 2) debug(INFO, "Updating security context for sid = "+new String(sid));
         ServerStore.instance().cacheSession(ttl,
               new SecurityContext(srp.getAlgorithm(), sid, K, cIV, sIV,
                     replayDetection, inCounter, outCounter, inMac, outMac,
                     inCipher, outCipher));
      }
   }
}
