/* 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.DataInput;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.util.Arrays;

import freenet.crypt.DSAPublicKey;
import freenet.crypt.SHA256;
import freenet.store.BlockMetadata;
import freenet.store.GetPubkey;
import freenet.support.Fields;
import freenet.support.HexUtil;
import freenet.support.Logger;

/**
 * An SSK is a Signed Subspace Key.
 * { pubkey, cryptokey, filename } -> document, basically.
 * To insert you need the private key corresponding to pubkey.
 * KSKs are implemented via SSKs.
 * 
 * This is just the key, so we have the hash of the pubkey, the entire cryptokey,
 * and the entire filename.
 */
public class NodeSSK extends Key {
	
	/** Crypto algorithm */
	final byte cryptoAlgorithm;
	/** Public key hash */
	final byte[] pubKeyHash;
	/** E(H(docname)) (E = encrypt using decrypt key, which only clients know) */
	final byte[] encryptedHashedDocname;
	/** The signature key, if we know it */
	transient DSAPublicKey pubKey;
	final int hashCode;
	
	public static final int SSK_VERSION = 1;
	
	public static final int PUBKEY_HASH_SIZE = 32;
	public static final int E_H_DOCNAME_SIZE = 32;
	public static final byte BASE_TYPE = 2;
	public static final int FULL_KEY_LENGTH = 66;
	public static final int ROUTING_KEY_LENGTH = 32;
	
	@Override
	public String toString() {
		return super.toString()+":pkh="+HexUtil.bytesToHex(pubKeyHash)+":ehd="+HexUtil.bytesToHex(encryptedHashedDocname);
	}
	
	@Override
	public Key archivalCopy() {
		return new ArchiveNodeSSK(pubKeyHash, encryptedHashedDocname, cryptoAlgorithm);
	}
	
	public NodeSSK(byte[] pkHash, byte[] ehDocname, byte cryptoAlgorithm) {
		super(makeRoutingKey(pkHash, ehDocname));
		this.encryptedHashedDocname = ehDocname;
		this.pubKeyHash = pkHash;
		this.cryptoAlgorithm = cryptoAlgorithm;
		this.pubKey = null;
		if(ehDocname.length != E_H_DOCNAME_SIZE)
			throw new IllegalArgumentException("ehDocname must be "+E_H_DOCNAME_SIZE+" bytes");
		if(pkHash.length != PUBKEY_HASH_SIZE)
			throw new IllegalArgumentException("pubKeyHash must be "+PUBKEY_HASH_SIZE+" bytes");
		hashCode = Fields.hashCode(pkHash) ^ Fields.hashCode(ehDocname);
	}
	
	public NodeSSK(byte[] pkHash, byte[] ehDocname, DSAPublicKey pubKey, byte cryptoAlgorithm) throws SSKVerifyException {
		super(makeRoutingKey(pkHash, ehDocname));
		this.encryptedHashedDocname = ehDocname;
		this.pubKeyHash = pkHash;
		this.cryptoAlgorithm = cryptoAlgorithm;
		this.pubKey = pubKey;
		if(pubKey != null) {
			byte[] hash = SHA256.digest(pubKey.asBytes());
			if(!Arrays.equals(hash, pkHash))
				throw new SSKVerifyException("Invalid pubKey: wrong hash");
		}
		if(ehDocname.length != E_H_DOCNAME_SIZE)
			throw new IllegalArgumentException("ehDocname must be "+E_H_DOCNAME_SIZE+" bytes");
		if(pkHash.length != PUBKEY_HASH_SIZE)
			throw new IllegalArgumentException("pubKeyHash must be "+PUBKEY_HASH_SIZE+" bytes");
		hashCode = Fields.hashCode(pkHash) ^ Fields.hashCode(ehDocname);
	}
	
    private NodeSSK(NodeSSK key) {
    	super(key);
    	this.cryptoAlgorithm = key.cryptoAlgorithm;
    	this.pubKey = key.pubKey;
    	this.pubKeyHash = key.pubKeyHash.clone();
    	this.encryptedHashedDocname = key.encryptedHashedDocname.clone();
    	this.hashCode = key.hashCode;
    }
    
    @Override
	public Key cloneKey() {
    	return new NodeSSK(this);
    }

	// routingKey = H( E(H(docname)) + H(pubkey) )
	private static byte[] makeRoutingKey(byte[] pkHash, byte[] ehDocname) {
		MessageDigest md256 = SHA256.getMessageDigest();
		md256.update(ehDocname);
		md256.update(pkHash);
		byte[] key = md256.digest();
		SHA256.returnMessageDigest(md256);
		return key;
	}
	
	@Override
	public void write(DataOutput _index) throws IOException {
        _index.writeShort(getType());
        _index.write(encryptedHashedDocname);
        _index.write(pubKeyHash);
	}

    public static Key readSSK(DataInput raf, byte cryptoAlgorithm) throws IOException {
        byte[] buf = new byte[E_H_DOCNAME_SIZE];
        raf.readFully(buf);
        byte[] buf2 = new byte[PUBKEY_HASH_SIZE];
        raf.readFully(buf2);
        try {
			return new NodeSSK(buf2, buf, null, cryptoAlgorithm);
		} catch (SSKVerifyException e) {
			throw (AssertionError)new AssertionError("Impossible").initCause(e);
		}
    }

	@Override
	public short getType() {
		return (short) ((BASE_TYPE << 8) + (cryptoAlgorithm & 0xff));
	}

	@Override
	public void writeToDataOutputStream(DataOutputStream stream) throws IOException {
		write(stream);
	}

	/**
	 * @return True if we know the pubkey.
	 */
	public boolean hasPubKey() {
		return pubKey != null;
	}
	
	/**
	 * @return The public key, *if* we know it. Otherwise null.
	 */
	public DSAPublicKey getPubKey() {
		return pubKey;
	}

	public byte[] getPubKeyHash() {
		return pubKeyHash;
	}

	public void setPubKey(DSAPublicKey pubKey2) throws SSKVerifyException {
		if(pubKey == pubKey2) return;
		if(pubKey2 == null) return;
		if((pubKey == null) || !pubKey2.equals(pubKey)) {
			if(pubKey2 != null) {
				byte[] newPubKeyHash = SHA256.digest(pubKey2.asBytes());
				if(Arrays.equals(pubKeyHash, newPubKeyHash)) {
					if(pubKey != null) {
						// same hash, yet different keys!
						Logger.error(this, "Found SHA-256 collision or something... WTF?");
						throw new SSKVerifyException("Invalid new pubkey: "+pubKey2+" old pubkey: "+pubKey);
					} 
					// Valid key, assign.
				} else {
					throw new SSKVerifyException("New pubkey has invalid hash");
				}
			}
			pubKey = pubKey2;
		}
	}

	@Override
	public boolean equals(Object o) {
		if(o == this) return true;
		if(!(o instanceof NodeSSK)) return false;
		NodeSSK key = (NodeSSK)o;
		if(!Arrays.equals(key.encryptedHashedDocname, encryptedHashedDocname)) return false;
		if(!Arrays.equals(key.pubKeyHash, pubKeyHash)) return false;
		if(!Arrays.equals(key.routingKey, routingKey)) return false;
		// cachedNormalizedDouble and pubKey could be negative/null.
		return true;
	}
	
	@Override
	public int hashCode() {
		return hashCode;
	}
	
    // Not just the routing key, enough data to reconstruct the key (excluding any pubkey needed)
    @Override
	public byte[] getKeyBytes() {
    	return encryptedHashedDocname;
    }
    
    @Override
	public byte[] getFullKey() {
    	byte[] buf = new byte[FULL_KEY_LENGTH];
    	short type = getType();
    	buf[0] = (byte) (type >> 8);
    	buf[1] = (byte) (type & 0xFF);
    	System.arraycopy(encryptedHashedDocname, 0, buf, 2, E_H_DOCNAME_SIZE);
    	System.arraycopy(pubKeyHash, 0, buf, 2+E_H_DOCNAME_SIZE, PUBKEY_HASH_SIZE);
    	return buf;
    }

	public static NodeSSK construct(byte[] buf) throws SSKVerifyException {
		if(buf[0] != 2)
			throw new SSKVerifyException("Unknown type byte "+buf[0]);
		byte cryptoAlgorithm = buf[1];
		if(cryptoAlgorithm != Key.ALGO_AES_PCFB_256_SHA256)
			throw new SSKVerifyException("Unknown crypto algorithm "+buf[1]);
		byte[] encryptedHashedDocname = Arrays.copyOfRange(buf, 2, 2+E_H_DOCNAME_SIZE);
		byte[] pubkeyHash = Arrays.copyOfRange(buf, 2+E_H_DOCNAME_SIZE, 2+E_H_DOCNAME_SIZE+PUBKEY_HASH_SIZE);
		return new NodeSSK(pubkeyHash, encryptedHashedDocname, null, cryptoAlgorithm);
	}

	public boolean grabPubkey(GetPubkey pubkeyCache, boolean canReadClientCache, boolean forULPR, BlockMetadata meta) {
		if(pubKey != null) return false;
		pubKey = pubkeyCache.getKey(pubKeyHash, canReadClientCache, forULPR, meta);
		return pubKey != null;
	}

	public static byte[] routingKeyFromFullKey(byte[] keyBuf) {
		if(keyBuf.length != FULL_KEY_LENGTH) {
			Logger.error(NodeSSK.class, "routingKeyFromFullKey() on buffer length "+keyBuf.length);
		}
		byte[] encryptedHashedDocname = Arrays.copyOfRange(keyBuf, 2, 2+E_H_DOCNAME_SIZE);
		byte[] pubKeyHash = Arrays.copyOfRange(keyBuf, 2+E_H_DOCNAME_SIZE, 2+E_H_DOCNAME_SIZE+PUBKEY_HASH_SIZE);
		return makeRoutingKey(pubKeyHash, encryptedHashedDocname);
	}

	@Override
	public int compareTo(Key arg0) {
		if(arg0 instanceof NodeCHK) return -1;
		NodeSSK key = (NodeSSK) arg0;
		int result = Fields.compareBytes(encryptedHashedDocname, key.encryptedHashedDocname);
		if(result != 0) return result;
		return Fields.compareBytes(pubKeyHash, key.pubKeyHash);
	}
	
}

final class ArchiveNodeSSK extends NodeSSK {

	public ArchiveNodeSSK(byte[] pubKeyHash, byte[] encryptedHashedDocname, byte cryptoAlgorithm) {
		super(pubKeyHash, encryptedHashedDocname, cryptoAlgorithm);
	}
	
	@Override
	public void setPubKey(DSAPublicKey pubKey2) throws SSKVerifyException {
		throw new UnsupportedOperationException();
	}
	
	@Override
	public boolean grabPubkey(GetPubkey pubkeyCache, boolean canReadClientCache, boolean forULPR, BlockMetadata meta) {
		throw new UnsupportedOperationException();
	}
	
}

