/* * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. * * Distributable under LGPL license. * See terms of license at gnu.org. */ package net.java.sip.communicator.impl.protocol.sip.security; import java.text.*; import java.util.*; import javax.sip.*; import javax.sip.header.*; import javax.sip.message.*; import net.java.sip.communicator.impl.protocol.sip.*; import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.util.*; /** * The class handles authentication challenges, caches user credentials and * takes care (through the SecurityAuthority interface) about retrieving * passwords. * * @author Emil Ivov * @author Jeroen van Bemmel * @version 1.0 */ public class SipSecurityManager { private static final Logger logger = Logger.getLogger(SipSecurityManager.class); /** * The SecurityAuthority instance that we could use to obtain new passwords * for the user. */ private SecurityAuthority securityAuthority = null; /** * An instance of the header factory that we have to use to create our * authentication headers. */ private HeaderFactory headerFactory = null; /** * Credentials cached so far. */ private CredentialsCache cachedCredentials = new CredentialsCache(); /** * The ID of the account that this security manager instance is serving. */ private AccountID accountID = null; /** * Default constructor for the security manager. * * @param accountID the id of the account that this security manager is * going to serve. */ public SipSecurityManager(AccountID accountID) { this.accountID = accountID; } /** * Set the header factory to be used when creating authorization headers * * @param headerFactory the header factory that we'll be using when creating * authorization headers. */ public void setHeaderFactory(HeaderFactory headerFactory) { this.headerFactory = headerFactory; } /** * Uses securityAuthority to determine a set of valid user credentials * for the specified Response (Challenge) and appends it to the challenged * request so that it could be retransmitted. * * Fredrik Wickstrom reported that dialog cseq counters are not incremented * when resending requests. He later uncovered additional problems and proposed * a way to fix them (his proposition was taken into account). * * @param challenge the 401/407 challenge response * @param challengedTransaction the transaction established by the challenged * request * @param transactionCreator the JAIN SipProvider that we should use to * create the new transaction. * * @return a transaction containing a reoriginated request with the * necessary authorization header. * @throws SipException if we get an exception white creating the * new transaction * @throws InvalidArgumentException if we fail to create a new header * containing user credentials. * @throws ParseException if we fail to create a new header containing user * credentials. * @throws NullPointerException if an argument or a header is null. * @throws OperationFailedException if we fail to acquire a password from * our security authority. */ public ClientTransaction handleChallenge( Response challenge, ClientTransaction challengedTransaction, SipProvider transactionCreator) throws SipException, InvalidArgumentException, ParseException, OperationFailedException, NullPointerException { String branchID = challengedTransaction.getBranchId(); Request challengedRequest = challengedTransaction.getRequest(); Request reoriginatedRequest = (Request) challengedRequest.clone(); //remove the branch id so that we could use the request in a new //transaction removeBranchID(reoriginatedRequest); ListIterator authHeaders = null; if (challenge == null || reoriginatedRequest == null) { throw new NullPointerException( "A null argument was passed to handle challenge."); } if (challenge.getStatusCode() == Response.UNAUTHORIZED) { authHeaders = challenge.getHeaders(WWWAuthenticateHeader.NAME); } else if (challenge.getStatusCode() == Response.PROXY_AUTHENTICATION_REQUIRED) { authHeaders = challenge.getHeaders(ProxyAuthenticateHeader.NAME); } if (authHeaders == null) { throw new NullPointerException( "Could not find WWWAuthenticate or ProxyAuthenticate headers"); } //Remove all authorization headers from the request (we'll re-add them //from cache) reoriginatedRequest.removeHeader(AuthorizationHeader.NAME); reoriginatedRequest.removeHeader(ProxyAuthorizationHeader.NAME); //rfc 3261 says that the cseq header should be augmented for the new //request. do it here so that the new dialog (created together with //the new client transaction) takes it into account. //Bug report - Fredrik Wickstrom CSeqHeader cSeq = (CSeqHeader) reoriginatedRequest.getHeader( (CSeqHeader.NAME)); cSeq.setSeqNumber(cSeq.getSeqNumber() + 1l); ClientTransaction retryTran = transactionCreator.getNewClientTransaction(reoriginatedRequest); WWWAuthenticateHeader authHeader = null; while (authHeaders.hasNext()) { authHeader = (WWWAuthenticateHeader) authHeaders.next(); String realm = authHeader.getRealm(); //Check whether we have cached credentials for authHeader's realm. //We remove them with the intention to re-add them at the end of the //method. If we fail to get to the end then it's best for the cache //entry to remain outside since it might have caused the problem CredentialsCacheEntry ccEntry = cachedCredentials.remove(realm); boolean ccEntryHasSeenTran = false; if (ccEntry != null) ccEntryHasSeenTran = ccEntry.popBranchID(branchID); String storedPassword = SipActivator.getProtocolProviderFactory() .loadPassword(accountID); if(ccEntry == null) { //we haven't yet authentified this realm since we were started. if(storedPassword != null) { //use the stored password to authenticate ccEntry = createCcEntryWithStoredPassword(storedPassword); logger.trace("seem to have a stored pass! Try with it."); } else { //obtain new credentials logger.trace("We don't seem to have a good pass! Get one."); ccEntry = createCcEntryWithNewCredentials( realm, SecurityAuthority.AUTHENTICATION_REQUIRED); if(ccEntry == null) throw new OperationFailedException( "User has canceled the authentication process.", OperationFailedException.AUTHENTICATION_CANCELED); } } else { //we have already authentified against this realm since we were //started. this authentication is either for a different request //or the previous authentication used a wrong pass. if (ccEntryHasSeenTran) { //this is the transaction that created the cc entry. if we //need to authenticate the same transaction then the //credentials we supplied the first time we wrong. //remove password and ask user again. SipActivator.getProtocolProviderFactory().storePassword( accountID, null); ccEntry = createCcEntryWithNewCredentials( realm, SecurityAuthority.WRONG_PASSWORD); if(ccEntry == null) throw new OperationFailedException( "User has canceled the authentication process.", OperationFailedException.AUTHENTICATION_CANCELED); } else { //we have a cache entry and it has not seen this transaction //lets use it again. //(this "else" is here for readability only) logger.trace( "We seem to have a pass in the cache. " +"Let's try with it."); } } //get a new pass if (ccEntry == null // we don't have credentials for the specified //realm || ( (ccEntryHasSeenTran // we have already tried with those && !authHeader.isStale()))) // and this is (!stale) not // just a request to reencode { } //if user canceled or sth else went wrong if (ccEntry.userCredentials == null) { throw new OperationFailedException( "Unable to authenticate with realm " + realm + ". User did not provide credentials." , OperationFailedException.AUTHENTICATION_FAILED); } AuthorizationHeader authorization = this.getAuthorization( reoriginatedRequest.getMethod(), reoriginatedRequest.getRequestURI().toString(), ( reoriginatedRequest.getContent() == null )? "" : reoriginatedRequest.getContent().toString(), authHeader, ccEntry.userCredentials); ccEntry.pushBranchID(retryTran.getBranchId()); cachedCredentials.cacheEntry(realm, ccEntry); logger.debug("Created authorization header: " + authorization.toString()); // get the unique Call-ID CallIdHeader call = (CallIdHeader)reoriginatedRequest .getHeader(CallIdHeader.NAME); if(call != null) { String callid = call.getCallId(); cachedCredentials .cacheAuthorizationHeader (callid, authorization); } reoriginatedRequest.addHeader(authorization); } logger.debug("Returning authorization transaction."); return retryTran; } /** * Sets the SecurityAuthority instance that should be queried for user * credentials. * * @param authority the SecurityAuthority instance that should be queried * for user credentials. */ public void setSecurityAuthority(SecurityAuthority authority) { this.securityAuthority = authority; } /** * Returns the SecurityAuthority instance that SipSecurityManager uses to * obtain user credentials. * * @return the SecurityAuthority instance that SipSecurityManager uses to * obtain user credentials. */ public SecurityAuthority getSecurityAuthority() { return this.securityAuthority; } /** * Makes sure that the password that was used for this forbidden response, * is removed from the local cache and is not stored for future use. * * @param forbidden the 401/407 challenge response * @param endedTransaction the transaction established by the challenged * request * @param transactionCreator the JAIN SipProvider that we should use to * create the new transaction. */ public void handleForbiddenResponse( Response forbidden, ClientTransaction endedTransaction, SipProvider transactionCreator) { //a request that we previously sent was mal-authenticated. empty the //credentials cache so that we don't use the same credentials once more. cachedCredentials.clear(); } /** * Generates an authorisation header in response to wwwAuthHeader. * * @param method method of the request being authenticated * @param uri digest-uri * @param requestBody the body of the request. * @param authHeader the challenge that we should respond to * @param userCredentials username and pass * * @return an authorisation header in response to authHeader. * * @throws OperationFailedException if auth header was malformated. */ private AuthorizationHeader getAuthorization( String method, String uri, String requestBody, WWWAuthenticateHeader authHeader, UserCredentials userCredentials) throws OperationFailedException { String response = null; // JvB: authHeader.getQop() is a quoted _list_ of qop values // (e.g. "auth,auth-int") Client is supposed to pick one String qopList = authHeader.getQop(); String qop = (qopList != null) ? "auth" : null; String nc_value = "00000001"; String cnonce = "xyz"; try { response = MessageDigestAlgorithm.calculateResponse( authHeader.getAlgorithm(), userCredentials.getUserName(), authHeader.getRealm(), new String(userCredentials.getPassword()), authHeader.getNonce(), nc_value, // JvB added cnonce, // JvB added method, uri, requestBody, qop);//jvb changed } catch (NullPointerException exc) { throw new OperationFailedException( "The authenticate header was malformatted" , OperationFailedException.GENERAL_ERROR , exc); } AuthorizationHeader authorization = null; try { if (authHeader instanceof ProxyAuthenticateHeader) { authorization = headerFactory.createProxyAuthorizationHeader( authHeader.getScheme()); } else { authorization = headerFactory.createAuthorizationHeader( authHeader.getScheme()); } authorization.setUsername(userCredentials.getUserName()); authorization.setRealm(authHeader.getRealm()); authorization.setNonce(authHeader.getNonce()); authorization.setParameter("uri", uri); authorization.setResponse(response); if (authHeader.getAlgorithm() != null) { authorization.setAlgorithm(authHeader.getAlgorithm()); } if (authHeader.getOpaque() != null) { authorization.setOpaque(authHeader.getOpaque()); } // jvb added if (qop!=null) { authorization.setQop(qop); authorization.setCNonce(cnonce); authorization.setNonceCount( Integer.parseInt(nc_value) ); } authorization.setResponse(response); } catch (ParseException ex) { throw new SecurityException( "Failed to create an authorization header!"); } return authorization; } /** * Caches <tt>realm</tt> and <tt>credentials</tt> for later usage. * * @param realm the * @param credentials UserCredentials */ public void cacheCredentials(String realm, UserCredentials credentials) { CredentialsCacheEntry ccEntry = new CredentialsCacheEntry(); ccEntry.userCredentials = credentials; this.cachedCredentials.cacheEntry(realm, ccEntry); } /** * Removes all via headers from <tt>request</tt> and replaces them with a * new one, equal to the one that was top most. * * @param request the Request whose branchID we'd like to remove. * * @throws ParseException in case the host port or transport in the original * request were malformed * @throws InvalidArgumentException if the port in the original via header * was invalid. */ private void removeBranchID(Request request) throws ParseException, InvalidArgumentException { ViaHeader viaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME); request.removeHeader(ViaHeader.NAME); ViaHeader newViaHeader = headerFactory.createViaHeader( viaHeader.getHost() , viaHeader.getPort() , viaHeader.getTransport() , null); request.setHeader(newViaHeader); } /** * Obtains user credentials from the security authority for the specified * <tt>realm</tt> and creates a new CredentialsCacheEntry with them. * * @param realm the realm that we'd like to obtain a * <tt>CredentialsCacheEntry</tt> for. * * @return a newly created <tt>CredentialsCacheEntry</tt> corresponding to * the specified <tt>realm</tt>. */ private CredentialsCacheEntry createCcEntryWithNewCredentials( String realm, int reasonCode) { CredentialsCacheEntry ccEntry = new CredentialsCacheEntry(); UserCredentials defaultCredentials = new UserCredentials(); defaultCredentials.setUserName(accountID.getUserID()); UserCredentials newCredentials = getSecurityAuthority().obtainCredentials( realm, defaultCredentials, reasonCode); // in case user has canceled the login window if(newCredentials == null) return null; if(newCredentials.getPassword() == null) return null; ccEntry.userCredentials = newCredentials; //store the password if the user wants us to if( ccEntry.userCredentials != null && ccEntry.userCredentials.isPasswordPersistent()) SipActivator.getProtocolProviderFactory().storePassword( accountID , ccEntry.userCredentials.getPasswordAsString()); return ccEntry; } /** * Creaes a new credentials cache entry using <tt>password</tt>. * * @param password the password that we'd like to use in our the credentials * associated with the new <tt>CredentialsCacheEntry</tt>. * * @return a newly created <tt>CredentialsCacheEntry</tt> using * <tt>password</tt>. */ private CredentialsCacheEntry createCcEntryWithStoredPassword( String password) { CredentialsCacheEntry ccEntry = new CredentialsCacheEntry(); ccEntry.userCredentials = new UserCredentials(); ccEntry.userCredentials.setUserName(accountID.getUserID()); ccEntry.userCredentials.setPassword(password.toCharArray()); return ccEntry; } /** * Returns an authorization header cached against the specified * <tt>callID</tt> or <tt>null</tt> if no auth. header has been previously * cached for this callID. * * @param callID the ID of the call that we'd like to reString * @return the <tt>AuthorizationHeader</tt> cached against the specified * call ID or null if no such header has been cached. */ public AuthorizationHeader getCachedAuthorizationHeader(String callID) { return this.cachedCredentials.getCachedAuthorizationHeader(callID); } }