/* This code is part of Freenet. It is distributed under the GNU General
 * Public License, version 2 (or at your option any later version). See
 * http://www.gnu.org/ for further details of the GPL. */
package freenet.keys;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.Provider;
import java.util.Arrays;

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import freenet.crypt.BlockCipher;
import freenet.crypt.CTRBlockCipher;
import freenet.crypt.JceLoader;
import freenet.crypt.PCFBMode;
import freenet.crypt.SHA256;
import freenet.crypt.UnsupportedCipherException;
import freenet.crypt.Util;
import freenet.crypt.ciphers.Rijndael;
import freenet.keys.Key.Compressed;
import freenet.node.Node;
import freenet.support.Logger;
import freenet.support.api.Bucket;
import freenet.support.api.BucketFactory;
import freenet.support.compress.InvalidCompressionCodecException;
import freenet.support.io.ArrayBucket;
import freenet.support.io.ArrayBucketFactory;
import freenet.support.io.BucketTools;
import freenet.support.math.MersenneTwister;

/**
 * @author amphibian
 * 
 * Client CHKBlock - provides functions for decoding, holds a client-key.
 */
public class ClientCHKBlock implements ClientKeyBlock {

	final ClientCHK key;
	private final CHKBlock block;
	
    @Override
	public String toString() {
        return super.toString()+",key="+key;
    }
    
    /**
     * Construct from data retrieved, and a key.
     * Do not do full decode. Verify what can be verified without doing
     * a full decode.
     * @param key2 The client key.
     * @param header The header.
     * @param data The data.
     */
    public ClientCHKBlock(byte[] data, byte[] header, ClientCHK key2, boolean verify) throws CHKVerifyException {
        block = new CHKBlock(data, header, key2.getNodeCHK(), verify, key2.cryptoAlgorithm);
        this.key = key2;
    }

    /**
     * Construct from a CHKBlock and a key.
     */
    public ClientCHKBlock(CHKBlock block, ClientCHK key2) throws CHKVerifyException {
        this(block.getData(), block.getHeaders(), key2, true);
    }

    /**
     * Decode into RAM, if short.
     * @throws CHKDecodeException 
     */
	@Override
	public byte[] memoryDecode() throws CHKDecodeException {
		try {
			ArrayBucket a = (ArrayBucket) decode(new ArrayBucketFactory(), 32*1024, false);
			return BucketTools.toByteArray(a); // FIXME
		} catch (IOException e) {
			throw new Error(e);
		}
	}

    /**
     * Decode the CHK and recover the original data
     * @return the original data
     * @throws IOException If there is a bucket error.
     */
    @Override
    public Bucket decode(BucketFactory bf, int maxLength, boolean dontCompress) throws CHKDecodeException, IOException {
    	return decode(bf, maxLength, dontCompress, false);
    }
    
    // forceNoJCA for unit tests.
    Bucket decode(BucketFactory bf, int maxLength, boolean dontCompress, boolean forceNoJCA) throws CHKDecodeException, IOException {
    	if(key.cryptoAlgorithm == Key.ALGO_AES_PCFB_256_SHA256)
    		return decodeOld(bf, maxLength, dontCompress);
    	else if(key.cryptoAlgorithm == Key.ALGO_AES_CTR_256_SHA256)
		{
				if(Rijndael.AesCtrProvider == null || forceNoJCA)
					return decodeNewNoJCA(bf, maxLength, dontCompress);
				else
					return decodeNew(bf, maxLength, dontCompress);
		}
		else
    		throw new UnsupportedOperationException();
    }

	
    /**
     * Decode the CHK and recover the original data
     * @return the original data
     * @throws IOException If there is a bucket error.
     */
    @SuppressWarnings("deprecation") // FIXME Back compatibility, using dubious ciphers; remove eventually.
    public Bucket decodeOld(BucketFactory bf, int maxLength, boolean dontCompress) throws CHKDecodeException, IOException {
        // Overall hash already verified, so first job is to decrypt.
		if(key.cryptoAlgorithm != Key.ALGO_AES_PCFB_256_SHA256)
            throw new UnsupportedOperationException();
        BlockCipher cipher;
        try {
            cipher = new Rijndael(256, 256);
        } catch (UnsupportedCipherException e) {
            // FIXME - log this properly
            throw new Error(e);
        }
        byte[] cryptoKey = key.cryptoKey;
        if(cryptoKey.length < Node.SYMMETRIC_KEY_LENGTH)
            throw new CHKDecodeException("Crypto key too short");
        cipher.initialize(key.cryptoKey);
        PCFBMode pcfb = PCFBMode.create(cipher);
        byte[] headers = block.headers;
        byte[] data = block.data;
		byte[] hbuf = Arrays.copyOfRange(headers, 2, headers.length);
        byte[] dbuf = Arrays.copyOf(data, data.length);
        // Decipher header first - functions as IV
        pcfb.blockDecipher(hbuf, 0, hbuf.length);
        pcfb.blockDecipher(dbuf, 0, dbuf.length);
        MessageDigest md256 = SHA256.getMessageDigest();
        byte[] dkey = key.cryptoKey;
        // Check: IV == hash of decryption key
        byte[] predIV = md256.digest(dkey);
        SHA256.returnMessageDigest(md256); md256 = null;
        // Extract the IV
        byte[] iv = Arrays.copyOf(hbuf, 32);
        if(!Arrays.equals(iv, predIV))
            throw new CHKDecodeException("Check failed: Decrypted IV == H(decryption key)");
        // Checks complete
        int size = ((hbuf[32] & 0xff) << 8) + (hbuf[33] & 0xff);
        if((size > 32768) || (size < 0)) {
            throw new CHKDecodeException("Invalid size: "+size);
        }
        return Key.decompress(dontCompress ? false : key.isCompressed(), dbuf, size, bf, 
        		Math.min(maxLength, CHKBlock.MAX_LENGTH_BEFORE_COMPRESSION), key.compressionAlgorithm, false);
    }
    
	private static final Provider hmacProvider;
	static private long benchmark(Mac hmac) throws GeneralSecurityException
	{
		long times = Long.MAX_VALUE;
		byte[] input = new byte[1024];
		byte[] output = new byte[hmac.getMacLength()];
		byte[] key = new byte[Node.SYMMETRIC_KEY_LENGTH];
		final String algo = hmac.getAlgorithm();
		hmac.init(new SecretKeySpec(key, algo));
		// warm-up
		for (int i = 0; i < 32; i++) {
			hmac.update(input, 0, input.length);
			hmac.doFinal(output, 0);
			System.arraycopy(output, 0, input, (i*output.length)%(input.length-output.length), output.length);
		}
		System.arraycopy(output, 0, key, 0, Math.min(key.length, output.length));
		for (int i = 0; i < 1024; i++) {
			long startTime = System.nanoTime();
			hmac.init(new SecretKeySpec(key, algo));
			for (int j = 0; j < 8; j++) {
				for (int k = 0; k < 32; k ++) {
					hmac.update(input, 0, input.length);
				}
				hmac.doFinal(output, 0);
			}
			long endTime = System.nanoTime();
			times = Math.min(endTime - startTime, times);
			System.arraycopy(output, 0, input, 0, output.length);
			System.arraycopy(output, 0, key, 0, Math.min(key.length, output.length));
		}
		return times;
	}
	static {
		try {
			final Class<ClientCHKBlock> clazz = ClientCHKBlock.class;
			final String algo = "HmacSHA256";
			final Provider sun = JceLoader.SunJCE;
			SecretKeySpec dummyKey = new SecretKeySpec(new byte[Node.SYMMETRIC_KEY_LENGTH], algo);
			Mac hmac = Mac.getInstance(algo);
			hmac.init(dummyKey); // resolve provider
			boolean logMINOR = Logger.shouldLog(Logger.LogLevel.MINOR, clazz);
			if (sun != null) {
				// SunJCE provider is faster (in some configurations)
				try {
					Mac sun_hmac = Mac.getInstance(algo, sun);
					sun_hmac.init(dummyKey); // resolve provider
					if (hmac.getProvider() != sun_hmac.getProvider()) {
						long time_def = benchmark(hmac);
						long time_sun = benchmark(sun_hmac);
						System.out.println(algo + " (" + hmac.getProvider() + "): " + time_def + "ns");
						System.out.println(algo + " (" + sun_hmac.getProvider() + "): " + time_sun + "ns");
						if(logMINOR) {
							Logger.minor(clazz, algo + "/" + hmac.getProvider() + ": " + time_def + "ns");
							Logger.minor(clazz, algo + "/" + sun_hmac.getProvider() + ": " + time_sun + "ns");
						}
						if (time_sun < time_def) {
							hmac = sun_hmac;
						}
					}
				} catch(GeneralSecurityException e) {
					Logger.warning(clazz, algo + "@" + sun + " benchmark failed", e);
					// ignore

				} catch(Throwable e) {
					Logger.error(clazz, algo + "@" + sun + " benchmark failed", e);
					// ignore
				}
			}
			hmacProvider = hmac.getProvider();
			System.out.println(algo + ": using " + hmacProvider);
			Logger.normal(clazz, algo + ": using " + hmacProvider);
		} catch(GeneralSecurityException e) {
			// impossible 
			throw new Error(e);
		}
	}

    /**
     * Decode the CHK and recover the original data
     * @return the original data
     * @throws IOException If there is a bucket error.
     */
    public Bucket decodeNew(BucketFactory bf, int maxLength, boolean dontCompress) throws CHKDecodeException, IOException {
		if(key.cryptoAlgorithm != Key.ALGO_AES_CTR_256_SHA256)
			throw new UnsupportedOperationException();
        byte[] headers = block.headers;
        byte[] data = block.data;
    	byte[] hash = Arrays.copyOfRange(headers, 2, 2+32);
        byte[] cryptoKey = key.cryptoKey;
        if(cryptoKey.length < Node.SYMMETRIC_KEY_LENGTH)
            throw new CHKDecodeException("Crypto key too short");
		try {
        Cipher cipher = Cipher.getInstance("AES/CTR/NOPADDING", Rijndael.AesCtrProvider);
        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(cryptoKey, "AES"), new IvParameterSpec(hash, 0, 16));
        byte[] plaintext = new byte[data.length + 2];
		int moved = cipher.update(data, 0, data.length, plaintext);
		cipher.doFinal(headers, hash.length+2, 2, plaintext, moved);
        int size = ((plaintext[data.length] & 0xff) << 8) + (plaintext[data.length + 1] & 0xff);
        if((size > 32768) || (size < 0)) {
            throw new CHKDecodeException("Invalid size: "+size);
        }
        // Check the hash.
        Mac hmac = Mac.getInstance("HmacSHA256", hmacProvider);
        hmac.init(new SecretKeySpec(cryptoKey, "HmacSHA256"));
        hmac.update(plaintext); // plaintext includes lengthBytes
        byte[] hashCheck = hmac.doFinal();
        if(!Arrays.equals(hash, hashCheck)) {
        	throw new CHKDecodeException("HMAC is wrong, wrong decryption key?");
        }
        return Key.decompress(dontCompress ? false : key.isCompressed(), plaintext, size, bf, 
        		Math.min(maxLength, CHKBlock.MAX_LENGTH_BEFORE_COMPRESSION), key.compressionAlgorithm, false);
		} catch(GeneralSecurityException e) {
			throw new CHKDecodeException("Problem with JCA, should be impossible!", e);
		}
    }

    /**
     * Decode using Freenet's built in crypto. FIXME remove once Java 1.7
     * is mandatory. Note that we assume that HMAC SHA256 is available; the
     * problem is AES is limited to 128 bits.
     * @return the original data
     * @throws IOException If there is a bucket error.
     */
    public Bucket decodeNewNoJCA(BucketFactory bf, int maxLength, boolean dontCompress) throws CHKDecodeException, IOException {
		if(key.cryptoAlgorithm != Key.ALGO_AES_CTR_256_SHA256)
			throw new UnsupportedOperationException();
        byte[] headers = block.headers;
        byte[] data = block.data;
    	byte[] hash = Arrays.copyOfRange(headers, 2, 2+32);
        byte[] cryptoKey = key.cryptoKey;
        if(cryptoKey.length < Node.SYMMETRIC_KEY_LENGTH)
            throw new CHKDecodeException("Crypto key too short");
        Rijndael aes;
        try {
			aes = new Rijndael(256, 128);
		} catch (UnsupportedCipherException e) {
			// Impossible.
			throw new Error(e);
		}
		aes.initialize(cryptoKey);
        CTRBlockCipher cipher = new CTRBlockCipher(aes);
        cipher.init(hash, 0, 16);
        byte[] plaintext = new byte[data.length];
        cipher.processBytes(data, 0, data.length, plaintext, 0);
        byte[] lengthBytes = new byte[2];
        cipher.processBytes(headers, hash.length+2, 2, lengthBytes, 0);
        int size = ((lengthBytes[0] & 0xff) << 8) + (lengthBytes[1] & 0xff);
        if((size > 32768) || (size < 0)) {
            throw new CHKDecodeException("Invalid size: "+size);
        }
		try {
        // Check the hash.
        Mac hmac = Mac.getInstance("HmacSHA256", hmacProvider);
        hmac.init(new SecretKeySpec(cryptoKey, "HmacSHA256"));
        hmac.update(plaintext);
        hmac.update(lengthBytes);
        byte[] hashCheck = hmac.doFinal();
        if(!Arrays.equals(hash, hashCheck)) {
        	throw new CHKDecodeException("HMAC is wrong, wrong decryption key?");
        }
		} catch(GeneralSecurityException e) {
			throw new CHKDecodeException("Problem with JCA, should be impossible!", e);
		}
        return Key.decompress(dontCompress ? false : key.isCompressed(), plaintext, size, bf, 
        		Math.min(maxLength, CHKBlock.MAX_LENGTH_BEFORE_COMPRESSION), key.compressionAlgorithm, false);
    }

    /**
     * Encode a splitfile block.
     * @param data The data to encode. Must be exactly DATA_LENGTH bytes.
     * @param cryptoKey The encryption key. Can be null in which case this is equivalent to a normal block
     * encode.
     */
    static public ClientCHKBlock encodeSplitfileBlock(byte[] data, byte[] cryptoKey, byte cryptoAlgorithm) throws CHKEncodeException {
    	if(data.length != CHKBlock.DATA_LENGTH) throw new IllegalArgumentException();
    	if(cryptoKey != null && cryptoKey.length != 32) throw new IllegalArgumentException();
        MessageDigest md256 = SHA256.getMessageDigest();
        // No need to pad
        if(cryptoKey == null) {
        	cryptoKey = md256.digest(data);
        }
        	if(cryptoAlgorithm == Key.ALGO_AES_PCFB_256_SHA256)
        		return innerEncode(data, CHKBlock.DATA_LENGTH, md256, cryptoKey, false, (short)-1, cryptoAlgorithm);
        	else if(cryptoAlgorithm != Key.ALGO_AES_CTR_256_SHA256)
        		throw new IllegalArgumentException("Unknown crypto algorithm: "+cryptoAlgorithm);
        	if(Rijndael.AesCtrProvider == null) {
        		return encodeNewNoJCA(data, CHKBlock.DATA_LENGTH, md256, cryptoKey, false, (short)-1, cryptoAlgorithm, KeyBlock.HASH_SHA256);
        	} else {
        		return encodeNew(data, CHKBlock.DATA_LENGTH, md256, cryptoKey, false, (short)-1, cryptoAlgorithm, KeyBlock.HASH_SHA256);
			}
    }
    
    /**
     * Encode a Bucket of data to a CHKBlock.
     * @param sourceData The bucket of data to encode. Can be arbitrarily large.
     * @param asMetadata Is this a metadata key?
     * @param dontCompress If set, don't even try to compress.
     * @param alreadyCompressedCodec If !dontCompress, and this is >=0, then the
     * data is already compressed, and this is the algorithm.
     * @param compressorDescriptor 
     * @param cryptoAlgorithm 
     * @param cryptoKey 
     * @throws CHKEncodeException
     * @throws IOException If there is an error reading from the Bucket.
     * @throws InvalidCompressionCodecException 
     */
    static public ClientCHKBlock encode(Bucket sourceData, boolean asMetadata, boolean dontCompress, short alreadyCompressedCodec, long sourceLength, String compressorDescriptor, boolean pre1254, byte[] cryptoKey, byte cryptoAlgorithm) throws CHKEncodeException, IOException {
    	return encode(sourceData, asMetadata, dontCompress, alreadyCompressedCodec, sourceLength, compressorDescriptor, pre1254, cryptoKey, cryptoAlgorithm, false);
    }

    // forceNoJCA for unit tests.
    static ClientCHKBlock encode(Bucket sourceData, boolean asMetadata, boolean dontCompress, short alreadyCompressedCodec, long sourceLength, String compressorDescriptor, boolean pre1254, byte[] cryptoKey, byte cryptoAlgorithm, boolean forceNoJCA) throws CHKEncodeException, IOException {
        byte[] finalData = null;
        byte[] data;
        short compressionAlgorithm = -1;
        try {
			Compressed comp = Key.compress(sourceData, dontCompress, alreadyCompressedCodec, sourceLength, CHKBlock.MAX_LENGTH_BEFORE_COMPRESSION, CHKBlock.DATA_LENGTH, false, compressorDescriptor, pre1254);
			finalData = comp.compressedData;
			compressionAlgorithm = comp.compressionAlgorithm;
		} catch (KeyEncodeException e2) {
			throw new CHKEncodeException(e2.getMessage(), e2);
		} catch (InvalidCompressionCodecException e2) {
			throw new CHKEncodeException(e2.getMessage(), e2);
		}
        // Now do the actual encode
        
        MessageDigest md256 = SHA256.getMessageDigest();
        // First pad it
        int dataLength = finalData.length;
        if(finalData.length != 32768) {
            // Hash the data
            if(finalData.length != 0)
            	md256.update(finalData);
            byte[] digest = md256.digest();
            MersenneTwister mt = new MersenneTwister(digest);
			data = Arrays.copyOf(finalData, 32768);
			Util.randomBytes(mt, data, finalData.length, 32768-finalData.length);
        } else {
        	data = finalData;
        }
        // Now make the header
        byte[] encKey;
        if(cryptoKey != null)
        	encKey = cryptoKey;
        else
        	encKey = md256.digest(data);
    	if(cryptoAlgorithm == 0) {
    		// TODO find all such cases and fix them.
    		Logger.error(ClientCHKBlock.class, "Passed in 0 crypto algorithm", new Exception("warning"));
    		cryptoAlgorithm = Key.ALGO_AES_PCFB_256_SHA256;
    	}
        if(cryptoAlgorithm == Key.ALGO_AES_PCFB_256_SHA256)
        	return innerEncode(data, dataLength, md256, encKey, asMetadata, compressionAlgorithm, cryptoAlgorithm);
		else {
				if(Rijndael.AesCtrProvider == null || forceNoJCA)
					return encodeNewNoJCA(data, dataLength, md256, encKey, asMetadata, compressionAlgorithm, cryptoAlgorithm, KeyBlock.HASH_SHA256);
				else
					return encodeNew(data, dataLength, md256, encKey, asMetadata, compressionAlgorithm, cryptoAlgorithm, KeyBlock.HASH_SHA256);
		}
    }
    
    /**
     * Format:
     * [0-1]: Block hash algorithm
     * [2-34]: HMAC (with cryptokey) of data + length bytes.
     * [35-36]: Length bytes.
     * Encryption: CTR with IV = 1st 16 bytes of the hash. (It has to be 
     * deterministic as this is a CHK and we need to be able to reinsert them
     * easily):
     * - Data
     * - Length bytes.
     * @param data Data should already have been padded.
     * @param dataLength Length of original data. Between 0 and 32768.
     * @param md256 Convenient reuse of hash object.
     * @param encKey Encryption key for the data, part of the URI.
     * @param asMetadata Whether the final CHK is metadata or not.
     * @param compressionAlgorithm The compression algorithm used.
     * @param cryptoAlgorithm The encryption algorithm used.
     * @return
     */
    public static ClientCHKBlock encodeNew(byte[] data, int dataLength, MessageDigest md256, byte[] encKey, boolean asMetadata, short compressionAlgorithm, byte cryptoAlgorithm, int blockHashAlgorithm) throws CHKEncodeException {
    	if(cryptoAlgorithm != Key.ALGO_AES_CTR_256_SHA256)
    		throw new IllegalArgumentException("Unsupported crypto algorithm "+cryptoAlgorithm);
		try {
    	// IV = HMAC<cryptokey>(plaintext).
        // It's okay that this is the same for 2 blocks with the same key and the same content.
        // In fact that's the point; this is still a Content Hash Key.
        // FIXME And yes we should check on insert for multiple identical keys.
        Mac hmac = Mac.getInstance("HmacSHA256", hmacProvider);
        hmac.init(new SecretKeySpec(encKey, "HmacSHA256"));
        byte[] tmpLen = new byte[] { 
            	(byte)(dataLength >> 8), (byte)(dataLength & 0xff)
            };
        hmac.update(data);
        hmac.update(tmpLen);
        byte[] hash = hmac.doFinal();
        byte[] header = new byte[hash.length+2+2];
    	if(blockHashAlgorithm == 0) cryptoAlgorithm = KeyBlock.HASH_SHA256;
    	if(blockHashAlgorithm != KeyBlock.HASH_SHA256)
    		throw new IllegalArgumentException("Unsupported block hash algorithm "+cryptoAlgorithm);
        header[0] = (byte)(blockHashAlgorithm >> 8);
        header[1] = (byte)(blockHashAlgorithm & 0xff);
        System.arraycopy(hash, 0, header, 2, hash.length);
        SecretKey ckey = new SecretKeySpec(encKey, "AES");
        // CTR mode IV is only 16 bytes.
        // That's still plenty though. It will still be unique.
        Cipher cipher = Cipher.getInstance("AES/CTR/NOPADDING", Rijndael.AesCtrProvider);
        cipher.init(Cipher.ENCRYPT_MODE, ckey, new IvParameterSpec(hash, 0, 16));
        byte[] cdata = new byte[data.length];
		int moved = cipher.update(data, 0, data.length, cdata);
		if (moved == data.length) {
			cipher.doFinal(tmpLen, 0, 2, header, hash.length+2);
		} else {
			// FIXME inefficient
			byte[] tmp = cipher.doFinal(tmpLen, 0, 2);
			System.arraycopy(tmp, 0, cdata, moved, tmp.length-2);
			System.arraycopy(tmp, tmp.length-2,	header, hash.length+2, 2);
		}
        
        // Now calculate the final hash
        md256.update(header);
        byte[] finalHash = md256.digest(cdata);
        
        SHA256.returnMessageDigest(md256);
        
        // Now convert it into a ClientCHK
        ClientCHK finalKey = new ClientCHK(finalHash, encKey, asMetadata, cryptoAlgorithm, compressionAlgorithm);
        
        try {
        	return new ClientCHKBlock(cdata, header, finalKey, false);
        } catch (CHKVerifyException e3) {
            //WTF?
            throw new Error(e3);
        }
		} catch (GeneralSecurityException e) {
			throw new CHKEncodeException("Problem with JCA, should be impossible!", e);
		}
    }
    
    /**
     * Encode using Freenet's built in crypto. FIXME remove once Java 1.7
     * is mandatory. Note that we assume that HMAC SHA256 is available; the
     * problem is AES is limited to 128 bits.
     * @param data Data should already have been padded.
     * @param dataLength Length of original data. Between 0 and 32768.
     * @param md256 Convenient reuse of hash object.
     * @param encKey Encryption key for the data, part of the URI.
     * @param asMetadata Whether the final CHK is metadata or not.
     * @param compressionAlgorithm The compression algorithm used.
     * @param cryptoAlgorithm The encryption algorithm used.
     * @return
     * @throws CHKVerifyException
     * @throws CHKEncodeException
     */
    public static ClientCHKBlock encodeNewNoJCA(byte[] data, int dataLength, MessageDigest md256, byte[] encKey, boolean asMetadata, short compressionAlgorithm, byte cryptoAlgorithm, int blockHashAlgorithm) throws CHKEncodeException {
    	if(cryptoAlgorithm != Key.ALGO_AES_CTR_256_SHA256)
    		throw new IllegalArgumentException("Unsupported crypto algorithm "+cryptoAlgorithm);
		try {
    	// IV = HMAC<cryptokey>(plaintext).
        // It's okay that this is the same for 2 blocks with the same key and the same content.
        // In fact that's the point; this is still a Content Hash Key.
        // FIXME And yes we should check on insert for multiple identical keys.
        Mac hmac = Mac.getInstance("HmacSHA256", hmacProvider);
        hmac.init(new SecretKeySpec(encKey, "HmacSHA256"));
        byte[] tmpLen = new byte[] { 
            	(byte)(dataLength >> 8), (byte)(dataLength & 0xff)
            };
        hmac.update(data);
        hmac.update(tmpLen);
        byte[] hash = hmac.doFinal();
        byte[] header = new byte[hash.length+2+2];
    	if(blockHashAlgorithm == 0) cryptoAlgorithm = KeyBlock.HASH_SHA256;
    	if(blockHashAlgorithm != KeyBlock.HASH_SHA256)
    		throw new IllegalArgumentException("Unsupported block hash algorithm "+cryptoAlgorithm);
        header[0] = (byte)(blockHashAlgorithm >> 8);
        header[1] = (byte)(blockHashAlgorithm & 0xff);
        Rijndael aes;
		try {
			aes = new Rijndael(256, 128);
		} catch (UnsupportedCipherException e) {
			// Impossible
			throw new Error(e);
		}
        aes.initialize(encKey);
        CTRBlockCipher ctr = new CTRBlockCipher(aes);
        // CTR mode IV is only 16 bytes.
        // That's still plenty though. It will still be unique.
        ctr.init(hash, 0, 16);
        System.arraycopy(hash, 0, header, 2, hash.length);
        byte[] cdata = new byte[data.length];
        ctr.processBytes(data, 0, data.length, cdata, 0);
        ctr.processBytes(tmpLen, 0, 2, header, hash.length+2);
        
        // Now calculate the final hash
        md256.update(header);
        byte[] finalHash = md256.digest(cdata);
        
        SHA256.returnMessageDigest(md256);
        
        // Now convert it into a ClientCHK
        ClientCHK finalKey = new ClientCHK(finalHash, encKey, asMetadata, cryptoAlgorithm, compressionAlgorithm);
        
        try {
        	return new ClientCHKBlock(cdata, header, finalKey, false);
        } catch (CHKVerifyException e3) {
            //WTF?
            throw new Error(e3);
        }
		} catch (GeneralSecurityException e) {
			throw new CHKEncodeException("Problem with JCA, should be impossible!", e);
		}
    }
    
    @SuppressWarnings("deprecation") // FIXME Back compatibility, using dubious ciphers; remove eventually.
    public static ClientCHKBlock innerEncode(byte[] data, int dataLength, MessageDigest md256, byte[] encKey, boolean asMetadata, short compressionAlgorithm, byte cryptoAlgorithm) {
        data = data.clone(); // Will overwrite otherwise. Callers expect data not to be clobbered.
    	if(cryptoAlgorithm != Key.ALGO_AES_PCFB_256_SHA256)
    		throw new IllegalArgumentException("Unsupported crypto algorithm "+cryptoAlgorithm);
        byte[] header;
        ClientCHK key;
        // IV = E(H(crypto key))
        byte[] plainIV = md256.digest(encKey);
        header = new byte[plainIV.length+2+2];
        header[0] = (byte)(KeyBlock.HASH_SHA256 >> 8);
        header[1] = (byte)(KeyBlock.HASH_SHA256 & 0xff);
        System.arraycopy(plainIV, 0, header, 2, plainIV.length);
        header[plainIV.length+2] = (byte)(dataLength >> 8);
        header[plainIV.length+3] = (byte)(dataLength & 0xff);
        // GRRR, java 1.4 does not have any symmetric crypto
        // despite exposing asymmetric and hashes!
        
        // Now encrypt the header, then the data, using the same PCFB instance
        BlockCipher cipher;
        try {
            cipher = new Rijndael(256, 256);
        } catch (UnsupportedCipherException e) {
        	Logger.error(ClientCHKBlock.class, "Impossible: "+e, e);
            throw new Error(e);
        }
        cipher.initialize(encKey);
        
        // FIXME CRYPTO plainIV, the hash of the crypto key, is encrypted with a null IV.
        // In other words, it is XORed with E(0).
        // For splitfiles we reuse the same decryption key for multiple blocks; it is derived from the overall hash,
        // or it is set randomly.
        // So the plaintext *and* ciphertext IV is always the same.
        // And the following 32 bytes are always XORed with the same value.
        // Ouch!
        // Those bytes being 2 bytes for the length, followed by the first 30 bytes of the data.
        
        PCFBMode pcfb = PCFBMode.create(cipher);
        pcfb.blockEncipher(header, 2, header.length-2);
        pcfb.blockEncipher(data, 0, data.length);
        
        // Now calculate the final hash
        md256.update(header);
        byte[] finalHash = md256.digest(data);
        
        SHA256.returnMessageDigest(md256);
        
        // Now convert it into a ClientCHK
        key = new ClientCHK(finalHash, encKey, asMetadata, cryptoAlgorithm, compressionAlgorithm);
        
        try {
            return new ClientCHKBlock(data, header, key, false);
        } catch (CHKVerifyException e3) {
            //WTF?
            throw new Error(e3);
        }
    }
    
    /**
     * Encode a block of data to a CHKBlock.
     * @param sourceData The data to encode.
     * @param asMetadata Is this a metadata key?
     * @param dontCompress If set, don't even try to compress.
     * @param alreadyCompressedCodec If !dontCompress, and this is >=0, then the
     * data is already compressed, and this is the algorithm.
     * @param compressorDescriptor Should be null, or list of compressors to try.
     * @throws InvalidCompressionCodecException 
     */
    static public ClientCHKBlock encode(byte[] sourceData, boolean asMetadata, boolean dontCompress, short alreadyCompressedCodec, int sourceLength, String compressorDescriptor, boolean pre1254) throws CHKEncodeException, InvalidCompressionCodecException {
    	try {
			return encode(new ArrayBucket(sourceData), asMetadata, dontCompress, alreadyCompressedCodec, sourceLength, compressorDescriptor, pre1254, null, Key.ALGO_AES_CTR_256_SHA256);
		} catch (IOException e) {
			// Can't happen
			throw new Error(e);
		}
    }

    @Override
    public ClientCHK getClientKey() {
        return key;
    }

	@Override
	public boolean isMetadata() {
		return key.isMetadata();
	}

	@Override
	public int hashCode() {
		return key.hashCode;
	}
	
	@Override
	public boolean equals(Object o) {
		if(!(o instanceof ClientCHKBlock)) return false;
		ClientCHKBlock block = (ClientCHKBlock) o;
		if(!key.equals(block.key)) return false;
		return block.block.equals(this.block);
	}

	@Override
	public CHKBlock getBlock() {
		return block;
	}

	@Override
	public Key getKey() {
		return block.getKey();
	}
}
