/*
 * JBoss, Home of Professional Open Source.
 * See the COPYRIGHT.txt file distributed with this work for information
 * regarding copyright ownership.  Some portions may be licensed
 * to Red Hat, Inc. under one or more contributor license agreements.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 */

package org.teiid.common.buffer.impl;

import java.io.IOException;
import java.security.GeneralSecurityException;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;

import org.teiid.common.buffer.FileStore;
import org.teiid.common.buffer.StorageManager;
import org.teiid.core.TeiidComponentException;
import org.teiid.core.TeiidRuntimeException;
import org.teiid.core.crypto.CryptoException;
import org.teiid.core.crypto.SymmetricCryptor;

/**
 * Implements a block AES cipher over a regular filestore.
 * <br>
 * With ECB mode and no padding, we just replace the
 * the bytes 1 block at a time with some special handling
 * for when not block aligned.
 * <br>
 * A great deal of the security comes from the encryption 
 * key only being used on a temporary basis.  We also xor 
 * by the block to add a very simple CTR like mode so that
 * identical blocks don't result in the same storage bytes.
 * <br>
 * TODO: use masking for division
 */
public class EncryptedStorageManager implements StorageManager {

	private static final String DEFAULT_ALGORITHM = "AES/ECB/NoPadding"; //$NON-NLS-1$

	static final class EncryptedFileStore extends FileStore {
		private final FileStore file;
		private volatile long len;
		private Cipher encrypt;
		private Cipher decrypt;
		private int blockSize;

		private EncryptedFileStore(FileStore file, SecretKey key) throws GeneralSecurityException {
			this.file = file;
			decrypt = Cipher.getInstance( DEFAULT_ALGORITHM); 
			decrypt.init( Cipher.DECRYPT_MODE, key );
			encrypt = Cipher.getInstance( DEFAULT_ALGORITHM);
			blockSize = encrypt.getBlockSize();
			encrypt.init( Cipher.ENCRYPT_MODE, key );
		}

		@Override
		public synchronized void setLength(long length) throws IOException {
			this.len = length;
			if (length % blockSize == 0) {
				file.setLength(length);
			} else {
				file.setLength((length/blockSize + 1)*blockSize);
			}
		}

		@Override
		protected void removeDirect() {
			file.remove();
		}
		
		@Override
		protected synchronized int readWrite(long fileOffset, byte[] b, int offSet, int length,
				boolean write) throws IOException {
			if (length == 0) {
				return 0;
			}
			long block = fileOffset/blockSize;
			int remainder = (int)(fileOffset%blockSize);
			if (!write) {
				if (fileOffset > len) {
	    			throw new IOException("Invalid file position " + fileOffset + " length " + length); //$NON-NLS-1$ //$NON-NLS-2$
	    		}
				length = (int)Math.min(Integer.MAX_VALUE, Math.min(len - fileOffset, length));
			}
			long adjustedfileOffset = fileOffset;
			int adjustedLength = length;
			byte[] buffer = b;
			int bufferOffset = offSet;
			int remainingLength = (int) ((length+fileOffset)%blockSize);
			if (remainder!=0||remainingLength!=0) {
				adjustedfileOffset -= remainder;
				adjustedLength += remainder;
				if (adjustedLength%blockSize!=0) {
					//round to the next block
					adjustedLength=(adjustedLength/blockSize + 1)*blockSize;
				}
				//create a new fully aligned buffer
				buffer = new byte[adjustedLength];
				bufferOffset = 0;
			}
			int blocks = adjustedLength/blockSize;
			int blockOffset = 0;
			if (!write) {
				file.readFully(adjustedfileOffset, buffer, bufferOffset, adjustedLength);
				for (int i = 0; i < blocks; i++) {
					try {
						//TODO: directly copy into the output buffer when not block aligned
						decrypt.doFinal(buffer, blockOffset, blockSize, buffer, blockOffset);
						xorByBlock(buffer, 0, block + i, blockOffset);
					} catch (GeneralSecurityException e) {
						throw new IOException(e);
					}
					blockOffset += blockSize;
				}
				if (adjustedLength != length) {
					//copy back the proper subset into the output buffer
					System.arraycopy(buffer, remainder, b, offSet, length);
				}
				return length;
			}
			if (remainder!=0) {
				//need to read the partial value and re-encrypt the whole block
				readFully(fileOffset - remainder, buffer, 0, remainder);
			} else if (buffer == b) {
				//create a temp buffer to do the encryption in
				buffer = new byte[adjustedLength];
			}
			System.arraycopy(b, offSet, buffer, remainder, length);
			for (int i = 0; i < blocks; i++) {
				try {
					if (i + 1 == blocks && remainingLength != 0 && fileOffset + length < len) {
						//TODO: combine with read above when working with a small enough length
						readFully(fileOffset + length, buffer, blockOffset + remainingLength, (int)Math.min(blockSize - remainingLength, len - fileOffset - length));
					}
					xorByBlock(buffer, offSet, block + i, blockOffset);
					encrypt.doFinal(buffer, blockOffset, blockSize, buffer, blockOffset);
				} catch (GeneralSecurityException e) {
					throw new IOException(e);
				}
				blockOffset += blockSize;
			}
			file.write(adjustedfileOffset, buffer, 0, adjustedLength);
			len = Math.max(len, fileOffset + length);
			return length;
		}

		private void xorByBlock(byte[] b, int offSet, long blockMask,
				int blockOffset) {
			for (int j = 0; j < blockSize && blockMask > 0; j++) {
				b[offSet + blockOffset + j] ^= blockMask;
				blockMask >>= 8;
			}
		}

		@Override
		public long getLength() {
			return len;
		}
		
		FileStore getFile() {
			return file;
		}
	}

	private StorageManager manager;
	private SecretKey key;
	
	public EncryptedStorageManager(StorageManager manager) {
		this.manager = manager;
	}

	@Override
	public void initialize() throws TeiidComponentException {
		manager.initialize();
		try {
			key = SymmetricCryptor.generateKey();
		} catch (CryptoException e) {
			throw new TeiidComponentException(e);
		}
	}

	@Override
	public EncryptedFileStore createFileStore(String name) {
		final FileStore file = manager.createFileStore(name);
		try {
			return new EncryptedFileStore(file, key);
		} catch (GeneralSecurityException e) {
			throw new TeiidRuntimeException(e);
		}
	}
	
}

