/* 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.net.MalformedURLException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;

import freenet.crypt.DSAPublicKey;
import freenet.crypt.SHA256;
import freenet.crypt.UnsupportedCipherException;
import freenet.crypt.ciphers.Rijndael;
import freenet.support.Fields;
import freenet.support.HexUtil;
import freenet.support.Logger;

/** Client-level SSK, i.e. a low level SSK with the decryption key needed to
 * decrypt the data once it is fetched. Note that you can only use this to
 * *REQUEST* keys, not to *INSERT* them, because it only has the public key
 * and not the private key, @see InsertableClientSSK.
 */
public class ClientSSK extends ClientKey {

    private static final long serialVersionUID = 1L;
    /** Crypto type */
	public final byte cryptoAlgorithm;
	/** Document name */
	public final String docName;
	/** Public key */
	protected transient DSAPublicKey pubKey;
	/** Public key hash */
	public final byte[] pubKeyHash;
	/** Encryption key */
	public final byte[] cryptoKey;
	/** Encrypted hashed docname */
	public final byte[] ehDocname;
	private final int hashCode;
	
	public static final int CRYPTO_KEY_LENGTH = 32;
	public static final int EXTRA_LENGTH = 5;
	
	private ClientSSK(ClientSSK key) {
		this.cryptoAlgorithm = key.cryptoAlgorithm;
		this.docName = key.docName;
		if(key.pubKey != null)
			this.pubKey = key.pubKey.cloneKey();
		else
			this.pubKey = null;
		pubKeyHash = key.pubKeyHash.clone();
		cryptoKey = key.cryptoKey.clone();
		ehDocname = key.ehDocname.clone();
		hashCode = Fields.hashCode(pubKeyHash) ^ Fields.hashCode(cryptoKey) ^ Fields.hashCode(ehDocname) ^ docName.hashCode();
	}
	
	public ClientSSK(String docName, byte[] pubKeyHash, byte[] extras, DSAPublicKey pubKey, byte[] cryptoKey) throws MalformedURLException {
		this.docName = docName;
		this.pubKey = pubKey;
		this.pubKeyHash = pubKeyHash;
		if(docName == null)
			throw new MalformedURLException("No document name.");
		if(extras == null)
			throw new MalformedURLException("No extra bytes in SSK - maybe a 0.5 key?");
		if(extras.length < 5)
			throw new MalformedURLException("Extra bytes too short: "+extras.length+" bytes");
		this.cryptoAlgorithm = extras[2];
		if(cryptoAlgorithm != Key.ALGO_AES_PCFB_256_SHA256)
			throw new MalformedURLException("Unknown encryption algorithm "+cryptoAlgorithm);
		if(!Arrays.equals(extras, getExtraBytes()))
			throw new MalformedURLException("Wrong extra bytes");
		if(pubKeyHash.length != NodeSSK.PUBKEY_HASH_SIZE)
			throw new MalformedURLException("Pubkey hash wrong length: "+pubKeyHash.length+" should be "+NodeSSK.PUBKEY_HASH_SIZE);
		if(cryptoKey.length != CRYPTO_KEY_LENGTH)
			throw new MalformedURLException("Decryption key wrong length: "+cryptoKey.length+" should be "+CRYPTO_KEY_LENGTH);
		MessageDigest md = SHA256.getMessageDigest();
		if (pubKey != null) {
			byte[] pubKeyAsBytes = pubKey.asBytes();
			md.update(pubKeyAsBytes);
			byte[] otherPubKeyHash = md.digest();
			if (!Arrays.equals(otherPubKeyHash, pubKeyHash))
				throw new IllegalArgumentException();
		}
		this.cryptoKey = cryptoKey;
		md.update(docName.getBytes(StandardCharsets.UTF_8));
		byte[] buf = md.digest();
		try {
			Rijndael aes = new Rijndael(256, 256);
			aes.initialize(cryptoKey);
			aes.encipher(buf, buf);
			ehDocname = buf;
		} catch (UnsupportedCipherException e) {
			throw new Error(e);
		}
		if(ehDocname == null)
			throw new NullPointerException();
		hashCode = Fields.hashCode(pubKeyHash) ^ Fields.hashCode(cryptoKey) ^ Fields.hashCode(ehDocname) ^ docName.hashCode();
	}
	
	public ClientSSK(FreenetURI origURI) throws MalformedURLException {
		this(origURI.getDocName(), origURI.getRoutingKey(), origURI.getExtra(), null, origURI.getCryptoKey());
		if(!origURI.getKeyType().equalsIgnoreCase("SSK"))
			throw new MalformedURLException();
	}
	
	protected ClientSSK() {
	    // For serialization.
	    this.cryptoAlgorithm = 0;
	    this.docName = null;
	    this.pubKeyHash = null;
	    this.cryptoKey = null;
	    this.ehDocname = null;
	    this.hashCode = 0;
	}
	
	public synchronized void setPublicKey(DSAPublicKey pubKey) {
		if((this.pubKey != null) && (this.pubKey != pubKey) && !this.pubKey.equals(pubKey))
			throw new IllegalArgumentException("Cannot reassign: was "+this.pubKey+" now "+pubKey);
		byte[] newKeyHash = pubKey.asBytesHash();
		if(!Arrays.equals(newKeyHash, pubKeyHash))
			throw new IllegalArgumentException("New pubKey hash does not match pubKeyHash: "+HexUtil.bytesToHex(newKeyHash)+" ( "+HexUtil.bytesToHex(pubKey.asBytesHash())+" != "+HexUtil.bytesToHex(pubKeyHash)+" for "+pubKey);
		this.pubKey = pubKey;
		this.cachedNodeKey = null;
	}
	
	@Override
	public FreenetURI getURI() {
		return new FreenetURI("SSK", docName, pubKeyHash, cryptoKey, getExtraBytes());
	}

	protected final byte[] getExtraBytes() {
		return getExtraBytes(cryptoAlgorithm);
	}
	
	protected static byte[] getExtraBytes(byte cryptoAlgorithm) {
		// 5 bytes.
		byte[] extra = new byte[5];

		extra[0] = NodeSSK.SSK_VERSION;
		extra[1] = 0; // 0 = fetch (public) URI; 1 = insert (private) URI
		extra[2] = cryptoAlgorithm;
		extra[3] = (byte) (KeyBlock.HASH_SHA256 >> 8);
		extra[4] = (byte) KeyBlock.HASH_SHA256;
		return extra;
	}

	/**
	 * @return the provided argument
	 * @deprecated mutable data cannot safely be interned
	 */
	public static byte[] internExtra(byte[] buf) {
		return buf;
	}

	private transient Key cachedNodeKey;
	
	@Override
	public Key getNodeKey(boolean cloneKey) {
		try {
			Key nodeKey;
			synchronized(this) {
				if(ehDocname == null)
					throw new NullPointerException();
				if(pubKeyHash == null)
					throw new NullPointerException();
				if (cachedNodeKey == null || cachedNodeKey.getKeyBytes() == null || cachedNodeKey.getRoutingKey() == null)
					cachedNodeKey = new NodeSSK(pubKeyHash, ehDocname, pubKey, cryptoAlgorithm);
				nodeKey = cachedNodeKey;
			}
			return cloneKey ? nodeKey.cloneKey() : nodeKey;
		} catch (SSKVerifyException e) {
			Logger.error(this, "Have already verified and yet it fails!: "+e);
			throw (AssertionError)new AssertionError("Have already verified and yet it fails!").initCause(e);
		}
	}

	public DSAPublicKey getPubKey() {
		return pubKey;
	}

	@Override
	public String toString() {
		return "ClientSSK:"+getURI().toString();
	}

	@Override
	public ClientKey cloneKey() {
		return new ClientSSK(this);
	}

	@Override
	public int hashCode() {
		return hashCode;
	}
	
	@Override
	public boolean equals(Object o) {
		if(!(o instanceof ClientSSK)) return false;
		ClientSSK key = (ClientSSK) o;
		if(cryptoAlgorithm != key.cryptoAlgorithm) return false;
		if(!docName.equals(key.docName)) return false;
		if(!Arrays.equals(pubKeyHash, key.pubKeyHash)) return false;
		if(!Arrays.equals(cryptoKey, key.cryptoKey)) return false;
		if(!Arrays.equals(ehDocname, key.ehDocname)) return false;
		return true;
	}
}
