/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2006 Sun Microsystems Inc. All Rights Reserved
 *
 * The contents of this file are subject to the terms
 * of the Common Development and Distribution License
 * (the License). You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the License at
 * https://opensso.dev.java.net/public/CDDLv1.0.html or
 * opensso/legal/CDDLv1.0.txt
 * See the License for the specific language governing
 * permission and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL
 * Header Notice in each file and include the License file
 * at opensso/legal/CDDLv1.0.txt.
 * If applicable, add the following below the CDDL Header,
 * with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * $Id: LogVerifier.java,v 1.7 2008/06/25 05:43:38 qcheng Exp $
 *
 * Portions Copyrighted 2011-2016 ForgeRock AS.
 */
package com.sun.identity.log.secure;

import static org.forgerock.openam.utils.Time.*;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Vector;
import java.util.logging.LogManager;

import com.sun.identity.common.GeneralTaskRunnable;
import com.sun.identity.common.SystemTimer;
import com.sun.identity.log.LogConstants;
import com.sun.identity.log.LogManagerUtil;
import com.sun.identity.log.LogReader;
import com.sun.identity.log.Logger;
import com.sun.identity.log.handlers.SecureFileHandler;
import com.sun.identity.log.spi.Debug;
import com.sun.identity.log.spi.Token;
import com.sun.identity.log.spi.VerifierAction;
import com.sun.identity.security.keystore.AMPassword;

/**
 * This class is verifying signature that is generated with the MAC value 
 * for each log entry.
 */
public class LogVerifier{
    
    private static String PREFIX = "_secure.";
    private String curMAC = null;
    private VerifyTask verifier;
    private String prevSignature = null;
    private boolean verified = true;
    private SecureLogHelper helper;
    private AMPassword verPassword;
    private String name;
    private LogManager manager = LogManagerUtil.getLogManager();
    private boolean verificationOn = false;
    
    /**
     * Constructs <code>LogVerifier</code> object
     * @param log logger name associated with this verifier
     * @param verPass the password for verifier secure store
     */
    public LogVerifier(String log, AMPassword verPass) {
        name = log;
        verPassword = verPass;
    }
    
    /**
     * Return verification flag
     * @return verification flag
     */
    public boolean getVerificationFlag() {
        return verificationOn;
    }
    
    /**
     *  Inner class which extends the abstract GeneralTaskRunnable class and
     *  impelements the run method which is run periodically which does the
     *  actual verification.
     */
    class VerifyTask extends GeneralTaskRunnable {
        
        private long runPeriod;
        
        public VerifyTask(long runPeriod) {
            this.runPeriod = runPeriod;
        }
        
        /**
         *  Method that runs at an interval as specified in the timer object.
         */
        public void run(){
            try{
                verify();
            } catch(Exception e) {
                Debug.error(name+":Error running verifier thread", e);
            }
            verificationOn = false;
        }
        
        /**
         *  Methods that need to be implemented from GeneralTaskRunnable.
         */
        
        public boolean isEmpty() {
            return true;
        }
        
        public boolean addElement(Object obj) {
            return false;
        }
        
        public boolean removeElement(Object obj) {
            return false;
        }
        
        public long getRunPeriod() {
            return runPeriod;
        }
        
    }
    
    /**
     *  Method that starts the log Verifier thread
     */
    public void startLogVerifier(){
        String period = 
            manager.getProperty(LogConstants.LOGVERIFY_PERIODINSECONDS);
        long interval;
        if((period != null) || (period.length() != 0)) {
            interval = Long.parseLong(period);
        } else {
            interval = LogConstants.LOGVERIFY_PERIODINSECONDS_DEFAULT;
        }
        interval *= 1000;
        if(verifier == null){
            verifier = new VerifyTask(interval);
            SystemTimer.getTimer().schedule(verifier, new Date(((
                    currentTimeMillis() + interval) / 1000) * 1000));
            if (Debug.messageEnabled()) {
                Debug.message(name+":Verifier Thread Started");
            }
        }
    }
    
    /**
     *  Method to stop the log verifier thread if it is running
     */
    public void stopLogVerifier() {
        if (verifier != null) {
            verifier.cancel();
            verifier = null;
        }
    }
    
    /**
     *  Verifies the passed LogRecord to check for tampering.
     *
     *  @param  record String array of the elements of the record.
     *  @param  macPos position of the mac header in the array.
     *  @return a boolean value of the result of the verification
     */
    private boolean verifyLogRecord(String[] record, int macPos)
    throws Exception {
        // Creating the data part for verification
        StringBuilder data = new StringBuilder();
        for(int m = 0; m < record.length-2; m++) {
            data.append(record[m]);
        }
        curMAC = record[macPos];
        verified = 
            helper.verifyMAC(data.toString(), helper.toByteArray(curMAC));
        return verified;
    }
    
    /**
     * Verifies the signature entry in the log file for tampering.
     *
     * @param  String array of the elements of the record.
     * @param  position of the signature field value in the array
     * @return a boolean value of the result of the verification
     */
    private boolean verifySignature(String[] record, int signPos, int recPos)
    throws Exception {
        String curSign = record[signPos];
        
        // Regenerate the MAC that was signed.
        byte[] prevMAC = helper.toByteArray(curMAC);
        byte[] newMAC ;
        if((prevSignature == null) || prevSignature.equals("")) {
            newMAC = new byte[prevMAC.length];
            System.arraycopy(prevMAC, 0, newMAC, 0, prevMAC.length);
        }else{
            newMAC = new byte[prevMAC.length + 
                              helper.toByteArray(prevSignature).length];
            System.arraycopy(prevMAC, 0, newMAC, 0, prevMAC.length);
            System.arraycopy(helper.toByteArray(prevSignature), 0, newMAC,
            prevMAC.length, helper.toByteArray(prevSignature).length);
        }
        // If this is the last record in the file then dont update the
        // prevSignature as the first record in the next file is also 
        // the same signature.
        if(recPos != 0) {
            prevSignature = curSign;
        }
        verified = helper.verifySignature(helper.toByteArray(curSign), newMAC);
        return verified;
    }
    
    /**
     * Checks each record in the list of log files for tampering.
     * @return a boolean value as a result of the verification
     * @throws Exception if it fails to verify any mac value in the log entry.
     */
    public boolean verify()
    throws Exception{
        Logger logger = (com.sun.identity.log.Logger)Logger.getLogger(name);
        ArrayList fileList = new ArrayList();
        String[][] tmpResult = new String[1][1];
        Object token = new Object();
        synchronized(logger) {
            verificationOn = true;
            long start = currentTimeMillis();
            helper = SecureFileHandler.getSecureLogHelper(name);
            fileList = SecureFileHandler.getCurrentFileList(name);
            if (fileList == null) {
                Debug.error("No fileList found in handler.");
                return VerifierAction.doVerifierAction(name, verified);
            }
            token = 
               Token.createToken("AUDITOR", new String(verPassword.getChars()));
            tmpResult = 
               LogReader.read((String)fileList.get(fileList.size() - 1), token);
        }
        
        for(int i = 0; i < fileList.size() - 1; i++) {
            String[][] result = new String[1][1];
            try{
                result = LogReader.read((String)fileList.get(i),token);
            }catch(Exception e){
                Debug.error("Error in reading File : "+fileList.get(i));
            }
            // Check if the result of read is null or empty string array.
            if(result != null && result.length != 0){
                Vector header = new Vector(result[0].length);
                // Extracting the field names as header from the first line 
                // of the returned string array.

                header.addAll(Arrays.asList(result[0]));
                
                int signPos = -1, macPos = -1;
                String signFldName, macFldName;
                signFldName = LogConstants.SIGNATURE_FIELDNAME;
                macFldName = LogConstants.MAC_FIELDNAME;
                
                for(int l = 0; l < header.size(); l++){
                    if((((String)header.get(l))).equalsIgnoreCase(signFldName)) 
                    {
                        signPos = l;
                        break;
                    }
                }
                for(int l = 0; l < header.size(); l++){
                    if((((String)header.get(l))).equalsIgnoreCase(macFldName)) {
                        macPos = l;
                        break;
                    }
                }
                if ((signPos == -1) || (macPos == -1)) {
                    Debug.error("Could not locate mac and sign header");
                    return VerifierAction.doVerifierAction(name, verified);
                }
                
                // Now check each record to see if it is a signature record or 
                // a log record.
                for(int k = 1; k < result.length; k++) {
                    if (Debug.messageEnabled()) {
                        Debug.message(name + ":Start checking records " + 
                            result.length + ":" + fileList.get(i));
                    }
                    if(result[k][signPos].equals("-")) {
                        verified = verifyLogRecord(result[k], macPos);
                        if(!verified) {
                            Debug.error("Log Record Verification " +
                                "Failed in file:" +
                                (String)fileList.get(i) + " at record no. "+ k);
                            break;
                        }
                        if (Debug.messageEnabled()) {
                            Debug.message(name+
                                ":Log Record Verification Succeeded in file:"+
                                (String)fileList.get(i) + "at record no."+ k);
                        }
                    } else {
                        /*
                         * To check if this is the last signature in the file 
                         * an additional parameter has to be passed to the 
                         * verifySignature since the signature is the same
                         * as the first signature in the next file. This is 
                         * to ensure that prevSignature is not updated with 
                         * the last signature in the file.
                         * Bcos the checking of the last signature in the file 
                         * will be the same for the first signature for the 
                         * next file.
                         */
                        int lastRecInFile = 0;
                        lastRecInFile = (result.length - 1)  - k;
                        verified = 
                            verifySignature(result[k], signPos, lastRecInFile);
                        if(!verified) {
                            Debug.error("Log Signature Verification " +
                                "Failed in file:" +
                                (String)fileList.get(i) + " at record no. "+ k);
                            break;
                        }
                            
                        if (Debug.messageEnabled()) {
                            Debug.message("Log Signature Verification " +
                                "Succeeded in file:" +
                                (String)fileList.get(i) + "at record no."+ k);
                        }
                    }
                } // end of loop k . i.e. verification check for current file 
                  // is over
            } else {
                if (Debug.messageEnabled()) {
                    Debug.message("LogVerifier::verify::Empty return " +
                        "from read of " + (String)fileList.get(i) +
                        ":" + fileList.get(i));
                }
                verified = false;
                break;
            }
            if (!verified) {
                break;
            }
        } // end of loop i i.e. current filelist verification is over.
        
        // This is for the current file that was read at the start.
        // This is done bcos in the time that the verifier reaches a point 
        // where  it starts verifying this file it might have already been 
        // timestamped, bcos of the logging that is going on in parallel.
        
        if(tmpResult != null && tmpResult.length != 0){
            Vector header = new Vector(tmpResult[0].length);
            // Extracting the field names as header from the first line of the
            // returned string array.
            header.addAll(Arrays.asList(tmpResult[0]));
            
            int signPos = -1, macPos = -1;
            String signFldName, macFldName;
            signFldName = LogConstants.SIGNATURE_FIELDNAME;
            macFldName = LogConstants.MAC_FIELDNAME;
            
            for(int l = 0; l < header.size(); l++){
                if((((String)header.get(l))).equalsIgnoreCase(signFldName)) {
                    signPos = l;
                    break;
                }
            }
            for(int l = 0; l < header.size(); l++){
                if((((String)header.get(l))).equalsIgnoreCase(macFldName)) {
                    macPos = l;
                    break;
                }
            }
            if ((signPos == -1) || (macPos == -1)) {
                Debug.error("Could not locate mac and sign header");
                return VerifierAction.doVerifierAction(name, verified);
            }
            
            // Now check each record to see if it is a signature record 
            // or a log record.
            for(int k = 1; k < tmpResult.length; k++) {
                if (Debug.messageEnabled()) {
                    Debug.message(name+":Start checking records " + 
                        tmpResult.length+":"+fileList.get(fileList.size() - 1));
                }
                if(tmpResult[k][signPos].equals("-")) {
                    verified = verifyLogRecord(tmpResult[k], macPos);
                    if(!verified) {
                        Debug.error("Log Record Verification Failed in file:"+
                        (String)fileList.get(fileList.size() - 1) + 
                        " at record no. "+ k);
                        break;
                    }
                    
                    if (Debug.messageEnabled()) {
                        Debug.message(name+":Log Record Verification " +
                            "Succeeded in file:" +
                            (String)fileList.get(fileList.size() - 1) + 
                            "at record no."+ k);
                    }
                } else {
                    // To check if this is the last signature in the file an 
                    // additional parameter has to be passed to the 
                    // verifySignature since the signature is the same
                    // as the first signature in the next file. 
                    // This is to ensure that prevSignature is not updated 
                    // with the last signature in the file.
                    // Bcos the checking of the last signature in the file 
                    // will be the same for the first signature for the 
                    // next file.
                    int lastRecInFile = 0;
                    lastRecInFile = (tmpResult.length - 1)  - k;
                    verified = verifySignature(tmpResult[k], 
                        signPos, lastRecInFile);
                    if(!verified) {
                        Debug.error("Log Signature Verification Failed " +
                            "in file:" +
                            (String)fileList.get(fileList.size() - 1) + 
                            " at record no. " + k);
                        break;
                    }
                    if (Debug.messageEnabled()) {
                        Debug.message("Log Signature Verification Succeeded" +
                            " in file:"+
                            (String)fileList.get(fileList.size() - 1) +
                            "at record no."+ k);
                    }
                }
            } // end of loop k. i.e. verification check for current file is over
        } else {
            if (Debug.messageEnabled()) {
                Debug.message("LogVerifier::verify::Empty return from read of "
                            + (String)fileList.get(fileList.size() - 1) + ":" + 
                            fileList.get(fileList.size() - 1) );
            }
            verified = false;
        }
        
        prevSignature = null;
        curMAC = null;
        String path = manager.getProperty(LogConstants.LOG_LOCATION);
        if(!path.endsWith("/"))
            path += "/";
        String verKeyStoreName = path + PREFIX + "ver." + name;
        helper.setLastLineforVerifier(true);
        boolean intrusion = helper.isIntrusionTrue();
        if(intrusion) {
            Debug.error(name+" Last Line check in Verifier failed." +
            " Possible intrusion detected");
            verified = false;
        }
        helper.setLastLineforVerifier(false);
        helper.reinitializeVerifier(verKeyStoreName , verPassword);
        if (Debug.messageEnabled()) {
            Debug.message(name + ":Done Verifying");
        }
        return VerifierAction.doVerifierAction(name, verified);
    }
}
