/**********************************************************************
 *
 * This file is part of HBCI4Java.
 * Copyright (c) 2001-2010 Stefan Palme
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 **********************************************************************/

package org.kapott.cryptalgs;

import java.math.BigInteger;
import java.security.InvalidAlgorithmParameterException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.SignatureSpi;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays;
import java.util.logging.Logger;

public class ISO9796p1
    extends SignatureSpi
{
    private RSAPublicKey       pubKey;
    private PrivateKey         privKey;
    private MessageDigest      dig;
    private SignatureParamSpec param;

    protected Logger getLogger()
    {
        return Logger.getLogger(this.getClass().getName());
    }

    @Override @Deprecated
    protected Object engineGetParameter(String parameter)
    {
        return null;
    }

    @Override
    protected void engineInitSign(PrivateKey privateKey)
    {
        try {
            this.dig=MessageDigest.getInstance(this.param.getHashAlg(), this.param.getProvider());
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        } catch (NoSuchProviderException e) {
            throw new RuntimeException(e);
        }
        this.privKey=privateKey;
    }

    @Override
    protected void engineInitVerify(PublicKey publicKey)
    {
        try {
            this.dig=MessageDigest.getInstance(this.param.getHashAlg(), this.param.getProvider());
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        } catch (NoSuchProviderException e) {
            throw new RuntimeException(e);
        }
        this.pubKey=(RSAPublicKey)publicKey;
    }

    @Override @Deprecated
    protected void engineSetParameter(String param1, Object value)
    {
        // do nothing
    }

    @Override
    protected void engineSetParameter(AlgorithmParameterSpec param1)
        throws InvalidAlgorithmParameterException
    {
        if (param1 instanceof SignatureParamSpec)
            this.param=(SignatureParamSpec)(param1);
        else {
            throw new InvalidAlgorithmParameterException();
        }
    }

    @Override
    protected byte[] engineSign()
        throws SignatureException
    {
        BigInteger bModulus;
        if (this.privKey instanceof RSAPrivateKey) {
            bModulus=((RSAPrivateKey)this.privKey).getModulus();
        } else {
            RSAPrivateCrtKey2 key2=(RSAPrivateCrtKey2)this.privKey;
            bModulus=key2.getP().multiply(key2.getQ());
        }
        byte[] modulus=bModulus.toByteArray();
        
        byte[] buffer=this.dig.digest();
        byte[] rr=prepareForSig(buffer,bModulus);
        
        byte[] is;
        if (this.privKey instanceof RSAPrivateKey) {
            getLogger().fine("signing with (n,d)-algorithm");
            BigInteger bPrivExponent=((RSAPrivateKey)this.privKey).getPrivateExponent();
            BigInteger bIS=(new BigInteger(+1,rr)).modPow(bPrivExponent,bModulus);
            is=bIS.toByteArray();
        } else {
            getLogger().fine("signing with (p,q,dP,dQ,qInv)-algorithm");
            RSAPrivateCrtKey2 key2=(RSAPrivateCrtKey2)this.privKey;
            BigInteger        p=key2.getP();
            BigInteger        q=key2.getQ();
            BigInteger        dP=key2.getdP();
            BigInteger        dQ=key2.getdQ();
            BigInteger        qInv=key2.getQInv();
    
            BigInteger encData=new BigInteger(+1,rr);
            BigInteger m1=encData.modPow(dP,p);
            BigInteger m2=encData.modPow(dQ,q);
            BigInteger h=m1.subtract(m2).multiply(qInv).mod(p); 
            is=m2.add(q.multiply(h)).toByteArray();
        }

        // adjust value
        byte[] sig=getSigFromIS(is,modulus);

        return sig;
    }
    
    public static byte[] prepareForSig(byte[] buffer,BigInteger bModulus)
        throws SignatureException
    {
        /* padding; 'cause my buffer is already byte-aligned, there
           are no padding bits to be prepended */
        byte[] mp=new byte[buffer.length];
        System.arraycopy(buffer,0,mp,0,buffer.length);

        int k=bModulus.bitLength()-1;
        int z=mp.length;
        int r=1;

        if (!((z<<4)<=(k+3))) {
            throw new SignatureException("16*z is greater than k");
        }

        // extension (concatenate MP multiple times)
        /*double t2=((double)(k))/8;
        if ((k&0x07) != 0)
            t2 += 1.0;
        int t = ((int)(t2)) >> 1;*/
        int t=(k-1)>>4;
        if (((k-1)&0x0F)!=0)
            t++;
        byte[] me=getMEfromMP(mp, t);

        /* creating redundancy by interleaving the extended message bytes
           with redundancy bytes */
        byte[] mr=getMRfromME(me,t,z,r);

        // get intermediate integer
        byte[] ir=getIRfromMR(mr,k);

        // calculate signature
        byte[] rr=ir; // getRRfromIR(ir, k, modulus, privExponent);
        
        return rr;
    }

    @Override
    protected int engineSign(byte[] output,int offset,int len)
        throws SignatureException
    {
        byte[] sig=engineSign();

        if (offset+len>output.length)
            throw new SignatureException("output result too large for buffer");
        System.arraycopy(sig,0,output,offset,sig.length);
        return sig.length;
    }

    @Override
    protected void engineUpdate(byte b)
    {
        this.dig.update(b);
    }

    @Override
    protected void engineUpdate(byte[] b,int offset,int length)
    {
        for (int i=0;i<length;i++) {
            engineUpdate(b[offset+i]);
        }
    }

    @Override
    protected boolean engineVerify(byte[] sig)
        throws SignatureException
    {
        BigInteger bExponent = this.pubKey.getPublicExponent();
        byte[] exponent = bExponent.toByteArray();
        BigInteger bModulus = this.pubKey.getModulus();
        byte[] modulus = bModulus.toByteArray();

        byte[] is=getISfromSig(sig,exponent,modulus);

        int[] ks=new int[1];
        byte[] ir=getIRfromIS(is,exponent,modulus,ks);
        int k=ks[0];

        int[] ts=new int[1];
        byte[] mr=getMRfromIR(ir,k,ts);
        int t=ts[0];

        int[] zs=new int[1];
        int[] rs=new int[1];
        byte[] mp=getMPfromMR(mr,t,zs,rs);
        int z=zs[0];
        int r=rs[0];
        
        int datalen=(z<<3)+1-r;
        int databytes=(datalen>>3);
        if ((datalen&0x07)!=0) {
            databytes++;
        }
        byte[] recHash=new byte[databytes];
        System.arraycopy(mp,mp.length-databytes,recHash,0,databytes);
        if ((datalen&0x07)!=0) {
            recHash[0]&=(1<<(datalen&0x07))-1;
        }
        BigInteger hash=new BigInteger(+1, recHash);
        BigInteger hash2=new BigInteger(+1, this.dig.digest());
        
        byte[] me2=getMEfromMP(mp,t);
        byte[] mr2=getMRfromME(me2,t,z,r);

        mr[0] &=(1<<(7-((mr.length <<3)-k)))-1;
        mr2[0]&=(1<<(7-((mr2.length<<3)-k)))-1;

        return hash.equals(hash2) && Arrays.equals(mr,mr2);
    }

    // ---------------------------------------------------------------------------------

    private static byte[] getISfromSig(byte[] sig,byte[] exp,byte[] mod)
    {
        return (new BigInteger(+1,sig)).modPow(new BigInteger(+1,exp),new BigInteger(+1,mod)).toByteArray();
    }

    private static byte[] getIRfromIS(byte[] is,byte[] exp,byte[] mod,int[] ks)
        throws SignatureException
    {
        BigInteger is_b=new BigInteger(+1,is);
        BigInteger mod_b=new BigInteger(+1,mod);
        BigInteger exp_b=new BigInteger(+1,exp);

        BigInteger ret=null;

        if (is_b.mod(new BigInteger("16")).equals(new BigInteger("6")))
            ret=is_b;
        else if (mod_b.subtract(is_b).mod(new BigInteger("16")).equals(new BigInteger("6")))
            ret=mod_b.subtract(is_b);

        if (ret==null && exp_b.mod(new BigInteger("2")).compareTo(new BigInteger("0"))==0)
            if (is_b.mod(new BigInteger("8")).equals(new BigInteger("3")))
                ret=is_b.multiply(new BigInteger("2"));
            else if (mod_b.subtract(is_b).mod(new BigInteger("8")).equals(new BigInteger("3")))
                ret=mod_b.subtract(is_b).multiply(new BigInteger("2"));

        if (ret==null) {
            throw new SignatureException("can not convert IS to IR");
        }

        byte[] reta=ret.toByteArray();
        int k=reta.length<<3;

        for (int idx=0,pos=0;;) {
            if ((reta[idx]&(1<<(7-pos)))!=0)
                break;

            k--;

            if (++pos==8) {
                pos=0;
                idx++;
            }
        }
        ks[0]=k;

        if ((reta[reta.length-1]&0x0F)!=0x06)
            throw new SignatureException("last nibble is not 0x06");

        return reta;
    }

    private static byte Perm(int b)
    {
        return (new byte[]{
                0x0E, 0x03, 0x05, 0x08,
                0x09, 0x04, 0x02, 0x0F,
                0x00, 0x0D, 0x0B, 0x06,
                0x07, 0x0A, 0x0C, 0x01})[b];
    }

    private static byte Perm1(int b)
    {
        return (new byte[]{
                0x08, 0x0F, 0x06, 0x01,
                0x05, 0x02, 0x0B, 0x0C,
                0x03, 0x04, 0x0D, 0x0A,
                0x0E, 0x09, 0x00, 0x07})[b];
    }

    private static byte[] getMRfromIR(byte[] ir,int k,int[] ts)
    {
        /*
        double t2 = ((double)(k)) / 8;
        if ((k & 0x07) != 0)
            t2 += 1.0;
        int t = ((int)(t2)) >> 1;*/
        
        int t=(k-1)>>4;
        if (((k-1)&0x0F)!=0)
            t++;
        ts[0]=t;

        byte[] mr=new byte[2*t];

        int bitsum=0;
        for (int i=0;i<2*t;i++) {
            mr[2*t-1-i]=ir[2*t-1-i];
            bitsum+=8;

            if (bitsum>=k)
                mr[2*t-1-i]&=(1<<(7-(bitsum-k)))-1;
        }

        mr[2*t-1]=(byte)((Perm1((mr[2*t-2]>>4)&0x0F)<<4) | ((mr[2*t-1]>>4)&0x0F));

        return mr;
    }

    private static byte S(int x)
    {
        return (byte)((Perm((x>>4)&0x0F)<<4) | Perm(x&0x0F));
    }

    private static byte[] getMPfromMR(byte[] mr,int t,int[] zs,int[] rs)
        throws SignatureException
    {
        int i;
        for (i=0;i<t;i++) {
            byte sum=(byte)(S(mr[2*t-1 - (2*i)]) ^ mr[2*t-1 - (2*i+1)]);

            if (sum!=0) {
                zs[0]=i+1;
                rs[0]=sum&0x0F;
                break;
            }
        }

        if (i==t) {
            throw new SignatureException("all sums are 0");
        }

        if (rs[0]<1 || rs[0]>8)
            throw new SignatureException("r is not in range 1..8");

        int z=zs[0];
        byte[] mp=new byte[z];
        for (i=0;i<z;i++) {
            mp[z-1-i]=mr[2*t-1-(2*i)];
        }

        return mp;
    }

    private static byte[] getMEfromMP(byte[] mp,int t)
    {
        byte[] ret=new byte[t];
        int sum=0;

        while (sum<t) {
            if (sum+mp.length<=t) {
                System.arraycopy(mp,0,ret,ret.length-sum-mp.length,mp.length);
                sum+=mp.length;
            } else {
                int diff=t-sum;
                System.arraycopy(mp,mp.length-diff,ret,0,diff);
                sum+=diff;
            }
        }

        return ret;
    }

    private static byte[] getMRfromME(byte[] me,int t,int z,int r)
    {
        byte[] mr=new byte[2*t];

        for (int i=0;i<t;i++) {
            mr[2*t-1 - (2*i)]  =me[t-1-i];
            mr[2*t-1 - (2*i+1)]=S(me[t-1-i]);
        }

        mr[(t-z)<<1]^=r;

        return mr;
    }

    private static byte[] getIRfromMR(byte[] mr,int k)
    {
        int    len=mr.length;
        byte[] ir=new byte[len];
        System.arraycopy(mr,0,ir,0,len);

        ir[0]&=(1<<(7-((len<<3)-k)))-1;
        ir[0]|=(1<<(7-((len<<3)-k)));
        ir[len-1]=(byte)(((ir[len-1]&0x0F)<<4) | 0x06);
        return ir;
    }

    /*
    private int jacobi(BigInteger a,BigInteger n)
    {
        int j=1;

        while (a.compareTo(new BigInteger("0"))!=0) {
            while (a.mod(new BigInteger("2")).compareTo(new BigInteger("0"))==0) {
                a=a.divide(new BigInteger("2"));
                
                BigInteger mod8=n.mod(new BigInteger("8"));
                if (mod8.compareTo(new BigInteger("3"))==0 ||
                    mod8.compareTo(new BigInteger("5"))==0) {
                    j=-j;
                }
            }

            BigInteger temp=a;
            a=n;
            n=temp;

            if (a.mod(new BigInteger("4")).compareTo(new BigInteger("3"))==0 &&
                n.mod(new BigInteger("4")).compareTo(new BigInteger("3"))==0) {
                j=-j;
            }

            a=a.mod(n);
        }

        int ret;
        
        if (n.compareTo(new BigInteger("1"))==0)
            ret=j;
        else
            ret=0;
        
        return ret;
    }*/

    /*private byte[] getRRfromIR(byte[] ir, int k, byte[] modulus, byte[] privExponent)
    {
        byte[] ir2 = new byte[ir.length];
        System.arraycopy(ir, 0, ir2, 0, ir.length);
        ir2[0] &= ((1 << (8 - ((ir.length << 3) - k))) - 1);

        BigInteger bIR = new BigInteger(+1, ir2);
        BigInteger bModulus = new BigInteger(+1, modulus);
        BigInteger bRR = null;

        bRR = bIR;

        return bRR.toByteArray();
    }*/

    private static byte[] getSigFromIS(byte[] is,byte[] modulus)
    {
        BigInteger bIS=new BigInteger(+1,is);
        BigInteger bModulus=new BigInteger(+1,modulus);
        BigInteger bIS2=bModulus.subtract(bIS);
        BigInteger bSig=null;

        if (bIS.compareTo(bIS2)<0)
            bSig=bIS;
        else
            bSig=bIS2;

        return bSig.toByteArray();
    }
}
