package net.i2p.client.impl;

/*
 * free (adj.): unencumbered; not under the control of others
 * Written by jrandom in 2003 and released into the public domain 
 * with no warranty of any kind, either expressed or implied.  
 * It probably won't  make your computer catch on fire, or eat 
 * your children, but it might.  Use at your own risk.
 *
 */

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.GeneralSecurityException;
import java.util.Properties;

import net.i2p.I2PAppContext;
import net.i2p.I2PException;
import net.i2p.client.I2PClient;
import net.i2p.client.I2PSession;
import net.i2p.client.I2PSessionException;
import net.i2p.crypto.KeyGenerator;
import net.i2p.crypto.SigType;
import net.i2p.data.Certificate;
import net.i2p.data.Destination;
import net.i2p.data.KeyCertificate;
import net.i2p.data.PrivateKey;
import net.i2p.data.PublicKey;
import net.i2p.data.SigningPrivateKey;
import net.i2p.data.SigningPublicKey;
import net.i2p.data.SimpleDataStructure;
import net.i2p.util.RandomSource;

/**
 * Base client implementation.
 * An I2PClient contains no state, it is just a facility for creating private key files
 * and generating sesssions from existing private key files.
 *
 * @author jrandom
 */
public class I2PClientImpl implements I2PClient {

    private static final int PADDING_ENTROPY = 32;

    /**
     * Create a destination with a DSA 1024/160 signature type and a null certificate.
     * This is not bound to the I2PClient, you must supply the data back again
     * in createSession().
     *
     * Caller must close stream.
     *
     * @param destKeyStream location to write out the destination, PrivateKey, and SigningPrivateKey,
     *                      format is specified in {@link net.i2p.data.PrivateKeyFile PrivateKeyFile}
     */
    public Destination createDestination(OutputStream destKeyStream) throws I2PException, IOException {
        return createDestination(destKeyStream, DEFAULT_SIGTYPE);
    }

    /**
     * Create a destination with the given signature type.
     * It will have a null certificate for DSA 1024/160 and KeyCertificate otherwise.
     * This is not bound to the I2PClient, you must supply the data back again
     * in createSession().
     *
     * Caller must close stream.
     *
     * @param destKeyStream location to write out the destination, PrivateKey, and SigningPrivateKey,
     *                      format is specified in {@link net.i2p.data.PrivateKeyFile PrivateKeyFile}
     * @since 0.9.12
     */
    public Destination createDestination(OutputStream destKeyStream, SigType type) throws I2PException, IOException {
        Certificate cert;
        if (type == SigType.DSA_SHA1) {
            cert = Certificate.NULL_CERT;
        } else {
            cert = new KeyCertificate(type);
        }
        return createDestination(destKeyStream, cert);
    }

    /** 
     * Create the destination with the given payload and write it out along with
     * the PrivateKey and SigningPrivateKey to the destKeyStream
     *
     * If cert is a KeyCertificate, the signing keypair will be of the specified type.
     * The KeyCertificate data must be .............................
     * The padding if any will be randomized. The extra key data if any will be set in the
     * key cert.
     *
     * Caller must close stream.
     *
     * @param destKeyStream location to write out the destination, PrivateKey, and SigningPrivateKey,
     *                      format is specified in {@link net.i2p.data.PrivateKeyFile PrivateKeyFile}
     */
    public Destination createDestination(OutputStream destKeyStream, Certificate cert) throws I2PException, IOException {
        Destination d = new Destination();
        // Don't generate ElGamal keys anymore, they are unused since release 0.6 2005
        //Object keypair[] = KeyGenerator.getInstance().generatePKIKeypair();
        //PublicKey publicKey = (PublicKey) keypair[0];
        //PrivateKey privateKey = (PrivateKey) keypair[1];
        // repeating pattern to be used in pubkey and padding, so the
        // destination will be compressible
        byte[] rand = new byte[PADDING_ENTROPY];
        RandomSource.getInstance().nextBytes(rand);
        byte[] pk = new byte[PublicKey.KEYSIZE_BYTES];
        for (int i = 0; i < pk.length; i += PADDING_ENTROPY) {
            System.arraycopy(rand, 0, pk, i, Math.min(PADDING_ENTROPY, pk.length - i));
        }
        PublicKey publicKey = new PublicKey(pk);
        // Unused private key.
        // Could be all zeros, but make it random so SAM doesn't show a string of AAAA
        byte[] prk = new byte[PrivateKey.KEYSIZE_BYTES];
        RandomSource.getInstance().nextBytes(prk);
        PrivateKey privateKey = new PrivateKey(prk);

        SimpleDataStructure signingKeys[];
        if (cert.getCertificateType() == Certificate.CERTIFICATE_TYPE_KEY) {
            KeyCertificate kcert = cert.toKeyCertificate();
            SigType type = kcert.getSigType();
            try {
                signingKeys = KeyGenerator.getInstance().generateSigningKeys(type);
            } catch (GeneralSecurityException gse) {
                throw new I2PException("keygen fail", gse);
            }
        } else {
            signingKeys = KeyGenerator.getInstance().generateSigningKeys();
        }
        SigningPublicKey signingPubKey = (SigningPublicKey) signingKeys[0];
        SigningPrivateKey signingPrivKey = (SigningPrivateKey) signingKeys[1];
        d.setPublicKey(publicKey);
        d.setSigningPublicKey(signingPubKey);
        if (cert.getCertificateType() == Certificate.CERTIFICATE_TYPE_KEY) {
            // fix up key certificate or padding
            KeyCertificate kcert = cert.toKeyCertificate();
            SigType type = kcert.getSigType();
            int len = type.getPubkeyLen();
            if (len < 128) {
                int padLen = 128 - len;
                byte[] pad = new byte[padLen];
                // pad with the same pattern as in the public key
                for (int i = 0; i < padLen; i += PADDING_ENTROPY) {
                    System.arraycopy(rand, 0, pad, i, Math.min(PADDING_ENTROPY, padLen - i));
                }
                d.setPadding(pad);
            } else if (len > 128) {
                System.arraycopy(signingPubKey.getData(), 128, kcert.getPayload(), KeyCertificate.HEADER_LENGTH, len - 128);
            }
        }
        d.setCertificate(cert);

        d.writeBytes(destKeyStream);
        privateKey.writeBytes(destKeyStream);
        signingPrivKey.writeBytes(destKeyStream);
        destKeyStream.flush();

        return d;
    }

    /**
     * Create a new session (though do not connect it yet)
     *
     * @param destKeyStream location from which to read the Destination, PrivateKey, and SigningPrivateKey from,
     *                      format is specified in {@link net.i2p.data.PrivateKeyFile PrivateKeyFile}
     * @param options set of options to configure the router with, if null will use System properties
     */
    public I2PSession createSession(InputStream destKeyStream, Properties options) throws I2PSessionException {
        return createSession(I2PAppContext.getGlobalContext(), destKeyStream, options);
    }

    /**
     * Create a new session (though do not connect it yet)
     *
     * @param destKeyStream location from which to read the Destination, PrivateKey, and SigningPrivateKey from,
     *                      format is specified in {@link net.i2p.data.PrivateKeyFile PrivateKeyFile}
     * @param options set of options to configure the router with, if null will use System properties
     */
    public I2PSession createSession(I2PAppContext context, InputStream destKeyStream, Properties options) throws I2PSessionException {
        return new I2PSessionMuxedImpl(context, destKeyStream, options); // thread safe and muxed
    }
}
