/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2008, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.security.srp;

import java.io.IOException;
import java.rmi.RemoteException;
import java.rmi.server.RMIClientSocketFactory;
import java.rmi.server.RMIServerSocketFactory;
import java.rmi.server.UnicastRemoteObject;
import java.security.GeneralSecurityException;
import java.security.KeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.SealedObject;

import org.jboss.logging.Logger;
import org.jboss.crypto.CryptoUtil;
import org.jboss.security.srp.SRPVerifierStore.VerifierInfo;

/** An implementation of the RMI SRPRemoteServerInterface interface.

 @author Scott.Stark@jboss.org
 @version $Revision: 81038 $
 */
public class SRPRemoteServer extends UnicastRemoteObject implements SRPRemoteServerInterface
{
   private static Logger log = Logger.getLogger(SRPRemoteServer.class);
   /** The counter used to unique user sessions */
   private static int userSessionCount = 0;
   /** A map of <SRPSessionKey, SRPServerSession> for the active sessions */
   private Map sessionMap = Collections.synchronizedMap(new HashMap());

   /** The store implementation providing the SRP VerifierInfo */
   private SRPVerifierStore verifierStore;
   /** The callback interface for SRP session events. */
   private SRPServerListener listener;
   /** A flag indicating if the client must supply an aux challenge */
   private boolean requireAuxChallenge;

   public SRPRemoteServer(SRPVerifierStore verifierStore) throws RemoteException
   {
      setVerifierStore(verifierStore);
   }

   public SRPRemoteServer(SRPVerifierStore verifierStore, int port) throws RemoteException
   {
      super(port);
      setVerifierStore(verifierStore);
   }

   public SRPRemoteServer(SRPVerifierStore verifierStore, int port, RMIClientSocketFactory csf,
         RMIServerSocketFactory ssf) throws RemoteException
   {
      super(port, csf, ssf);
      setVerifierStore(verifierStore);
   }

   /**
    */
   public void setVerifierStore(SRPVerifierStore verifierStore)
   {
      this.verifierStore = verifierStore;
      log.info("setVerifierStore, " + verifierStore);
   }

   public void addSRPServerListener(SRPServerListener listener)
   {
      this.listener = listener;
   }

   public void removeSRPServerListener(SRPServerListener listener)
   {
      if (this.listener == listener)
         this.listener = null;
   }

   public boolean getRequireAuxChallenge()
   {
      return this.requireAuxChallenge;
   }
   public void setRequireAuxChallenge(boolean flag)
   {
      this.requireAuxChallenge = flag;
   }

   /** The start of a new client session.
    */
   public SRPParameters getSRPParameters(String username)
         throws KeyException, RemoteException
   {
      Object[] params = this.getSRPParameters(username,false);
      SRPParameters srpParams = (SRPParameters) params[0];
      return srpParams;
   }

   public Object[] getSRPParameters(String username, boolean multipleSessions)
         throws KeyException, RemoteException
   {
      boolean trace = log.isTraceEnabled();
      if( trace )
         log.trace("getSRPParameters, " + username);
      SRPParameters params = null;
      VerifierInfo info = null;
      try
      {
         info = verifierStore.getUserVerifier(username);
         if (info == null)
            throw new KeyException("Unknown username: " + username);
         params = new SRPParameters(info.N, info.g, info.salt,
            info.hashAlgorithm, info.cipherAlgorithm, info.cipherIV);
         if (log.isTraceEnabled())
         {
            log.trace("Params: " + params);
            byte[] hn = CryptoUtil.newDigest().digest(params.N);
            log.trace("H(N): " + CryptoUtil.tob64(hn));
            byte[] hg = CryptoUtil.newDigest().digest(params.g);
            log.trace("H(g): " + CryptoUtil.tob64(hg));
         }
      }
      catch (IOException e)
      {
         throw new RemoteException("Error during user info retrieval", e);
      }
      catch (KeyException e)
      {
         throw e;
      }
      catch (Throwable t)
      {
         log.error("Unexpected exception in getSRPParameters", t);
         throw new RemoteException("Unexpected exception in getSRPParameters", t);
      }

      // Generate a session id if the user may run multiple sessions
      Integer sessionID = SRPSessionKey.NO_SESSION_ID;
      if( multipleSessions == true )
         sessionID = nextSessionID();
      Object[] sessionInfo = {params, sessionID};
      // Create an SRP session
      SRPSessionKey key = new SRPSessionKey(username, sessionID);
      SRPServerSession session = new SRPServerSession(username, info.verifier,
            params);
      sessionMap.put(key, session);
      if( trace )
         log.trace("getSRPParameters, completed " + key);

      return sessionInfo;
   }

   public byte[] init(String username, byte[] A) throws SecurityException,
         NoSuchAlgorithmException, RemoteException
   {
      return this.init(username, A, 0);
   }
   public byte[] init(String username, byte[] A, int sessionID) throws SecurityException,
         NoSuchAlgorithmException, RemoteException
   {
      SRPSessionKey key = new SRPSessionKey(username, sessionID);
      boolean trace = log.isTraceEnabled();
      if( trace )
         log.trace("init, " + key);
      SRPServerSession session = (SRPServerSession) sessionMap.get(key);
      if (session == null)
         throw new SecurityException("Failed to find active session for username: " + username);

      byte[] B = session.exponential();
      session.buildSessionKey(A);
      if( trace )
         log.trace("init, completed "+key);
      return B;
   }

   public byte[] verify(String username, byte[] M1)
         throws SecurityException, RemoteException
   {
      return this.verify(username, M1, null, 0);
   }
   public byte[] verify(String username, byte[] M1, int sessionID)
         throws SecurityException, RemoteException
   {
      return this.verify(username, M1, null, sessionID);
   }

   /** Verify the session key hash. The client sends their username and M1
    hash to validate completion of the SRP handshake.

    @param username - the user ID by which the client is known. This is repeated to simplify
    the server session management.
    @param M1 - the client hash of the session key; M1 = H(H(N) xor H(g) | H(U) | A | B | K)
    @param auxChallenge - an arbitrary addition data item that my be used as an additional
    challenge. One example usage would be to send a hardware generated token that was encrypted
    with the session private key for validation by the server.
    @return M2, the server hash of the client challenge; M2 = H(A | M1 | K)
    @throws SecurityException thrown if M1 cannot be verified by the server
    @throws RemoteException thrown by remote implementations
    */
   public byte[] verify(String username, byte[] M1, Object auxChallenge)
         throws SecurityException, RemoteException
   {
      return this.verify(username, M1, auxChallenge, 0);
   }
   public byte[] verify(String username, byte[] M1, Object auxChallenge, int sessionID)
         throws SecurityException, RemoteException
   {
      SRPSessionKey key = new SRPSessionKey(username, sessionID);
      boolean trace = log.isTraceEnabled();
      if( trace )
         log.trace("verify, " + key);
      SRPServerSession session = (SRPServerSession) sessionMap.get(key);
      if (session == null)
         throw new SecurityException("Failed to find active session for username: " + username);

      if (session.verify(M1) == false)
         throw new SecurityException("Failed to verify M1");

      /* If there is a auxChallenge have the verierStore verify the data
      */
      if( auxChallenge != null )
      {
         // See if this is an encrypted object
         if( auxChallenge instanceof SealedObject )
         {
            if( trace )
               log.trace("Decrypting sealed object");
            SRPParameters params = session.getParameters();
            Object challenge = null;
            try
            {
               byte[] skey = session.getSessionKey();
               Object tmpKey = CryptoUtil.createSecretKey(params.cipherAlgorithm, skey);
               challenge = CryptoUtil.accessSealedObject(params.cipherAlgorithm, tmpKey,
                  params.cipherIV, auxChallenge);
            }
            catch (GeneralSecurityException e)
            {
               throw new RemoteException("Failed to access SealedObject", e);
            }
            auxChallenge = challenge;
         }
         if( trace )
            log.trace("Verifing aux challenge");
         this.verifierStore.verifyUserChallenge(username, auxChallenge);
      }
      else if( requireAuxChallenge == true )
      {
         throw new RemoteException("A non-null auxChallenge is required for verification");
      }

      // Inform the listener the user has been validated
      if (listener != null)
         listener.verifiedUser(key, session);
      if( trace )
         log.trace("verify, completed " + key);

      return session.getServerResponse();
   }

   /** Close the SRP session for the given username.
    */
   public void close(String username) throws SecurityException, RemoteException
   {
      this.close(username, 0);
   }
   public void close(String username, int sessionID) throws SecurityException, RemoteException
   {
      SRPSessionKey key = new SRPSessionKey(username, sessionID);
      boolean trace = log.isTraceEnabled();
      if( trace )
         log.trace("close, " + key);
      SRPServerSession session = (SRPServerSession) sessionMap.remove(key);
      if (session == null)
         throw new SecurityException("Failed to find active session for username: " + username);
      if (listener != null)
         listener.closedUserSession(key);
      if( trace )
         log.trace("close, completed " + key);
   }

   private static synchronized Integer nextSessionID()
   {
      return new Integer(userSessionCount ++);
   }
}
