/*
* Copyright 2010 Srikanth Reddy Lingala  
* 
* Licensed under the Apache License, Version 2.0 (the "License"); 
* you may not use this file except in compliance with the License. 
* You may obtain a copy of the License at 
* 
* http://www.apache.org/licenses/LICENSE-2.0 
* 
* Unless required by applicable law or agreed to in writing, 
* software distributed under the License is distributed on an "AS IS" BASIS, 
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
* See the License for the specific language governing permissions and 
* limitations under the License. 
*/

package com.zip4j.crypto;

import com.zip4j.crypto.PBKDF2.MacBasedPRF;
import com.zip4j.crypto.PBKDF2.PBKDF2Engine;
import com.zip4j.crypto.PBKDF2.PBKDF2Parameters;
import com.zip4j.crypto.engine.AESEngine;
import com.zip4j.model.AESExtraDataRecord;
import com.zip4j.model.LocalFileHeader;

import java.util.Arrays;

import com.zip4j.exception.ZipException;
import com.zip4j.exception.ZipExceptionConstants;
import com.zip4j.util.InternalZipConstants;
import com.zip4j.util.Raw;
import com.zip4j.util.Zip4jConstants;

public class AESDecrypter implements IDecrypter {
	
	private LocalFileHeader localFileHeader;
	private AESEngine aesEngine;
	private MacBasedPRF mac;
	
	private final int PASSWORD_VERIFIER_LENGTH = 2;
	private int KEY_LENGTH;
	private int MAC_LENGTH;
	private int SALT_LENGTH;
	
	private byte[] aesKey;
	private byte[] macKey;
	private byte[] derivedPasswordVerifier;
	private byte[] storedMac;
	
	private int nonce = 1;
	private byte[] iv;
	private byte[] counterBlock;
	private int loopCount = 0;
	
	public AESDecrypter(LocalFileHeader localFileHeader, 
			byte[] salt, byte[] passwordVerifier) throws ZipException {
		
		if (localFileHeader == null) {
			throw new ZipException("one of the input parameters is null in AESDecryptor Constructor");
		}
		
		this.localFileHeader = localFileHeader;
		this.storedMac = null;
		iv = new byte[InternalZipConstants.AES_BLOCK_SIZE];
		counterBlock = new byte[InternalZipConstants.AES_BLOCK_SIZE];
		init(salt, passwordVerifier);
	}
	
	private void init(byte[] salt, byte[] passwordVerifier) throws ZipException {
		if (localFileHeader == null) {
			throw new ZipException("invalid file header in init method of AESDecryptor");
		}
		
		AESExtraDataRecord aesExtraDataRecord = localFileHeader.getAesExtraDataRecord();
		if (aesExtraDataRecord == null) {
			throw new ZipException("invalid aes extra data record - in init method of AESDecryptor");
		}
		
		switch (aesExtraDataRecord.getAesStrength()) {
		case Zip4jConstants.AES_STRENGTH_128:
			KEY_LENGTH = 16;
			MAC_LENGTH = 16;
			SALT_LENGTH = 8;
			break;
		case Zip4jConstants.AES_STRENGTH_192:
			KEY_LENGTH = 24;
			MAC_LENGTH = 24;
			SALT_LENGTH = 12;
			break;
		case Zip4jConstants.AES_STRENGTH_256:
			KEY_LENGTH = 32;
			MAC_LENGTH = 32;
			SALT_LENGTH = 16;
			break;
		default:
			throw new ZipException("invalid aes key strength for file: " + localFileHeader.getFileName());
		}
		
		if (localFileHeader.getPassword() == null || localFileHeader.getPassword().length <= 0) {
			throw new ZipException("empty or null password provided for AES Decryptor");
		}
		
		byte[] derivedKey = deriveKey(salt, localFileHeader.getPassword());
		if (derivedKey == null || 
				derivedKey.length != (KEY_LENGTH + MAC_LENGTH + PASSWORD_VERIFIER_LENGTH)) {
			throw new ZipException("invalid derived key");
		}
		
		aesKey = new byte[KEY_LENGTH];
		macKey = new byte[MAC_LENGTH];
		derivedPasswordVerifier = new byte[PASSWORD_VERIFIER_LENGTH];
		
		System.arraycopy(derivedKey, 0, aesKey, 0, KEY_LENGTH);
		System.arraycopy(derivedKey, KEY_LENGTH, macKey, 0, MAC_LENGTH);
		System.arraycopy(derivedKey, KEY_LENGTH + MAC_LENGTH, derivedPasswordVerifier, 0, PASSWORD_VERIFIER_LENGTH);
		
		if (derivedPasswordVerifier == null) {
			throw new ZipException("invalid derived password verifier for AES");
		}
		
		if (!Arrays.equals(passwordVerifier, derivedPasswordVerifier)) {
			throw new ZipException("Wrong Password for file: " + localFileHeader.getFileName(), ZipExceptionConstants.WRONG_PASSWORD);
		}
		
		aesEngine = new AESEngine(aesKey);
		mac = new MacBasedPRF("HmacSHA1");
		mac.init(macKey);
	}
	
	public int decryptData(byte[] buff, int start, int len) throws ZipException {
		
		if (aesEngine == null) {
			throw new ZipException("AES not initialized properly");
		}
		
		try {
			
			for (int j = start; j < (start + len); j += InternalZipConstants.AES_BLOCK_SIZE) {
				loopCount = (j + InternalZipConstants.AES_BLOCK_SIZE <= (start + len)) ? 
						InternalZipConstants.AES_BLOCK_SIZE : ((start + len) - j);
				
				mac.update(buff, j, loopCount);
				Raw.prepareBuffAESIVBytes(iv, nonce, InternalZipConstants.AES_BLOCK_SIZE);
				aesEngine.processBlock(iv, counterBlock);
				
				for (int k = 0; k < loopCount; k++) {
					buff[j + k] = (byte)(buff[j + k] ^ counterBlock[k]);
				}
				
				nonce++;
			}
			
			return len;
			
		} catch (ZipException e) {
			throw e;
		} catch (Exception e) {
			throw new ZipException(e);
		}
	}
	
	public int decryptData(byte[] buff) throws ZipException {
		return decryptData(buff, 0, buff.length);
	}
	
	private byte[] deriveKey(byte[] salt, char[] password) throws ZipException {
		try {
			PBKDF2Parameters p = new PBKDF2Parameters("HmacSHA1", "ISO-8859-1",
	                    salt, 1000);
	        PBKDF2Engine e = new PBKDF2Engine(p);
	        byte[] derivedKey = e.deriveKey(password, KEY_LENGTH + MAC_LENGTH + PASSWORD_VERIFIER_LENGTH);
			return derivedKey;
		} catch (Exception e) {
			throw new ZipException(e);
		}
	}
	
	public int getPasswordVerifierLength() {
		return PASSWORD_VERIFIER_LENGTH;
	}
	
	public int getSaltLength() {
		return SALT_LENGTH;
	}
	
	public byte[] getCalculatedAuthenticationBytes() {
		return mac.doFinal();
	}
	
	public void setStoredMac(byte[] storedMac) {
		this.storedMac = storedMac;
	}

	public byte[] getStoredMac() {
		return storedMac;
	}

//	public byte[] getStoredMac() throws ZipException {
//		if (raf == null) {
//			throw new ZipException("attempting to read MAC on closed file handle");
//		}
//		
//		try {
//			byte[] storedMacBytes = new byte[InternalZipConstants.AES_AUTH_LENGTH];
//			int bytesRead = raf.read(storedMacBytes);
//			if (bytesRead != InternalZipConstants.AES_AUTH_LENGTH) {
//				if (zipModel.isSplitArchive()) {
////					unzipEngine.startNextSplitFile();
//					if (bytesRead == -1) bytesRead = 0;
//					int newlyRead = raf.read(storedMacBytes, bytesRead, InternalZipConstants.AES_AUTH_LENGTH - bytesRead);
//					bytesRead += newlyRead;
//					if (bytesRead != InternalZipConstants.AES_AUTH_LENGTH) {
//						throw new ZipException("invalid number of bytes read for stored MAC after starting split file");
//					}
//				} else {
//					throw new ZipException("invalid number of bytes read for stored MAC");
//				}
//			}
//			return storedMacBytes;
//		} catch (IOException e) {
//			throw new ZipException(e);
//		} catch (Exception e) {
//			throw new ZipException(e);
//		}
//		
//	}
}
