/*
 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.Strings;

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.SecureRandom;

public class EddsaSignature 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 EddsaSignature() {
        this.r = new byte[32];
        this.s = new byte[32];
    }

    public EddsaSignature(Ed25519 r, BigInteger s) {
        this.r = r.encode();
        this.s = Ed25519.encodeScalar(s);
    }

    /**
     * Verify the signature on a message with given purpose.
     *
     * @param m the message signed by this signature
     * @param publicKey public key of the signer
     * @return true if the signature is valid, false otherwise
     */
    public boolean verifyRaw(byte[] m, EddsaPublicKey publicKey) {
        Ed25519 R = Ed25519.decode(r);
        if (!R.isOnCurve())
            return false;
        Ed25519 A = publicKey.asPoint();
        BigInteger S = Ed25519.decodeScalar(s);
        ByteBuffer Stemp = ByteBuffer.allocate(32 + 32 + m.length);
        Stemp.put(R.encode()).put(A.encode()).put(m);
        BigInteger h = Ed25519.Hint(Stemp.array());
        Ed25519 ra = Ed25519.B.scalarmult(S);
        Ed25519 rb = R.add(A.scalarmult(h));
        if (!A.isOnCurve()) {
            throw new AssertionError();
        }
        if (!R.isOnCurve()) {
            throw new AssertionError();
        }
        if (!ra.isOnCurve()) {
            throw new AssertionError();
        }
        if (!rb.isOnCurve()) {
            throw new AssertionError();
        }
        return ra.equals(rb);
    }

    public static EddsaSignature fromString(String value) {
        byte[] data = new byte[64];
        if (!Strings.stringToData(value, data)) {
            throw new AssertionError();
        }
        EddsaSignature sig = new EddsaSignature();
        System.arraycopy(data, 0, sig.r, 0, 32);
        System.arraycopy(data, 32, sig.s, 0, 32);
        return sig;
    }


    public boolean verify(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 verifyRaw(os.toByteArray(), publicKey);
    }

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

    @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);
    }
}
