package gnu.crypto.sasl.srp;

// ----------------------------------------------------------------------------
// $Id: SRPClient.java,v 1.9 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.auth.Password;
import gnu.crypto.key.IKeyAgreementParty;
import gnu.crypto.key.IncomingMessage;
import gnu.crypto.key.KeyAgreementFactory;
import gnu.crypto.key.KeyAgreementException;
import gnu.crypto.key.OutgoingMessage;
import gnu.crypto.key.srp6.SRP6KeyAgreement;
import gnu.crypto.assembly.Direction;
import gnu.crypto.cipher.CipherFactory;
import gnu.crypto.cipher.IBlockCipher;
import gnu.crypto.hash.MD5;
import gnu.crypto.sasl.ClientMechanism;
import gnu.crypto.sasl.IllegalMechanismStateException;
import gnu.crypto.sasl.InputBuffer;
import gnu.crypto.sasl.IntegrityException;
import gnu.crypto.sasl.OutputBuffer;
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.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.StringTokenizer;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.DestroyFailedException;
import javax.security.sasl.AuthenticationException;
import javax.security.sasl.SaslClient;
import javax.security.sasl.SaslException;

/**
 * <p>The SASL-SRP client-side mechanism.</p>
 *
 * @version $Revision: 1.9 $
 */
public class SRPClient extends ClientMechanism implements SaslClient {

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

   private static final String NAME = "SRPClient";
//   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 static final HashMap uid2ctx = new HashMap();

   private String uid; // the unique key for this type of client
   private String U; // the authentication identity
   BigInteger N, g, A, B;
   private Password password; // the authentication credentials
   private byte[] s; // the user's salt
   private byte[] cIV, sIV; // client+server IVs, when confidentiality is on
   private byte[] M1, M2; // client+server evidences
   private byte[] cn, sn; // client's and server's nonce
   private SRP srp; // SRP algorithm instance used by this client
   private byte[] sid; // session ID when re-used
   private int ttl; // session time-to-live in seconds
   private byte[] sCB; // the peer's channel binding data

   private String L; // available options
   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 clientHandler =
         KeyAgreementFactory.getPartyAInstance(Registry.SRP_SASL_KA);

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

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

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

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

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

   protected void initMechanism() throws SaslException {
      // we shall keep track of the sid (and the security context of this
      // SRP client) based on the initialisation parameters of an SRP session.
      // we shall compute a unique key for those parameters and key the sid
      // (and the security context) accordingly.
      // 1. compute the mapping key. use MD5 (the fastest) for this purpose
      final MD5 md = new MD5();
      byte[] b;
      b = authorizationID.getBytes();
      md.update(b, 0, b.length);
      b = serverName.getBytes();
      md.update(b, 0, b.length);
      b = protocol.getBytes();
      md.update(b, 0, b.length);
      if (channelBinding.length > 0) {
         md.update(channelBinding, 0, channelBinding.length);
      }
      uid = Util.toBase64(md.digest());
      if (ClientStore.instance().isAlive(uid)) {
         final SecurityContext ctx = ClientStore.instance().restoreSession(uid);
         srp = SRP.instance(ctx.getMdName());
         sid = ctx.getSID();
         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();
      } else {
         sid = new byte[0];
         ttl = 0;
         K = null;
         cIV = null;
         sIV = null;
         cn = null;
         sn = null;
      }
   }

   protected void resetMechanism() throws SaslException {
      try {
         password.destroy();
      } catch (DestroyFailedException dfe) {
         SaslException se = new SaslException ("resetMechanism()");
         se.initCause (dfe);
         throw se;
      }
      password = null;
      M1 = null;
      K = null;
      cIV = null;
      sIV = null;
      inMac = outMac = null;
      inCipher = outCipher = null;

      sid = null;
      ttl = 0;
      cn = null;
      sn = null;
   }

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

   public boolean hasInitialResponse() {
      return true;
   }

   public byte[] evaluateChallenge(final byte[] challenge) throws SaslException {
      switch (state) {
      case 0:
         state++;
         return sendIdentities();
      case 1:
         state++;
         final byte[] result = sendPublicKey(challenge);
         try {
            password.destroy(); //don't need further this session
         } catch(DestroyFailedException x) {
            SaslException se = new SaslException ("sendPublicKey()");
            se.initCause (se);
            throw se;
         }
         return result;
      case 2: // should only occur if session re-use was rejected
         if (!complete) {
            state++;
            return receiveEvidence(challenge);
         }
         // else fall through
      default:
         throw new IllegalMechanismStateException("evaluateChallenge()");
      }
   }

   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");
      }

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

      final byte[] result;
      try {
//         final InputBuffer frameIn = InputBuffer.getInstance(incoming, offset, len);
//         result = frameIn.getEOS();
         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 = frameIn.getOS();
            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(result);
            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[len - macBytesCount];
               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);
//            }
         }
//         if (inCipher != null) {
//            result = inCipher.doFinal(result);
//         }
      } catch (IOException x) {
         if (x instanceof SaslException) {
            throw (SaslException) x;
         }
         throw new SaslException("engineUnwrap()", x);
      }

      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");
      }

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

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

//            frameOut.setEOS(data);
            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();
//               frameOut.setOS(C);
               out.write(C);
               if (DEBUG && debuglevel > 6) debug(TRACE, "Encoding C (integrity checksum): "+Util.dumpString(C));
            } // else confidentiality only; do nothing
         } else { // no confidentiality; just integrity [+ replay detection]
//            if (DEBUG && debuglevel > 6) debug(TRACE, "Encoding p (plaintext): "+Util.dumpString(data));
            if (DEBUG && debuglevel > 6) debug(TRACE, "Encoding p (plaintext): "+Util.dumpString(outgoing, offset, len));

//            frameOut.setEOS(data);
            out.write(outgoing, offset, len);

//            if (outMac != null) {
//               outMac.update(data);
            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();
//               frameOut.setOS(C);
            out.write(C);
            if (DEBUG && debuglevel > 6) debug(TRACE, "Encoding C (integrity checksum): "+Util.dumpString(C));
//            }
         }

//         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();
         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[] sendIdentities() throws SaslException {
      if (DEBUG && debuglevel > 8) debug(TRACE, "==> sendIdentities()");

      // If necessary, prompt the client for the username and password
      getUsernameAndPassword();

      if (DEBUG && debuglevel > 6) debug(TRACE, "Password: \""+new String(password.getPassword())+"\"");
      if (DEBUG && debuglevel > 6) debug(TRACE, "Encoding U (username): \""+U+"\"");
      if (DEBUG && debuglevel > 6) debug(TRACE, "Encoding I (userid): \""+authorizationID+"\"");

      // if session re-use generate new 16-byte nonce
      if (sid.length != 0) {
         cn = new byte[16];
         PRNG.nextBytes(cn);
      } else {
         cn = new byte[0];
      }

      final OutputBuffer frameOut = new OutputBuffer();
      try {
         frameOut.setText(U);
         frameOut.setText(authorizationID);
         frameOut.setEOS(sid); // session ID to re-use
         frameOut.setOS(cn); // client nonce
         frameOut.setEOS(channelBinding);
      } catch (IOException x) {
         if (x instanceof SaslException) {
            throw (SaslException) x;
         }
         throw new AuthenticationException("sendIdentities()", x);
      }
      final byte[] result = frameOut.encode();
      if (DEBUG && debuglevel > 8) debug(TRACE, "<== sendIdentities()");
      if (DEBUG && debuglevel > 2) debug(INFO, "C: "+Util.dumpString(result));
      if (DEBUG && debuglevel > 2) debug(INFO, "  U = "+U);
      if (DEBUG && debuglevel > 2) debug(INFO, "  I = "+authorizationID);
      if (DEBUG && debuglevel > 2) debug(INFO, "sid = "+new String(sid));
      if (DEBUG && debuglevel > 2) debug(INFO, " cn = "+Util.dumpString(cn));
      if (DEBUG && debuglevel > 2) debug(INFO, "cCB = "+Util.dumpString(channelBinding));
      return result;
   }

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

      // Server sends [00], N, g, s, B, L
      // or [FF], sn, sCB
      final InputBuffer frameIn = new InputBuffer(input);
      final int ack;
      try {
         ack = (int) frameIn.getScalar(1);
         if (ack == 0x00) { // new session
            N = frameIn.getMPI();
            if (DEBUG && debuglevel > 6) debug(TRACE, "Got N (modulus): "+Util.dump(N));
            g = frameIn.getMPI();
            if (DEBUG && debuglevel > 6) debug(TRACE, "Got g (generator): "+Util.dump(g));
            s = frameIn.getOS();
            if (DEBUG && debuglevel > 6) debug(TRACE, "Got s (salt): "+Util.dumpString(s));
            B = frameIn.getMPI();
            if (DEBUG && debuglevel > 6) debug(TRACE, "Got B (server ephermeral public key): "+Util.dump(B));
            L = frameIn.getText();
            if (DEBUG && debuglevel > 6) debug(TRACE, "Got L (available options): \""+L+"\"");
         } else if (ack == 0xFF) { // session re-use
            sn = frameIn.getOS();
            if (DEBUG && debuglevel > 6) debug(TRACE, "Got sn (server nonce): "+Util.dumpString(sn));
            sCB = frameIn.getEOS();
            if (DEBUG && debuglevel > 6) debug(TRACE, "Got sCB (server channel binding): "+Util.dumpString(sCB));
         } else { // unexpected scalar
            throw new SaslException("sendPublicKey(): Invalid scalar ("+ack+") in server's request");
         }
      } catch (IOException x) {
         if (x instanceof SaslException) {
            throw (SaslException) x;
         }
         throw new SaslException("sendPublicKey()", x);
      }

      if (ack == 0x00) { // new session ---------------------------------------
         o = createO(L.toLowerCase()); // do this first to initialise the SRP hash

         final byte[] pBytes; // use ASCII encoding to inter-operate w/ non-java
         pBytes = password.getBytes();

         // ----------------------------------------------------------------------
         final HashMap mapA = new HashMap();
//         mapA.put(SRP6KeyAgreement.HASH_FUNCTION, srp.newDigest());
         mapA.put(SRP6KeyAgreement.HASH_FUNCTION, srp.getAlgorithm());
         mapA.put(SRP6KeyAgreement.USER_IDENTITY, U);
         mapA.put(SRP6KeyAgreement.USER_PASSWORD, pBytes);
         try {
            clientHandler.init(mapA);
            clientHandler.processMessage(null);
         } catch (KeyAgreementException x) {
            throw new SaslException("sendPublicKey()", x);
         }

         // ----------------------------------------------------------------------

         // -------------------------------------------------------------------
         try {
            OutgoingMessage out = new OutgoingMessage();
            out.writeMPI(N);
            out.writeMPI(g);
            out.writeMPI(new BigInteger(1, s));
            out.writeMPI(B);
            IncomingMessage in = new IncomingMessage(out.toByteArray());
            out = clientHandler.processMessage(in);

            in = new IncomingMessage(out.toByteArray());
            A = in.readMPI();
            K = clientHandler.getSharedSecret();
         } catch (KeyAgreementException x) {
            throw new SaslException("sendPublicKey()", x);
         }
         // -------------------------------------------------------------------

         if (DEBUG && debuglevel > 6) debug(TRACE, "K: "+Util.dumpString(K));
         if (DEBUG && debuglevel > 6) debug(TRACE, "Encoding A (client ephemeral public key): "+Util.dump(A));

         try {
            M1 = srp.generateM1(
                  N, g, U, s, A, B, K, authorizationID, L, cn, channelBinding);
         } catch (UnsupportedEncodingException x) {
            throw new AuthenticationException("sendPublicKey()", x);
         }

         if (DEBUG && debuglevel > 6) debug(TRACE, "Encoding o (client chosen options): \""+o+"\"");
         if (DEBUG && debuglevel > 6) debug(TRACE, "Encoding cIV (client IV): \""+Util.dumpString(cIV)+"\"");

         final OutputBuffer frameOut = new OutputBuffer();
         try {
            frameOut.setMPI(A);
            frameOut.setOS(M1);
            frameOut.setText(o);
            frameOut.setOS(cIV);
         } catch (IOException x) {
            if (x instanceof SaslException) {
               throw (SaslException) x;
            }
            throw new AuthenticationException("sendPublicKey()", x);
         }
         final byte[] result = frameOut.encode();
         if (DEBUG && debuglevel > 8) debug(TRACE, "<== sendPublicKey()");
         if (DEBUG && debuglevel > 2) debug(INFO, "New session, or session re-use rejected...");
         if (DEBUG && debuglevel > 2) debug(INFO, "C: "+Util.dumpString(result));
         if (DEBUG && debuglevel > 2) debug(INFO, "  A = 0x"+A.toString(16));
         if (DEBUG && debuglevel > 2) debug(INFO, " M1 = "+Util.dumpString(M1));
         if (DEBUG && debuglevel > 2) debug(INFO, "  o = "+o);
         if (DEBUG && debuglevel > 2) debug(INFO, "cIV = "+Util.dumpString(cIV));

         return result;
      } else { // session re-use accepted -------------------------------------
         setupSecurityServices(true);
         if (DEBUG && debuglevel > 8) debug(TRACE, "<== sendPublicKey()");
         if (DEBUG && debuglevel > 2) debug(INFO, "Session re-use accepted...");
         return null;
      }
   }

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

      // Server send M2, sIV, sCB, sid, ttl
      final InputBuffer frameIn = new InputBuffer(input);
      try {
         M2 = frameIn.getOS();
         if (DEBUG && debuglevel > 6) debug(TRACE, "Got M2 (server evidence): "+Util.dumpString(M2));
         sIV = frameIn.getOS();
         if (DEBUG && debuglevel > 6) debug(TRACE, "Got sIV (server IV): "+Util.dumpString(sIV));
         sid = frameIn.getEOS();
         if (DEBUG && debuglevel > 6) debug(TRACE, "Got sid (session ID): "+new String(sid));
         ttl = (int) frameIn.getScalar(4);
         if (DEBUG && debuglevel > 6) debug(TRACE, "Got ttl (session time-to-live): "+ttl+"sec.");
         sCB = frameIn.getEOS();
         if (DEBUG && debuglevel > 6) debug(TRACE, "Got sCB (server channel binding): "+Util.dumpString(sCB));
      } catch (IOException x) {
         if (x instanceof SaslException) {
            throw (SaslException) x;
         }
         throw new AuthenticationException("receiveEvidence()", x);
      }

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

      if (DEBUG && debuglevel > 6) debug(TRACE, "Expected: "+Util.dumpString(expected));
      if (!Arrays.equals(M2, expected)) {
         throw new AuthenticationException("M2 mismatch");
      }

      setupSecurityServices(false);

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

   private void getUsernameAndPassword() throws AuthenticationException {
      try {
         if ((!properties.containsKey(Registry.SASL_USERNAME))
               && (!properties.containsKey(Registry.SASL_PASSWORD))) {
            final NameCallback nameCB;
            final String defaultName = System.getProperty("user.name");
            if (defaultName == null) {
               nameCB = new NameCallback("username: ");
            } else {
               nameCB = new NameCallback("username: ",defaultName);
            }
            final PasswordCallback pwdCB = new PasswordCallback("password: ", false);
            handler.handle(new Callback[] { nameCB, pwdCB });
            U = nameCB.getName();
            password = new Password(pwdCB.getPassword());
         } else {
            if (properties.containsKey(Registry.SASL_USERNAME)) {
               this.U = (String)properties.get(Registry.SASL_USERNAME);
            } else {
               final NameCallback nameCB;
               final String defaultName = System.getProperty("user.name");
               if (defaultName == null) {
                  nameCB = new NameCallback("username: ");
               } else {
                  nameCB = new NameCallback("username: ", defaultName);
               }
               this.handler.handle(new Callback[] { nameCB });
               this.U = nameCB.getName();
            }

            if (properties.containsKey(Registry.SASL_PASSWORD)) {
               Object pw = properties.get (Registry.SASL_PASSWORD);
               if (pw instanceof char[])
                 password = new Password((char[]) pw);
               else if (pw instanceof Password)
                 password = (Password) pw;
               else if (pw instanceof String)
                 password = new Password(((String) pw).toCharArray());
               else
                 throw new IllegalArgumentException (pw.getClass().getName() +
                                                     "is not a valid password class");
            } else {
               final PasswordCallback pwdCB = new PasswordCallback("password: ", false);
               this.handler.handle(new Callback[] { pwdCB });
               password = new Password(pwdCB.getPassword());
            }
         }

         if (U == null) {
            throw new AuthenticationException("null username supplied");
         }
         if (password == null) {
            throw new AuthenticationException("null password supplied");
         }
      } catch (UnsupportedCallbackException x) {
         throw new AuthenticationException("getUsernameAndPassword()", x);
      } catch (IOException x) {
         throw new AuthenticationException("getUsernameAndPassword()", x);
      }
   }

   // We go through the list of available services and for each available one
   // we decide whether or not we want it enabled, based on properties passed
   // to us by the client.
   private String createO(final String aol) throws AuthenticationException {
      if (DEBUG && debuglevel > 8) debug(TRACE, "==> createO(\""+aol+"\")");

      boolean replaydetectionAvailable = false;
      boolean integrityAvailable = false;
      boolean confidentialityAvailable = false;
      String option, mandatory = SRPRegistry.DEFAULT_MANDATORY;
      int i;

      String mdName = SRPRegistry.SRP_DEFAULT_DIGEST_NAME;

      final StringTokenizer st = new StringTokenizer(aol, ",");
      while (st.hasMoreTokens()) {
         option = st.nextToken();
         if (option.startsWith(SRPRegistry.OPTION_SRP_DIGEST + "=")) {
            option = option.substring(option.indexOf('=') + 1);
            if (DEBUG && debuglevel > 6) debug(TRACE, "mda: <"+option+">");
            for (i = 0; i < SRPRegistry.INTEGRITY_ALGORITHMS.length; i++) {
               if (SRPRegistry.SRP_ALGORITHMS[i].equals(option)) {
                  mdName = option;
                  break;
               }
            }
         } else if (option.equals(SRPRegistry.OPTION_REPLAY_DETECTION)) {
            replaydetectionAvailable = true;
         } else if (option.startsWith(SRPRegistry.OPTION_INTEGRITY + "=")) {
            option = option.substring(option.indexOf('=') + 1);
            if (DEBUG && debuglevel > 6) debug(TRACE, "ialg: <"+option+">");
            for (i = 0; i < SRPRegistry.INTEGRITY_ALGORITHMS.length; i++) {
               if (SRPRegistry.INTEGRITY_ALGORITHMS[i].equals(option)) {
                  chosenIntegrityAlgorithm = option;
                  integrityAvailable = true;
                  break;
               }
            }
         } else if (option.startsWith(SRPRegistry.OPTION_CONFIDENTIALITY + "=")) {
            option = option.substring(option.indexOf('=') + 1);
            if (DEBUG && debuglevel > 6) debug(TRACE, "calg: <"+option+">");
            for (i = 0; i < SRPRegistry.CONFIDENTIALITY_ALGORITHMS.length; i++) {
               if (SRPRegistry.CONFIDENTIALITY_ALGORITHMS[i].equals(option)) {
                  chosenConfidentialityAlgorithm = option;
                  confidentialityAvailable = true;
                  break;
               }
            }
         } else if (option.startsWith(SRPRegistry.OPTION_MANDATORY + "=")) {
            mandatory = option.substring(option.indexOf('=') + 1);
         } 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);
            }
         }
      }

      replayDetection = replaydetectionAvailable && Boolean.valueOf(
            (String) properties.get(SRPRegistry.SRP_REPLAY_DETECTION)).booleanValue();
      boolean integrity = integrityAvailable && Boolean.valueOf(
            (String) properties.get(SRPRegistry.SRP_INTEGRITY_PROTECTION)).booleanValue();
      boolean confidentiality = confidentialityAvailable && Boolean.valueOf(
            (String) properties.get(SRPRegistry.SRP_CONFIDENTIALITY)).booleanValue();

      // make sure we do the right thing
      if (SRPRegistry.OPTION_REPLAY_DETECTION.equals(mandatory)) {
         replayDetection = true;
         integrity = true;
      } else if (SRPRegistry.OPTION_INTEGRITY.equals(mandatory)) {
         integrity = true;
      } else if (SRPRegistry.OPTION_CONFIDENTIALITY.equals(mandatory)) {
         confidentiality = true;
      }
      if (replayDetection) {
         if (chosenIntegrityAlgorithm == null) {
            throw new AuthenticationException("Replay detection is required but no "
               +"integrity protection algorithm was chosen");
         }
      }
      if (integrity) {
         if (chosenIntegrityAlgorithm == null) {
            throw new AuthenticationException("Integrity protection is required but no "
               +"algorithm was chosen");
         }
      }
      if (confidentiality) {
         if (chosenConfidentialityAlgorithm == null) {
            throw new AuthenticationException("Confidentiality protection is required "
               +"but no algorithm was chosen");
         }
      }

      // 1. check if we'll be using confidentiality; if not set IV to 0-byte
      if (chosenConfidentialityAlgorithm == null) {
         cIV = new byte[0];
      } else {
         // 2. get the block size of the cipher
         final IBlockCipher cipher =
               CipherFactory.getInstance(chosenConfidentialityAlgorithm);
         if (cipher == null) {
            throw new AuthenticationException("createO()", new NoSuchAlgorithmException());
         }
         final int blockSize = cipher.defaultBlockSize();
         // 3. generate random iv
         cIV = new byte[blockSize];
         PRNG.nextBytes(cIV);
      }

      srp = SRP.instance(mdName);

      // Now create the options list specifying which of the available options
      // we have chosen.

      // For now we just select the defaults. Later we need to add support for
      // properties (perhaps in a file) where a user can specify the list of
      // algorithms they would prefer to use.

      final StringBuffer sb = new StringBuffer();
      sb.append(SRPRegistry.OPTION_SRP_DIGEST).append("=")
            .append(mdName).append(",");
      if (replayDetection) {
         sb.append(SRPRegistry.OPTION_REPLAY_DETECTION).append(",");
      }
      if (integrity) {
         sb.append(SRPRegistry.OPTION_INTEGRITY).append("=")
               .append(chosenIntegrityAlgorithm).append(",");
      }
      if (confidentiality) {
         sb.append(SRPRegistry.OPTION_CONFIDENTIALITY).append("=")
               .append(chosenConfidentialityAlgorithm).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, "<== createO() --> "+result);
      return result;
   }

   private void setupSecurityServices(final boolean sessionReUse)
   throws SaslException {
      complete = true; // signal end of authentication phase
      if (!sessionReUse) {
         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);
         }
      } else { // same session new Keys
         K = srp.generateKn(K, cn, sn);
      }

      final KDF kdf = KDF.getInstance(K);

      // initialise in/out ciphers if confidentiality protection is used
      if (inCipher != null) {
         inCipher.init(kdf, sIV, Direction.REVERSED);
         outCipher.init(kdf, cIV, Direction.FORWARD);
      }
      // initialise in/out macs if integrity protection is used
      if (inMac != null) {
         inMac.init(kdf);
         outMac.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 UID = "+uid);
         ClientStore.instance().cacheSession(uid, ttl,
               new SecurityContext(srp.getAlgorithm(), sid, K, cIV, sIV,
                     replayDetection, inCounter, outCounter, inMac, outMac,
                     inCipher, outCipher));
      }
   }
}
