package net.i2p.crypto.eddsa;

import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Arrays;

import net.i2p.crypto.eddsa.math.GroupElement;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec;
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec;

/**
 * An EdDSA private key.
 *<p>
 * Warning: Private key encoding is not fully specified in the
 * current IETF draft. This implementation uses PKCS#8 encoding,
 * and is subject to change. See getEncoded().
 *</p><p>
 * Ref: https://tools.ietf.org/html/draft-josefsson-pkix-eddsa-04
 *</p>
 *
 * @since 0.9.15
 * @author str4d
 *
 */
public class EdDSAPrivateKey implements EdDSAKey, PrivateKey {
    private static final long serialVersionUID = 23495873459878957L;
    private final byte[] seed;
    private final byte[] h;
    private final byte[] a;
    private final GroupElement A;
    private final byte[] Abyte;
    private final EdDSAParameterSpec edDsaSpec;

    public EdDSAPrivateKey(EdDSAPrivateKeySpec spec) {
        this.seed = spec.getSeed();
        this.h = spec.getH();
        this.a = spec.geta();
        this.A = spec.getA();
        this.Abyte = this.A.toByteArray();
        this.edDsaSpec = spec.getParams();
    }

    /**
     *  @since 0.9.25
     */
    public EdDSAPrivateKey(PKCS8EncodedKeySpec spec) throws InvalidKeySpecException {
        this(new EdDSAPrivateKeySpec(decode(spec.getEncoded()),
                                     EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.CURVE_ED25519_SHA512)));
    }

    public String getAlgorithm() {
        return "EdDSA";
    }

    public String getFormat() {
        return "PKCS#8";
    }

    /**
     *  This follows the docs from
     *  java.security.spec.PKCS8EncodedKeySpec
     *  quote:
     *<pre>
     *  The PrivateKeyInfo syntax is defined in the PKCS#8 standard as follows:
     *  PrivateKeyInfo ::= SEQUENCE {
     *    version Version,
     *    privateKeyAlgorithm PrivateKeyAlgorithmIdentifier,
     *    privateKey PrivateKey,
     *    attributes [0] IMPLICIT Attributes OPTIONAL }
     *  Version ::= INTEGER
     *  PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier
     *  PrivateKey ::= OCTET STRING
     *  Attributes ::= SET OF Attribute
     *</pre>
     *
     *<pre>
     *  AlgorithmIdentifier ::= SEQUENCE
     *  {
     *    algorithm           OBJECT IDENTIFIER,
     *    parameters          ANY OPTIONAL
     *  }
     *</pre>
     *
     *  Ref: https://tools.ietf.org/html/draft-josefsson-pkix-eddsa-04
     *
     *  Note that the private key encoding is not fully specified in the Josefsson draft version 04,
     *  and the example could be wrong, as it's lacking Version and AlgorithmIdentifier.
     *  This will hopefully be clarified in the next draft.
     *  But sun.security.pkcs.PKCS8Key expects them so we must include them for keytool to work.
     *
     *  This encodes the seed. It will return null if constructed from
     *  a spec which was directly constructed from H, in which case seed is null.
     *
     *  @return 49 bytes for Ed25519, null for other curves
     *  @since implemented in 0.9.25
     */
    public byte[] getEncoded() {
        if (!edDsaSpec.equals(EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.CURVE_ED25519_SHA512)))
            return null;
        int totlen = 17 + seed.length;
        byte[] rv = new byte[totlen];
        int idx = 0;
        // sequence
        rv[idx++] = 0x30;
        rv[idx++] = (byte) (15 + seed.length);

        // version
        // not in the Josefsson example
        rv[idx++] = 0x02;
        rv[idx++] = 1;
        rv[idx++] = 0;

        // Algorithm Identifier
        // sequence
        // not in the Josefsson example
        rv[idx++] = 0x30;
        rv[idx++] = 8;
        // OID 1.3.101.100
        // https://msdn.microsoft.com/en-us/library/windows/desktop/bb540809%28v=vs.85%29.aspx
        // not in the Josefsson example
        rv[idx++] = 0x06;
        rv[idx++] = 3;
        rv[idx++] = (1 * 40) + 3;
        rv[idx++] = 101;
        rv[idx++] = 100;
        // params
        rv[idx++] = 0x0a;
        rv[idx++] = 1;
        rv[idx++] = 1; // Ed25519
        // the key
        rv[idx++] = 0x04;  // octet string
        rv[idx++] = (byte) seed.length;
        System.arraycopy(seed, 0, rv, idx, seed.length);
        return rv;
    }

    /**
     *  This is really dumb for now.
     *  See getEncoded().
     *
     *  @return 32 bytes for Ed25519, throws for other curves
     *  @since 0.9.25
     */
    private static byte[] decode(byte[] d) throws InvalidKeySpecException {
        try {
            int idx = 0;
            if (d[idx++] != 0x30 ||
                d[idx++] != 47 ||
                d[idx++] != 0x02 ||
                d[idx++] != 1 ||
                d[idx++] != 0 ||
                d[idx++] != 0x30 ||
                d[idx++] != 8 ||
                d[idx++] != 0x06 ||
                d[idx++] != 3 ||
                d[idx++] != (1 * 40) + 3 ||
                d[idx++] != 101 ||
                d[idx++] != 100 ||
                d[idx++] != 0x0a ||
                d[idx++] != 1 ||
                d[idx++] != 1 ||
                d[idx++] != 0x04 ||
                d[idx++] != 32) {
                throw new InvalidKeySpecException("unsupported key spec");
            }
            byte[] rv = new byte[32];
            System.arraycopy(d, idx, rv, 0, 32);
            return rv;
        } catch (IndexOutOfBoundsException ioobe) {
            throw new InvalidKeySpecException(ioobe);
        }
    }

    public EdDSAParameterSpec getParams() {
        return edDsaSpec;
    }

    /**
     *  @return will be null if constructed from a spec which was
     *          directly constructed from H
     */
    public byte[] getSeed() {
        return seed;
    }

    /**
     *  @return the hash of the seed
     */
    public byte[] getH() {
        return h;
    }

    /**
     *  @return the private key
     */
    public byte[] geta() {
        return a;
    }

    /**
     *  @return the public key
     */
    public GroupElement getA() {
        return A;
    }

    /**
     *  @return the public key
     */
    public byte[] getAbyte() {
        return Abyte;
    }

    /**
     *  @since 0.9.25
     */
    @Override
    public int hashCode() {
        return Arrays.hashCode(seed);
    }

    /**
     *  @since 0.9.25
     */
    @Override
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof EdDSAPrivateKey))
            return false;
        EdDSAPrivateKey pk = (EdDSAPrivateKey) o;
        return Arrays.equals(seed, pk.getSeed()) &&
               edDsaSpec.equals(pk.getParams());
    }
}
