/*
 This file is part of GNUnet.
  (C) 2012, 2013 Christian Grothoff (and other contributing authors)

  GNUnet is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published
  by the Free Software Foundation; either version 3, or (at your
  option) any later version.

  GNUnet is distributed in the hope that it will be useful, but
  WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with GNUnet; see the file COPYING.  If not, write to the
  Free Software Foundation, Inc., 59 Temple Place - Suite 330,
  Boston, MA 02111-1307, USA.
 */
package org.gnunet.util.crypto;

import org.gnunet.construct.FixedSizeIntegerArray;
import org.gnunet.construct.Message;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOError;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

public class EddsaPrivateKey implements Message {
    @FixedSizeIntegerArray(bitSize = 8, signed = false, length = 32)
    public byte[] d;

    public EddsaSignature signRaw(byte[] m) {
        return signRaw(getPublicKey(), m);
    }

    /**
     * Sign the given data with this private key.  Must include a purpose to mitigate
     * replay / copy and paste attacks.
     *
     * @param publicKey public key corresponding to this private key, supplying this parameter
     *                  leads to better performance as the public key does not have to be derived
     * @param m data to signRaw
     * @return the signature over both the data and the purpose
     */
    public EddsaSignature signRaw(EddsaPublicKey publicKey, byte[] m) {
        if (!publicKey.asPoint().isOnCurve()) {
            throw new AssertionError();
        }
        MessageDigest sha512;
        try {
            sha512 = MessageDigest.getInstance("SHA-512");
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("crypto algorithm required but not provided");
        }
        byte[] h = sha512.digest(d);
        BigInteger a = BigInteger.valueOf(2).pow(Ed25519.b-2);
        for (int i = 3; i < (Ed25519.b - 2); i++) {
            a = a.add(BigInteger.valueOf(2).pow(i).multiply(BigInteger.valueOf(bit(h,i))));
        }
        ByteBuffer rsub = ByteBuffer.allocate((Ed25519.b/8)+m.length);
        rsub.put(h, Ed25519.b/8, Ed25519.b/4-Ed25519.b/8).put(m);
        BigInteger r = Ed25519.Hint(rsub.array());
        Ed25519 R = Ed25519.B.scalarmult(r);

        byte[] encodedPublicKey = publicKey.asPoint().encode();
        ByteBuffer buf = ByteBuffer.allocate(32 + encodedPublicKey.length + m.length);
        buf.put(R.encode()).put(encodedPublicKey).put(m);

        BigInteger S = r.add(Ed25519.Hint(buf.array()).multiply(a)).mod(Ed25519.l);

        if (!R.isOnCurve()) {
            throw new AssertionError();
        }
        if (!publicKey.asPoint().isOnCurve()) {
            throw new AssertionError();
        }
        return new EddsaSignature(R, S);
    }

    /**
     * Get return the i-th bit in the given array of bytes h.
     *
     * @param h array of bytes
     * @param i bit index
     * @return i-th bit in h
     */
    private static int bit(byte[] h, int i) {
        return h[i/8] >> (i%8) & 1;
    }


    /**
     * Compute the coefficient that is used to derive the public key.
     * See 'Daniel J. Bernstein et al, High-speed high-security signatures' for details.
     *
     * @return the public key coefficient
     */
    private BigInteger computePublicKeyCoefficient() {
        MessageDigest sha512;
        try {
            sha512 = MessageDigest.getInstance("SHA-512");
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("crypto algorithm required but not provided");
        }
        byte[] h = sha512.digest(d);
        BigInteger a = BigInteger.valueOf(2).pow(Ed25519.b - 2);
        for (int i=3; i < (Ed25519.b - 2); i++) {
            BigInteger apart = BigInteger.valueOf(2).pow(i).multiply(BigInteger.valueOf(bit(h,i)));
            a = a.add(apart);
        }
        return a;
    }

    public EddsaSignature sign(byte[] data, int purpose) {
        return sign(data, purpose, getPublicKey());
    }

    public EddsaSignature sign(byte[] data, int purpose, EddsaPublicKey publicKey) {
        ByteArrayOutputStream os = new ByteArrayOutputStream(data.length + 8);
        DataOutputStream dos = new DataOutputStream(os);
        try {
            dos.writeInt(data.length);
            dos.writeInt(purpose);
            dos.write(data);
        } catch (IOException e) {
            throw new IOError(e);
        }
        return signRaw(publicKey, os.toByteArray());
    }


    /**
     * Get the public key for this private key.
     *
     * @return the public key for this private key
     */
    public EddsaPublicKey getPublicKey() {
        BigInteger a = computePublicKeyCoefficient();
        Ed25519 A = Ed25519.B.scalarmult(a);
        return new EddsaPublicKey(A);
    }

    /**
     * Create a random private key.
     *
     * @return a random private key
     */
    public static EddsaPrivateKey createRandom() {
        SecureRandom sr = new SecureRandom();
        EddsaPrivateKey privateKey = new EddsaPrivateKey();
        privateKey.d = new byte[32];
        sr.nextBytes(privateKey.d);
        return privateKey;
    }
}
