/*
 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 org.gnunet.util.HashCode;
import org.gnunet.util.Strings;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOError;
import java.io.IOException;
import java.math.BigInteger;
import java.security.SecureRandom;

/**
 * ECDSA Signature.
 */
public class EcdsaSignature implements Message {
    /**
     * R value of the signature in compressed form.
     * The number is stored as little endian.
     */
    @FixedSizeIntegerArray(bitSize = 8, signed = false, length = 32)
    public byte[] r;

    /**
     * S-value of the signature.
     * The number is stored as little endian.
     */
    @FixedSizeIntegerArray(bitSize = 8, signed = false, length = 32)
    public byte[] s;

    public EcdsaSignature() {
        this.r = new byte[32];
        this.s = new byte[32];
    }

    /**
     * Verify that this signature has been created by the given public key and signs the
     * given data and purpose.
     *
     * @param m message that was signed
     * @param publicKey public key to check for
     * @return whether the signature is valid
     */
    public boolean verifyRaw(byte[] m, EcdsaPublicKey publicKey) {
        if (publicKey.asPoint().isIdentity()) {
            throw new AssertionError();
        }

        if (!publicKey.asPoint().isOnCurve()) {
            throw new AssertionError();
        }

        if (!publicKey.asPoint().scalarmult(Ed25519.l).isIdentity()) {
            throw new AssertionError("invalid public key");
        }

        HashCode h = HashCode.hash(m);
        BigInteger z = new BigInteger(1, h.data);
        BigInteger sCoeff = Ed25519.decodeScalar(s);

        if (sCoeff.equals(BigInteger.ZERO) || sCoeff.compareTo(Ed25519.l) >= 0) {
            return false;
        }

        BigInteger rCoeff = Ed25519.decodeScalar(r);
        if (rCoeff.equals(BigInteger.ZERO) || rCoeff.compareTo(Ed25519.l) >= 0) {
            return false;
        }

        BigInteger w = sCoeff.modInverse(Ed25519.l);
        BigInteger u1 = z.multiply(w).mod(Ed25519.l);
        BigInteger u2 = rCoeff.multiply(w).mod(Ed25519.l);
        // P = u1*B + u2*Q
        Ed25519 P = Ed25519.B.scalarmult(u1).add(publicKey.asPoint().scalarmult(u2));
        return P.P0.mod(Ed25519.l).equals(rCoeff);
    }

    public boolean verify(byte[] data, int purpose, EcdsaPublicKey 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 verifyRaw(os.toByteArray(), publicKey);
    }

    /**
     * Load a signature from a string.
     *
     * @param value serialized signature
     * @return signature
     */
    public static EcdsaSignature fromString(String value) {
        byte[] data = new byte[64];
        if (! Strings.stringToData(value, data)) {
            throw new AssertionError();
        }
        EcdsaSignature sig = new EcdsaSignature();
        System.arraycopy(data, 0, sig.r, 0, 32);
        System.arraycopy(data, 32, sig.s, 0, 32);
        return sig;
    }


    /**
     * Serialize the signature to a string.
     *
     * @return serialized signature
     */
    @Override
    public String toString() {
        byte[] sigData = new byte[64];
        System.arraycopy(r, 0, sigData, 0, 32);
        System.arraycopy(s, 0, sigData, 32, 32);
        return Strings.dataToString(sigData);
    }


    /**
     * Return a signature that is invalid with very, very high probability.
     *
     * @return signature with random garbage
     */
    public static EcdsaSignature randomGarbage() {
        EcdsaSignature sig = new EcdsaSignature();
        SecureRandom r = new SecureRandom();
        r.nextBytes(sig.r);
        r.nextBytes(sig.s);
        return sig;
    }
}
