/*
* Copyright (c) 2008 Nokia Corporation and/or its subsidiary(-ies).
* All rights reserved.
* This component and the accompanying materials are made available
* under the terms of "Eclipse Public License v1.0"
* which accompanies this distribution, and is available
* at the URL "http://www.eclipse.org/legal/epl-v10.html".
*
* Initial Contributors:
* Nokia Corporation - initial contribution.
*
* Contributors:
*
* Description:
*
*/

package com.nokia.mj.impl.security.midp.authentication;

import com.nokia.mj.impl.utils.Uid;
import com.nokia.mj.impl.utils.Tokenizer;
import com.nokia.mj.impl.security.common.InstallerSecurityException;
import com.nokia.mj.impl.security.common.RuntimeSecurityException;
import com.nokia.mj.impl.utils.InstallerDetailedErrorMessage;
import com.nokia.mj.impl.utils.InstallerErrorMessage;
import com.nokia.mj.impl.utils.OtaStatusCode;
import com.nokia.mj.impl.security.utils.SecurityErrorMessage;
import com.nokia.mj.impl.security.utils.SecurityDetailedErrorMessage;
import com.nokia.mj.impl.security.utils.TelUtils;
import com.nokia.mj.impl.security.midp.common.AuthenticationCredentials;
import com.nokia.mj.impl.security.midp.common.AuthenticationInfo;
import com.nokia.mj.impl.security.midp.common.AuthenticationAttribute;
import com.nokia.mj.impl.security.midp.common.SecurityAttributes;
import com.nokia.mj.impl.security.common.Certificate;
import com.nokia.mj.impl.security.common.SecurityCommsMessages;
import com.nokia.mj.impl.security.midp.common.ProtectionDomain;
import com.nokia.mj.impl.security.midp.common.SigningCertificate;
import com.nokia.mj.impl.security.midp.common.SigningInfo;
import com.nokia.mj.impl.security.midp.common.GeneralSecuritySettings;
import com.nokia.mj.impl.security.midp.storage.*;
import com.nokia.mj.impl.storage.StorageSession;
import com.nokia.mj.impl.rt.support.Jvm;
import com.nokia.mj.impl.rt.support.ApplicationInfo;
import com.nokia.mj.impl.security.utils.Logger;
import com.nokia.mj.impl.comms.CommsEndpoint;
import com.nokia.mj.impl.comms.CommsMessage;
import com.nokia.mj.impl.comms.exception.CommsException;
import com.nokia.mj.impl.fileutils.DriveUtilities;
import com.nokia.mj.impl.fileutils.DriveInfo;
import java.util.Hashtable;
import java.util.Vector;

/**
 * MIDP authentication is build around X.509 Public Key Infrastructure so that
 * MIDlet suites are signed using public key certificates and therefore
 * authenticated by verifying the authenticity of the MIDlet suite's signing
 * certificates with the help of trusted (root) certificates.
 * As a result of authentication a MIDlet suite is bound to a protection domain
 * which will be used as a criteria for granting the MIDlet access to protected
 * functionality.
 * This class is used for authenticating MIDlet suites.
 */
public final class AuthenticationModule
{
    static
    {
        Jvm.loadSystemLibrary("javasecurity");
    }

    /*
     * Hashtable containing the all of the authentication credentials
     * of different aplications being installed
     */
    private Hashtable iAuthCredentials;

    /*
     * Hashtable containing the selected authentication credentials of
     * different aplications being installed
     */
    private Hashtable iSelectedAuthCredentials;

    /*
     * Hashtable containing the ocsp checkers corresponding to
     * installation of various MIDlet suites
     */
    private Hashtable iOcspCheckers;

    /*
     * Hashtable containing the ocsp event listeners corresponding to
     * ocsp checks of various MIDlet suites
     */
    private Hashtable iOcspEventListeners;

    // self
    private static AuthenticationModule self;

    // data structure which holds the flags about different suites
    // being legacy suites or not; the flag info is available at
    // authentication JAD operation time, but not available at
    // authentication JAR operation time -> this hashtable carries
    // the flag between authenticating JAD and JAR operations for
    // same suite
    private static Hashtable iLegacySuiteFlags = new Hashtable();

    /**
     * The ocsp settings
     */
    private static OcspSettings iOcspSettings;
    private static OcspUserPreferences iOcspUserPreferences;

    /*
     * The security warnings mode
     */
    private static int iSecurityWarningsMode = GeneralSecuritySettings.UNINITIALIZED_SECURITY_MODE;

    /**
     * Creates an instance of the AuthenticationModule
     *
     * @return An instance of AuthenticationModule
     */
    public static AuthenticationModule getInstance()
    {
        if (self == null)
        {
            self = new AuthenticationModule();
        }
        return self;
    }

    /**
     * Authenticates a certain MIDlet suite. This method is called
     * when/if the JAD is available
     *
     * @param msUID    the UID if the MIDlet suite being authenticated
     * @param oldMSUID the UID if the MIDlet suite being updated
     *                 (if applicable)
     * @param authInfo the authentication info based on which the MIDlet suite
     *                 is authenticated
     * @return         a set of credentials assigned to the MIDlet suite
     */
    public AuthenticationCredentials[] authenticateJad(
        Uid msUID,
        Uid oldMSUID,
        AuthenticationInfo[] authInfo)
    {
        Vector allAuthCredentials = null;
        if (authInfo == null || authInfo.length == 0)
        {
            // this is untrusted MIDlet -> save the protection domain only
            allAuthCredentials = verifyUpdate(
                                     new Credentials[] {new Credentials("UnidentifiedThirdParty",
                                                                        ApplicationInfo.UNIDENTIFIED_THIRD_PARTY_DOMAIN,
                                                                        null, null, -1, null)
                                                       }, oldMSUID);
        }
        else
        {
            try
            {
                // save all the credentials as candidates -> after having the JAR,
                // based on its hash we will select the protection_domain,
                // root_hash and the validated_chain_indexes
                allAuthCredentials = verifyUpdate(
                                         authInfo[0].getLegacyInfo(),
                                         _validateChainsAndSignatures(authInfo), oldMSUID,
                                         true /* order the result */);
                // start an OCSP client if the ocsp checks are enabled
                if (performOcsp())
                {
                    // start the OCSP checks for all the succesfully validated chains
                    if (allAuthCredentials.size() > 0)
                    {
                        OcspData[] ocspData = new OcspData[allAuthCredentials.size()];
                        for (int i=0; i<allAuthCredentials.size(); i++)
                        {
                            Credentials authCredentials =
                                (Credentials)allAuthCredentials.elementAt(i);
                            // need to add the root the to chain when doing the ocsp checks
                            String[] certChain  = authInfo[(authCredentials
                                                            .validatedChainIndex - 1)].getCertChain();
                            String root = getRoot(authCredentials.rootHashValue);
                            if (root != null)
                            {
                                String[] completeCertChain =
                                    new String[certChain.length + 1];
                                int j;
                                for (j=0; j<certChain.length; j++)
                                {
                                    completeCertChain[j] = certChain[j];
                                }
                                completeCertChain[j] = root;
                                certChain = completeCertChain;
                            }
                            ocspData[i] = new OcspData(certChain);
                        }
                        // build the ocspChecker with the newly built cert chains
                        OcspChecker ocspChecker = new OcspChecker(ocspData,
                                iOcspSettings,
                                iOcspUserPreferences,
                                (OcspEventListener)iOcspEventListeners.get(msUID));
                        iOcspCheckers.put(msUID, ocspChecker);
                        // start the OCSP check
                        ocspChecker.start();
                    }
                }
            }
            catch (AuthenticationException e)
            {
                Logger.log(e);
                switch (e.getErrorCode())
                {
                case AuthenticationException.CONNECTION_TO_CAPTAIN_FAILED:
                case AuthenticationException.SENDING_MSG_TO_CAPTAIN_FAILED:
                    throw new InstallerSecurityException(
                        InstallerErrorMessage.INST_UNEXPECTED_ERR,
                        null, /* no params for short msg */
                        InstallerDetailedErrorMessage.INTERNAL_ERROR,
                        new String[] {"Internal communication problem."},
                        OtaStatusCode.INTERNAL_ERROR);
                case AuthenticationException.CERT_VERIFICATION_FAILED:
                case AuthenticationException.CERT_NOT_YET_VALID:
                case AuthenticationException.CERT_EXPIRED:
                case AuthenticationException.ROOT_CERT_IN_CHAIN:
                case AuthenticationException.MISSING_ROOT:
                case AuthenticationException.UNKNOWN_EXT_KEY_USAGE:
                    throw new InstallerSecurityException(
                        InstallerErrorMessage.INST_AUTHENTICATION_ERR,
                        null, /* no params for short msg */
                        InstallerDetailedErrorMessage.CERT_UNSUPPORTED,
                        null, /* no params for detailed msg */
                        OtaStatusCode.APPLICATION_AUTHENTICATION_FAILURE);
                case AuthenticationException.SIG_VERIFICATION_FAILED:
                    throw new InstallerSecurityException(
                        InstallerErrorMessage.INST_AUTHORIZATION_ERR,
                        null, /* no params for short msg */
                        InstallerDetailedErrorMessage.ATTR_HANDLING_FAILED,
                        new String[] {AuthenticationAttribute
                                      .SECOND_LEGACY_ATTRIBUTE_NAME
                                     },
                        OtaStatusCode.APPLICATION_AUTHORIZATION_FAILURE);
                case AuthenticationException.MISSING_DOMAIN_MAPPING:
                    throw new InstallerSecurityException(
                        InstallerErrorMessage.INST_AUTHORIZATION_ERR,
                        null, /* no params for short msg */
                        InstallerDetailedErrorMessage.CERT_UNSUPPORTED,
                        null, /* no params for detailed msg */
                        OtaStatusCode.APPLICATION_AUTHORIZATION_FAILURE);
                }
            }
        }
        iAuthCredentials.put(msUID, allAuthCredentials);
        Credentials[] credentials = new Credentials[allAuthCredentials.size()];
        allAuthCredentials.copyInto(credentials);
        Logger.logAuthenticationCredentials(allAuthCredentials);
        return credentials;
    }

    /**
     * Authenticates a certain MIDlet suite. This method is called
     * when the JAR is available
     *
     * @param storageSession the JavaStorage session to be used when
     *                       storing security data
     * @param msUID          the UID if the MIDlet suite being authenticated
     * @param oldMSUID       the UID if the MIDlet suite being updated
     *                       (if applicable)
     * @param appJARPath     the path to the JAR being authenticated
     */
    public void authenticateJar(
        StorageSession storageSession,
        Uid msUID,
        Uid oldMSUID,
        String appJARPath,
        boolean drmContent)
    {
        AuthenticationStorageData data = null;
        try
        {
            Vector allAuthCredentials = (Vector)iAuthCredentials.get(msUID);
            String jarHash = _computeHash(appJARPath);
            if (jarHash == null || jarHash.length() == 0)
            {
                // could not compute hash for the given application
                Logger.logWarning("Could not compute hash for " + appJARPath);
                throw new InstallerSecurityException(
                    InstallerErrorMessage.INST_UNEXPECTED_ERR,
                    null, /* no params for short msg */
                    InstallerDetailedErrorMessage.INTERNAL_ERROR,
                    new String[] {"Could not compute hash for " + appJARPath},
                    OtaStatusCode.INTERNAL_ERROR);
            }
            if (allAuthCredentials == null
                    || (allAuthCredentials.size() == 1
                        && ((Credentials)allAuthCredentials.elementAt(0))
                        .getProtectionDomainCategory().equals(
                            ApplicationInfo.UNIDENTIFIED_THIRD_PARTY_DOMAIN)))
            {
                data = new AuthenticationStorageData(
                    "UnidentifiedThirdParty",
                    ApplicationInfo.UNIDENTIFIED_THIRD_PARTY_DOMAIN,
                    jarHash,
                    null /*rootHashValue*/,
                    null /*validatedChainIndexes*/,
                    null /* jarPath*/,
                    iSecurityWarningsMode);
                verifyUpdate(
                    new Credentials[] {new Credentials(
                                           data.getProtectionDomain(),
                                           data.getProtectionDomainCategory(),
                                           jarHash,
                                           null /* root hash */,
                                           -1 /* validated chain index*/,
                                           null /* signing cert */)
                                      },
                    oldMSUID);
            }
            else
            {
                // get the authentication credential candidates and select the right
                // domain/root_hash and validated_chain_index based on the hash of
                // the JAR
                String protectionDomainName = null;
                String protectionDomainCategory = null;
                String jarHashValue = null;
                String rootHashValue = null;
                Vector validatedChainIndexes = new Vector();
                Credentials selectedCredentials = selectCredentials(jarHash, allAuthCredentials, validatedChainIndexes);
                if (selectedCredentials == null
                        || selectedCredentials.getProtectionDomainName() == null)
                {
                    if (!drmContent)
                    {
                        Logger.logWarning("Signature verification failure: the computed and the decrypted JAR hashes do not match");
                        throw new InstallerSecurityException(
                            InstallerErrorMessage.INST_AUTHORIZATION_ERR,
                            null, /* no params for short msg */
                            InstallerDetailedErrorMessage.ATTR_HANDLING_FAILED,
                            new String[] {AuthenticationAttribute
                                          .SECOND_LEGACY_ATTRIBUTE_NAME
                                         },
                            OtaStatusCode.APPLICATION_AUTHORIZATION_FAILURE);
                    }
                    else
                    {
                        // give it one more try
                        String drmJarHash = _drmDecryptAndComputeHash(appJARPath);
                        validatedChainIndexes.removeAllElements();
                        selectedCredentials = selectCredentials(drmJarHash, allAuthCredentials, validatedChainIndexes);
                        if (selectedCredentials == null
                                || selectedCredentials.getProtectionDomainName() == null)
                        {
                            Logger.logWarning("Signature verification failure: the computed and the decrypted JAR hashes do not match");
                            throw new InstallerSecurityException(
                                InstallerErrorMessage.INST_AUTHORIZATION_ERR,
                                null, /* no params for short msg */
                                InstallerDetailedErrorMessage.ATTR_HANDLING_FAILED,
                                new String[] {AuthenticationAttribute
                                              .SECOND_LEGACY_ATTRIBUTE_NAME
                                             },
                                OtaStatusCode.APPLICATION_AUTHORIZATION_FAILURE);
                        }
                        // re-set the jar hash to the hash of what acts as JAR
                        // (not the JAR from inside the DRM content), so no
                        // need to do the DRM decrypting when doing the
                        // tamper detection at runtime
                        selectedCredentials.jarHashValue = jarHash;
                    }
                }
                data = new AuthenticationStorageData(
                    selectedCredentials.getProtectionDomainName(),
                    selectedCredentials.getProtectionDomainCategory(),
                    selectedCredentials.jarHashValue,
                    selectedCredentials.rootHashValue,
                    validatedChainIndexes,
                    iSecurityWarningsMode);
            }

            writeAuthenticationStorageData(storageSession, msUID, data, (oldMSUID != null && oldMSUID.equals(msUID)));
        }
        finally
        {
            // remove all the auth credentials with the selection
            if (data != null)
            {
                iSelectedAuthCredentials.put(msUID, data);
            }
        }
    }

    /**
     */
    public void authenticateJar(
        StorageSession storageSession,
        Uid uid,
        Uid oldUid,
        ProtectionDomain protectionDomain,
        String appJARPath)
    {
        if (protectionDomain == null
                || (!protectionDomain.equals(ProtectionDomain.getManufacturerDomain())
                    && !protectionDomain.equals(ProtectionDomain.getOperatorDomain())
                    && !protectionDomain.equals(ProtectionDomain.getIdentifiedThirdPartyDomain())
                    && !protectionDomain.equals(ProtectionDomain.getUnidentifiedThirdPartyDomain())))
        {
            Logger.logWarning("Unknown protection domain " + protectionDomain);
            throw new InstallerSecurityException(
                InstallerErrorMessage.INST_UNEXPECTED_ERR,
                null, /* no params for short msg */
                InstallerDetailedErrorMessage.INTERNAL_ERROR,
                new String[] {"Unknown protection domain " + protectionDomain},
                OtaStatusCode.INTERNAL_ERROR);
        }
        String jarHash = _computeHash(appJARPath);
        if (jarHash == null || jarHash.length() == 0)
        {
            // could not compute hash for the given application
            Logger.logWarning("Could not compute hash for " + appJARPath);
            throw new InstallerSecurityException(
                InstallerErrorMessage.INST_UNEXPECTED_ERR,
                null, /* no params for short msg */
                InstallerDetailedErrorMessage.INTERNAL_ERROR,
                new String[] {"Could not compute hash for " + appJARPath},
                OtaStatusCode.INTERNAL_ERROR);
        }
        AuthenticationStorageData data = new AuthenticationStorageData(
            protectionDomain.getName(),
            protectionDomain.getCategory(),
            jarHash,
            null /*rootHashValue*/,
            null /*validatedChainIndexes*/,
            null /* jarPath*/,
            iSecurityWarningsMode);
        verifyUpdate(
            new Credentials[] {new Credentials(
                                   data.getProtectionDomain(),
                                   data.getProtectionDomainCategory(),
                                   jarHash,
                                   null /* root hash */,
                                   -1 /* validated chain index*/,
                                   null /* signing cert */)
                              },
            oldUid);

        writeAuthenticationStorageData(storageSession, uid, data, (oldUid != null && oldUid.equals(uid)));
    }

    /**
     * Registers a listener for ocsp events corresponding to the
     * installation of a certain MIDlet suite
     *
     * @param aMsUid    the uid of the MIDlet suite on behalf
     *                  of which the listener is registered
     * @param aListener the ocsp events listener
     */
    public void registerOcspEventListener(Uid aMsUid,
                                          OcspEventListener aListener)
    {
        if (aMsUid != null && aListener != null)
        {
            Logger.log("OcspEventListener registered on behalf of the suite " + aMsUid.toString());
            iOcspEventListeners.put(aMsUid, aListener);
        }
    }

    /**
     * Unregisters the listener for ocsp events corresponding to the
     * installation of a certain MIDlet suite
     *
     * @param aMsUid    the uid of the MIDlet suite on behalf
     *                  of which the listener is unregistered
     */
    public void unregisterOcspEventListener(Uid aMsUid)
    {
        if (aMsUid != null)
        {
            Logger.log("OcspEventListener unregistered on behalf of the suite " + aMsUid.toString());
            iOcspEventListeners.remove(aMsUid);
        }
    }

    /**
     * Cancels any Ocsp checks (if any)
     *
     * @param msUid the UID if the MIDlet suite oh whose behalf the ocsp
     *              checks are canceled
     */
    public void cancelOcspCheck(Uid msUid)
    {
        OcspChecker ocspChecker = (OcspChecker)iOcspCheckers.get(msUid);
        if (ocspChecker != null)
        {
            ocspChecker.cancel();
        }
    }

    /**
     * Returns the signing info of certain application suite
     *
     * @param aAppSuiteName    the name of the application suite for which the
                               signing info is queried
     * @param aAppSuiteVersion the version of the application suite for which
     *                         the signing info is queried
     * @param aAppSuiteVendor  the vendor of the application suite for which
                               the signing info is queried
     * @return                 The signing info if the queried application
     *                         suite has been signed or NULL otherwise
     */
    public SigningInfo getSigningInfo(String aAppSuiteName,
                                      String aAppSuiteVersion,
                                      String aAppSuiteVendor)
    {
        SecurityStorage storage = new SecurityStorage();
        try
        {
            AuthenticationStorageData authData = storage.
                                                 readAuthenticationStorageData(
                                                     aAppSuiteName, aAppSuiteVersion, aAppSuiteVendor,
                                                     SecurityStorage.AUTHENTICATION_DOMAIN_NAME_QUERY
                                                     | SecurityStorage.AUTHENTICATION_DOMAIN_CATEGORY_QUERY
                                                     | SecurityStorage.AUTHENTICATION_ROOT_HASH_QUERY);
            if (authData != null)
            {
                Certificate signingCert = null;
                Certificate rootCert = null;
                AppAccessAuthorizationStorageData appAccesAuthData = storage
                        .readAppAccessAuthorizationStorageData(aAppSuiteName,
                                                               aAppSuiteVersion, aAppSuiteVendor,
                                                               SecurityStorage.APP_ACCESS_AUTH_SIGNERS_LIST_QUERY);
                if (appAccesAuthData != null && appAccesAuthData.getSignersList() != null
                        && appAccesAuthData.getSignersList().length > 0)
                {
                    signingCert = _parseCertificate(
                                      appAccesAuthData.getSignersList()[0]);
                    rootCert = _getRootCertificate(
                                   authData.getRootHashValue());
                }
                return new SigningInfo(signingCert, rootCert,
                                       new ProtectionDomain(authData.getProtectionDomain(),
                                                            authData.getProtectionDomainCategory()));
            }
            return null;
        }
        finally
        {
            storage.close();
        }
    }


    /**
     * Removes all the security data related to a certain MIDlet suite
     *
     * @param sessionID the JavaStorage session to be used when
     *                  removing the security data
     * @param msUID     the UID if the MIDlet suite whose security data is
     *                  being removed
     */
    public void removeSecurityData(StorageSession storageSession, Uid msUID)
    {
        Logger.log("Remove authentication data");
        SecurityStorage storage = new SecurityStorage(storageSession);
        storage.removeAuthenticationStorageData(msUID);
        // clean the caches as well
        iAuthCredentials.remove(msUID);
        iSelectedAuthCredentials.remove(msUID);
        iLegacySuiteFlags.remove(msUID);
        OcspChecker ocspChecker = (OcspChecker)iOcspCheckers.remove(msUID);
        if (ocspChecker != null)
        {
            ocspChecker.destroy();
        }
        iOcspEventListeners.remove(msUID);
    }

    /**
     * Returns the details of the certificates used for authenticating a
     * MIDlet suite. This method is used at installation time.
     *
     * @param sessionID the JavaStorage session to be used when
     *                  retrieving the certificates details
     * @param msUID     the UID if the MIDlet suite whose certificate details
     *                  are queried
     * @return          the details of the certificate used for authenticating
     *                  the MIDlet suite or null if the details are not
     *                  available
     */
    public SigningCertificate[] getCertificatesDetails(StorageSession storageSession, Uid msUID)
    {
        Vector allAuthCredentials = (Vector)iAuthCredentials.get(msUID);
        SigningCertificate[] certDetails = null;
        if (allAuthCredentials != null && allAuthCredentials.size() > 0)
        {
            Vector vCertDetails = new Vector();
            for (int i=0; i<allAuthCredentials.size(); i++)
            {
                Credentials credentials = ((Credentials)allAuthCredentials
                                           .elementAt(i));
                Certificate cert = credentials.signingCert;
                if (cert != null)
                {
                    vCertDetails.addElement(new SigningCertificate(
                                                cert, credentials.rootHashValue,
                                                credentials.getProtectionDomainName(),
                                                credentials.getProtectionDomainCategory()));
                }
            }
            if (vCertDetails.size() > 0)
            {
                certDetails = new SigningCertificate[vCertDetails.size()];
                vCertDetails.copyInto(certDetails);
            }
        }
        else
        {
            // if cert details are not found in cache, retrieve the signing
            // certificate from storage and extract the details from it
            SecurityStorage storage = new SecurityStorage(storageSession);
            SigningCertificate signingCertificate = retrieveSigningCertificate(
                                                        storage, msUID);
            if (signingCertificate != null)
            {
                certDetails = new SigningCertificate[1];
                certDetails[0] = signingCertificate;
            }
        }
        return certDetails;
    }

    /**
     * Returns the protection domain info of a certain MIDlet suite. This
     * method is used at uninstallation time.
     *
     * @param sessionID the JavaStorage session to be used when
     *                  retrieving the domain category
     * @param msUID     the UID if the MIDlet suite whose protection
     *                  domain info is queried
     * @param           one of the constants defined in ApplicationInfo
     */
    public String getProtectionDomainCategory(StorageSession storageSession, Uid msUID)
    {
        SecurityStorage storage = new SecurityStorage(storageSession);
        return storage.readProtectionDomainCategory(msUID);
    }

    /**
     * Notification about the media where a certain MIDlet suite is installed.
     *
     * @param aStorageSession the JavaStorage session to be used when/if
     *                        making storage operations related to this
     *                        notification
     * @param aMsUid          the UID of the MIDlet suite whose media info is
     *                        notified
     * @param aMediaId        the identifier of the media where the MIDlet
     *                        suite is installed
     */
    public void setMediaId(StorageSession aStorageSession, Uid aMsUid, int aMediaId)
    {
        // store the jar hash only if the suite was installed on a non-protected media
        if (isDriveProtected(aMediaId))
        {
            SecurityStorage storage = new SecurityStorage(aStorageSession);
            try
            {
                AuthenticationStorageData authStorageData =
                    (AuthenticationStorageData)iSelectedAuthCredentials.get(
                        aMsUid);
                if (authStorageData != null)
                {
                    Logger.log("Suite installed on protected media -> the runtime tamper detection is disabled");
                    authStorageData.setJarHashValue(null);
                    storage.writeAuthenticationStorageData(aMsUid,
                                                           authStorageData,
                                                           true /* this is an update */);
                }
            }
            finally
            {
                iSelectedAuthCredentials.remove(aMsUid);
            }

        }
    }

    /**
     * Setter for the OCSP settings
     */
    public void setOCSPFlags(OcspSettings ocspSettings)
    {
        Logger.log("Ocsp settings = " + ocspSettings.toString());
        iOcspSettings = ocspSettings;
    }

    /**
     * Performs a cleanup (e.g. on cached data)
     *
     */
    public void cleanup()
    {
        Logger.log("Cleanup authentication module cache");
        iAuthCredentials.clear();
        iSelectedAuthCredentials.clear();
        iLegacySuiteFlags.clear();
        iOcspCheckers.clear();
        iOcspEventListeners.clear();
    }

    /**
     * Verifies the authenticity of MIDlet suites
     *
     * @param msUid           The Uid of the MIDlet suite whose authenticity
     *                        is verified
     * @param authStorageData The stored authentication data assigned to the
     *                        MIDlet suite whose authenticity is verified
     *
     */
    public void verifyMIDletSuiteAuthenticity(Uid msUid, AuthenticationStorageData authStorageData)
    {
        Logger.log("Verifying the authenticity of the suite " + msUid.toString());
        // for the operator MIDlets, check if there are any network restrictions
        if (ApplicationInfo.OPERATOR_DOMAIN.equals(
                    authStorageData.getProtectionDomainCategory()))
        {
            Logger.log("  Checking network restrictions for operator signed suites");
            // get the restrictions from storage
            SecurityStorage storage = new SecurityStorage();
            String networkRestrictions = storage.readNetworkRestrictions(msUid);
            storage.close();
            if (networkRestrictions != null
                    && networkRestrictions.length() > 0)
            {
                // get the real network codes
                TelUtils.NetworkCodes networkCodes = TelUtils.getNetworkCodes();
                boolean found = false;
                if (networkCodes != null)
                {
                    Logger.log("    Network restrictions: " + networkRestrictions);
                    Logger.log("    Network codes: mcc(" + networkCodes.mcc + ") mnc(" + networkCodes.mnc + ")");
                    // go through the list of restrictions and try to find a match
                    // the list of restrictions is a space-separated list of MCC-MNC*/
                    String[] tuples = Tokenizer.split(networkRestrictions, " ");
                    if (tuples != null)
                    {
                        for (int i=0; i<tuples.length; i++)
                        {
                            int mccEndPos = tuples[i].indexOf('-');
                            String mcc = tuples[i].substring(0, mccEndPos);
                            String mnc = tuples[i].substring(mccEndPos + 1);
                            if (mcc.equals(networkCodes.mcc)
                                    && mnc.equals(networkCodes.mnc))
                            {
                                found = true;
                                break;
                            }
                        }
                    }
                }
                if (!found)
                {
                    Logger.logWarning("  -> the network restrictions are violated");
                    throw new RuntimeSecurityException(
                        SecurityErrorMessage.NETWORK_RESTRICTION_VIOLATION,
                        null, /* no params for short msg */
                        SecurityDetailedErrorMessage.NETWORK_RESTRICTION_VIOLATION,
                        null /* no params for detailed msg */);
                }
                Logger.log("  -> the network restrictions are obeyed");
            }
        }

        // check the root validity (if applicable)
        if (authStorageData.getRootHashValue() != null
                && authStorageData.getRootHashValue().length() > 0)
        {
            Logger.log("  Checking validity of the root certificate used in authentication");
            switch (retrieveRootState(authStorageData.getRootHashValue()))
            {
            case SecurityCommsMessages.JAVA_CERT_STORE_STATE_ENABLED:
                // ok -> just go on
                Logger.log("    Root ok");
                break;
            case SecurityCommsMessages.JAVA_CERT_STORE_STATE_DISABLED:
                Logger.logWarning("    Root disabled");
                throw new RuntimeSecurityException(
                    SecurityErrorMessage.CERT_NOT_AVAILABLE,
                    null, /* no params for short msg */
                    SecurityDetailedErrorMessage.CERT_DISABLED,
                    null /* no params for detailed msg */);
            case SecurityCommsMessages.JAVA_CERT_STORE_STATE_DELETED:
                Logger.logWarning("    Root deleted");
                throw new RuntimeSecurityException(
                    SecurityErrorMessage.CERT_NOT_AVAILABLE,
                    null, /* no params for short msg */
                    SecurityDetailedErrorMessage.CERT_DELETED,
                    null /* no params for detailed msg */);
            case SecurityCommsMessages.JAVA_CERT_STORE_STATE_NOT_PRESENT:
                Logger.logWarning("    Root not available");
                throw new RuntimeSecurityException(
                    SecurityErrorMessage.CERT_NOT_AVAILABLE,
                    null, /* no params for short msg */
                    SecurityDetailedErrorMessage.SIM_CHANGED,
                    null /* no params for detailed msg */);
            case SecurityCommsMessages.JAVA_CERT_STORE_STATE_UNKNOWN:
                Logger.logWarning("    Root unknown");
                throw new RuntimeSecurityException(
                    SecurityErrorMessage.CERT_NOT_AVAILABLE,
                    null, /* no params for short msg */
                    SecurityDetailedErrorMessage.UNIDENTIFIED_APPLICATION,
                    null /* no params for detailed msg */);
            }
        }

        if (authStorageData.getJarPath() == null
                || authStorageData.getJarPath().length() == 0)
        {
            Logger.logWarning("  JarPath not available");
            throw new RuntimeSecurityException(
                SecurityErrorMessage.UNEXPECTED_ERR,
                null, /* no params for short msg */
                SecurityDetailedErrorMessage.UNIDENTIFIED_APPLICATION,
                null /* no params for detailed msg */);
        }

        // do the tamper detection
        if (authStorageData.getJarHashValue() != null
                && authStorageData.getJarHashValue().length() > 0)
        {
            Logger.log("  Doing tamper detection");
            String computedJarHash = _computeHash(authStorageData.getJarPath());
            // do the tampering check: compute the hash and compare it with the stored hash
            if (computedJarHash == null || !computedJarHash.equals(
                        authStorageData.getJarHashValue()))
            {
                Logger.logWarning("    Application has been tampered");
                throw new RuntimeSecurityException(
                    SecurityErrorMessage.JAR_TAMPERED,
                    null, /* no params for short msg */
                    SecurityDetailedErrorMessage.JAR_TAMPERED,
                    null /* no params for detailed msg */);
            }
        }
    }

    private AuthenticationModule()
    {
        iAuthCredentials = new Hashtable();
        iSelectedAuthCredentials = new Hashtable();
        iOcspCheckers = new Hashtable();
        iOcspEventListeners = new Hashtable();
        // default ocsp settings
        iOcspSettings = new OcspSettings(
            OcspSettings.OCSP_MODE_UNDEFINED,
            OcspSettings.OCSP_WARNING_UNDEFINED,
            false /* silent*/,
            "0" /* iap */,
            "0" /* snap */);
        iOcspUserPreferences = new OcspUserPreferences();
        iSecurityWarningsMode = GeneralSecuritySettings
                                .getDefaultSecurityWarningsMode();
        Logger.log("Ocsp user preferences = " + iOcspUserPreferences.toString());
    }

    private SigningCertificate retrieveSigningCertificate(SecurityStorage storage, Uid msUID)
    {
        SigningCertificate signingCertificate = null;
        AppAccessAuthorizationStorageData authData = storage
                .readAppAccessAuthorizationStorageData(msUID,
                                                       SecurityStorage.APP_ACCESS_AUTH_SIGNERS_LIST_QUERY);
        if (authData != null && authData.getSignersList() != null
                && authData.getSignersList().length > 0)
        {
            Certificate cert = _parseCertificate(authData
                                                 .getSignersList()[0]);
            if (cert != null)
            {
                AuthenticationStorageData signingData = storage.
                                                        readAuthenticationStorageData(msUID);
                signingCertificate = new SigningCertificate(cert,
                        signingData.getRootHashValue(),
                        signingData.getProtectionDomain(),
                        signingData.getProtectionDomainCategory());
            }
        }
        return signingCertificate;
    }

    private Vector verifyUpdate(Credentials[] credentials, Uid oldMSUID)
    {
        return verifyUpdate(isLegacySuite(oldMSUID), credentials, oldMSUID, false /* don't order the returned credentials */);
    }

    // Security rules for update:
    // 1. the old and the new MIDlet suites must be bound to the same
    // protection domain
    // 2. Only for MIDP3 MIDlets, the the old and the new MIDlet
    // suites MUST share at least one common signer (for legacy MIDlets
    // it is ok not to share a common signer, case in which the update
    // is decided by the user)
    //
    // Common signer is defined as matching the Organization field
    // within the Subject field of the signing certificate of MIDlet
    // Suite update and the signing certificate of the original MIDlet
    // Suite, where the signing certificates are validated against
    // the same Protection Domain Root Certificate)
    //
    // This method discards the credentials which do not obey the rules
    // mentioned above and returns either all credentials which have same
    // signer OR all credentials which do NOT have same signer,
    // but not any combination
    private Vector verifyUpdate(boolean legacyMIDletSuite, Credentials[] credentials, Uid oldMSUID, boolean orderResult)
    {
        Vector allCredentials = new Vector(credentials.length);
        for (int i=0; i<credentials.length; i++)
        {
            allCredentials.addElement(credentials[i]);
        }
        if (oldMSUID != null)
        {
            SecurityStorage storage = new SecurityStorage();
            Vector sameSignerCredentials = new Vector();
            Vector differentSignerCredentials = new Vector();
            try
            {
                ProtectionDomain oldProtectionDomain = new ProtectionDomain(
                    storage.readProtectionDomain(oldMSUID),
                    storage.readProtectionDomainCategory(oldMSUID));
                for (int i=0; i<credentials.length; i++)
                {
                    if (oldProtectionDomain.equals(
                                credentials[i].getProtectionDomain()))
                    {
                        SigningCertificate oldSigningCertificate =
                            retrieveSigningCertificate(storage, oldMSUID);
                        SigningCertificate newSigningCertificate =
                            new SigningCertificate(credentials[i].signingCert,
                                                   credentials[i].rootHashValue,
                                                   credentials[i].getProtectionDomainName(),
                                                   credentials[i].getProtectionDomainCategory());
                        if (newSigningCertificate.isSameSigner(
                                    oldSigningCertificate))
                        {
                            sameSignerCredentials.addElement(credentials[i]);
                        }
                        else if (legacyMIDletSuite)
                        {
                            differentSignerCredentials.addElement(credentials[i]);
                        }
                    }
                    else if (legacyMIDletSuite
                             && !ApplicationInfo.UNIDENTIFIED_THIRD_PARTY_DOMAIN.equals(
                                 credentials[i].getProtectionDomain().getCategory()))
                    {
                        differentSignerCredentials.addElement(credentials[i]);
                    }
                }
                if (sameSignerCredentials.size() != 0)
                {
                    allCredentials = sameSignerCredentials;
                }
                else if (differentSignerCredentials.size() != 0)
                {
                    allCredentials = differentSignerCredentials;
                }
                else
                {
                    throw new InstallerSecurityException(
                        InstallerErrorMessage.INST_AUTHORIZATION_ERR,
                        null, /* no params for short msg */
                        InstallerDetailedErrorMessage.DIFFERENT_SIGNERS,
                        null, /* no params for the detailed msg*/
                        OtaStatusCode.APPLICATION_AUTHORIZATION_FAILURE);
                }
            }
            finally
            {
                storage.close();
            }
        }
        if (orderResult)
        {
            // put the credentials with domain info first
            int i=0;
            int size = allCredentials.size();
            while (i<size)
            {
                Credentials current = (Credentials)allCredentials.elementAt(i);
                if (current.getProtectionDomainName() == null
                        || current.getProtectionDomainCategory() == null)
                {
                    // swap the current element with the last one
                    Object last = allCredentials.lastElement();
                    allCredentials.setElementAt(last,i);
                    allCredentials.setElementAt(current,(size-1));
                    size--;
                }
                else
                {
                    i++;
                }
            }
        }
        return allCredentials;
    }

    private boolean isLegacySuite(Uid msUID)
    {
        if (msUID == null)
        {
            return true;
        }
        Boolean tmp = (Boolean)iLegacySuiteFlags.remove(msUID);
        if (tmp != null)
        {
            return tmp.booleanValue();
        }
        else
        {
            SecurityStorage storage = new SecurityStorage();
            String suiteVersion = storage.readSuiteVersion(msUID);
            storage.close();
            if (suiteVersion != null)
            {
                boolean legacySuite = !suiteVersion.equalsIgnoreCase(
                                          SecurityAttributes.MIDP3_VERSION_ATTRIBUTE_VALUE);
                iLegacySuiteFlags.put(msUID, new Boolean(legacySuite));
                return legacySuite;
            }
            return true;
        }
    }

    private String getRoot(String rootHash)
    {
        CommsEndpoint comms = null;
        try
        {
            comms = new CommsEndpoint();
            comms.connect(CommsEndpoint.JAVA_CAPTAIN);
            CommsMessage sMessage = new CommsMessage();
            sMessage.setMessageId(SecurityCommsMessages.JAVA_CERT_STORE_MSG_ID_REQUEST);
            sMessage.setModuleId(SecurityCommsMessages.PLUGIN_ID_JAVA_CERT_STORE_EXTENSION);
            sMessage.write(SecurityCommsMessages.JAVA_CERT_STORE_OPERATION_QUERY_CERTS);
            // add filter
            sMessage.write(SecurityCommsMessages.JAVA_CERT_STORE_FILTER_ID_STATE);
            sMessage.write(SecurityCommsMessages.JAVA_CERT_STORE_STATE_ENABLED);
            sMessage.write(SecurityCommsMessages.JAVA_CERT_STORE_FILTER_ID_HASH);
            sMessage.write(rootHash);
            // add the query ID
            sMessage.write(SecurityCommsMessages.JAVA_CERT_STORE_QUERY_ID_CERT_CONTENT_PEM);
            CommsMessage rMessage = comms.sendReceive(sMessage, 10);
            // read the reply
            String tmp = rMessage.readString();
            if (tmp != null && tmp.length() > 0)
            {
                return rMessage.readString();
            }
        }
        catch (CommsException e)
        {
            // fall through
        }
        finally
        {
            if (comms != null)
            {
                comms.destroy();
            }
        }
        return null;
    }

    private int retrieveRootState(String rootHash)
    {

        CommsEndpoint comms = null;
        try
        {
            comms = new CommsEndpoint();
            comms.connect(CommsEndpoint.JAVA_CAPTAIN);
            CommsMessage sMessage = new CommsMessage();
            sMessage.setMessageId(SecurityCommsMessages.JAVA_CERT_STORE_MSG_ID_REQUEST);
            sMessage.setModuleId(SecurityCommsMessages.PLUGIN_ID_JAVA_CERT_STORE_EXTENSION);
            sMessage.write(SecurityCommsMessages.JAVA_CERT_STORE_OPERATION_QUERY_CERTS);
            // add filter
            sMessage.write(SecurityCommsMessages.JAVA_CERT_STORE_FILTER_ID_HASH);
            sMessage.write(rootHash);
            // add the query ID
            sMessage.write(SecurityCommsMessages.JAVA_CERT_STORE_QUERY_ID_STATE);
            CommsMessage rMessage = comms.sendReceive(sMessage, 10);
            // read the reply
            return rMessage.readInt();
        }
        catch (CommsException e)
        {
            // fall through
        }
        finally
        {
            if (comms != null)
            {
                comms.destroy();
            }
        }
        return SecurityCommsMessages.JAVA_CERT_STORE_STATE_UNKNOWN;
    }

    private boolean performOcsp()
    {
        if (iOcspSettings.ocspMode
                != OcspSettings.OCSP_MODE_UNDEFINED)
        {
            return (iOcspSettings.ocspMode == OcspSettings.OCSP_MODE_ENABLED);
        }
        // check the user preferences
        return (iOcspUserPreferences.getOcspMode()
                == OcspUserPreferences.OCSP_MODE_ON
                || iOcspUserPreferences.getOcspMode()
                == OcspUserPreferences.OCSP_MODE_MUST);
    }

    private boolean isDriveProtected(int aMediaId)
    {
        DriveInfo[] allDrives = DriveUtilities.getAllDrives();
        if (allDrives != null)
        {
            for (int i=0; i<allDrives.length; i++)
            {
                if (aMediaId == allDrives[i].iId)
                {
                    if (allDrives[i].iIsRemovable)
                    {
                        return false;
                    }
                    if (allDrives[i].iIsExternallyMountable)
                    {
                        return false;
                    }
                    return true;
                }
            }
        }
        return false;
    }

    private Credentials selectCredentials(String selectedJarHash, Vector allAuthCredentials, Vector validatedChainIndexes)
    {
        Credentials selectedCredentials = null;
        if (selectedJarHash != null)
        {
            for (int i=0; i<allAuthCredentials.size(); i++)
            {
                Credentials authCredentials =
                    (Credentials)allAuthCredentials.elementAt(i);
                if (selectedJarHash.equalsIgnoreCase(authCredentials.jarHashValue))
                {
                    // the first one wins
                    if (selectedCredentials == null)
                    {
                        selectedCredentials = new Credentials(
                            authCredentials.getProtectionDomainName(),
                            authCredentials.getProtectionDomainCategory(),
                            authCredentials.jarHashValue,
                            authCredentials.rootHashValue,
                            -1,
                            null);
                    }
                    // collect the validated chain indexes
                    validatedChainIndexes.addElement(
                        new Integer(authCredentials.validatedChainIndex));
                }
            }
        }
        return selectedCredentials;
    }

    private void writeAuthenticationStorageData(StorageSession storageSession, Uid uid, AuthenticationStorageData data, boolean isUpdate)
    {
        if (storageSession == null)
        {
            return;
        }
        SecurityStorage storage = new SecurityStorage(storageSession);
        storage.writeAuthenticationStorageData(uid, data, isUpdate);
        Logger.logAuthenticationData(data);
    }

    private native Credentials[] _validateChainsAndSignatures(AuthenticationInfo[] authInfo);
    private native String _computeHash(String appJARPath);
    private native String _drmDecryptAndComputeHash(String appJARPath);
    private native Certificate _parseCertificate(String rawCert);
    private native Certificate _getRootCertificate(String certHash);
}
