/**********************************************************************
 *
 * This file is part of HBCI4Java.
 * Copyright (c) 2001-2008 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.hbci.passport;

import java.math.BigInteger;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Arrays;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;
import javax.crypto.spec.IvParameterSpec;

import org.kapott.cryptalgs.RSAPrivateCrtKey2;
import org.kapott.hbci.exceptions.HBCI_Exception;
import org.kapott.hbci.manager.HBCIKey;
import org.kapott.hbci.manager.HBCIUtils;
import org.kapott.hbci.manager.HBCIUtilsInternal;
import org.kapott.hbci.tools.CryptUtils;

public abstract class AbstractRDHSWPassport 
	extends AbstractRDHPassport 
{
    protected HBCIKey[][] keys;

    protected AbstractRDHSWPassport(Object init)
    {
    	super(init);
    	
        keys=new HBCIKey[3][];
        for (int i=0;i<3;i++) {
            keys[i]=new HBCIKey[2];
            for (int j=0;j<2;j++) {
                keys[i][j]=null;
            }
        }
    }
    
    public boolean hasInstSigKey()
    {
        return getInstSigKey()!=null;
    }
    
    public boolean hasInstEncKey()
    {
        return getInstEncKey()!=null;
    }
    
    public boolean hasMySigKey()
    {
        return getMyPublicSigKey()!=null;
    }
    
    public boolean hasMyEncKey()
    {
        return getMyPublicEncKey()!=null;
    }
    
    public HBCIKey getKey(int i,int j)
    {
        return keys[i][j];
    }
    
    public void setInstSigKey(HBCIKey key)
    {
        setKey(0,0,key);
    }

    public void setInstEncKey(HBCIKey key)
    {
        setKey(0,1,key);
    }

    public void setMySigKey(HBCIKey key)
    {
        setKey(1,0,key);
        setKey(1,1,key);
    }

    public void setMyEncKey(HBCIKey key)
    {
        setKey(2,0,key);
        setKey(2,1,key);
    }

    public void setMyDigKey(HBCIKey key)
    {
        // TODO
    }
    
    public void setMyPublicSigKey(HBCIKey key)
    {
        setKey(1,0,key);
    }

    public void setMyPrivateSigKey(HBCIKey key)
    {
        setKey(1,1,key);
    }

    public void setMyPublicEncKey(HBCIKey key)
    {
        setKey(2,0,key);
    }

    public void setMyPrivateEncKey(HBCIKey key)
    {
        setKey(2,1,key);
    }

    public void setMyPublicDigKey(HBCIKey key)
    {
        // TODO
    }

    public void setMyPrivateDigKey(HBCIKey key)
    {
        // TODO
    }

    public HBCIKey getMyPublicSigKey()
    {
        return getKey(1,0);
    }

    public HBCIKey getMyPrivateSigKey()
    {
        return getKey(1,1);
    }

    public HBCIKey getMyPublicEncKey()
    {
        return getKey(2,0);
    }

    public HBCIKey getMyPrivateEncKey()
    {
        return getKey(2,1);
    }

    public HBCIKey getMyPublicDigKey()
    {
        // TODO
        return null;
    }
    
    public HBCIKey getMyPrivateDigKey()
    {
        // TODO
        return null;
    }
    
    public HBCIKey getInstSigKey()
    {
        return getKey(0,0);
    }

    public String getInstSigKeyName()
    {
        return getInstSigKey()!=null?getInstSigKey().userid:null;
    }

    public String getInstSigKeyNum()
    {
        return getInstSigKey()!=null?getInstSigKey().num:null;
    }

    public String getInstSigKeyVersion()
    {
        return getInstSigKey()!=null?getInstSigKey().version:null;
    }
    
    public HBCIKey getInstEncKey()
    {
        return getKey(0,1);
    }

    public String getInstEncKeyName()
    {
        return getInstEncKey()!=null?getInstEncKey().userid:null;
    }

    public String getInstEncKeyNum()
    {
        return getInstEncKey()!=null?getInstEncKey().num:null;
    }

    public String getInstEncKeyVersion()
    {
        return getInstEncKey()!=null?getInstEncKey().version:null;
    }

    public String getMySigKeyName()
    {
        return getMyPublicSigKey()!=null?getMyPublicSigKey().userid:null;
    }

    public String getMySigKeyNum()
    {
        return getMyPublicSigKey()!=null?getMyPublicSigKey().num:null;
    }

    public String getMySigKeyVersion()
    {
        return getMyPublicSigKey()!=null?getMyPublicSigKey().version:null;
    }

    public String getMyEncKeyName()
    {
        return getMyPublicEncKey()!=null?getMyPublicEncKey().userid:null;
    }

    public String getMyEncKeyNum()
    {
        return getMyPublicEncKey()!=null?getMyPublicEncKey().num:null;
    }

    public String getMyEncKeyVersion()
    {
        return getMyPublicEncKey()!=null?getMyPublicEncKey().version:null;
    }

    public final void setKey(int i,int j,HBCIKey key)
    {
        // System.out.println("passportRDH: setting key "+i+","+j+" to "+(key==null?"null":key.country+":"+key.blz+":"+key.cid+":"+key.num+":"+key.version));
        keys[i][j]=key;
    }
    
    public byte[] sign(byte[] data)
    {
        /* data is the result from the hash() method. In most cases, this is simply
         * the hbci message to be signed, because the signature algorithms used here
         * (iso-9796-1, iso-9796-2, pkcs#1-pss) INCLUDE the hash-step, so it must
         * not be done manually before.
         * the only exception for this is is RDH-10 where an extra round of hashing
         * must be done before using PKCS#1-PSS */
        
        try {
            Signature sig=getSignatureInstance();
            sig.initSign((PrivateKey)(getMyPrivateSigKey().key));
            sig.update(data);
            byte[] result=sig.sign();
            result=checkForCryptDataSize(result, getCryptDataSize(getMyPublicSigKey().key));
            return result;
        } catch (Exception ex) {
            throw new HBCI_Exception("*** signing of message failed",ex);
        }
    }

    public boolean verify(byte[] data,byte[] sig)
    {
        /* data is the result from the hash() method. In most cases, this is simply
         * the hbci message to be signed, because the signature algorithms used here
         * (iso-9796-1, iso-9796-2, pkcs#1-pss) INCLUDE the hash-step, so it must
         * not be done manually before.
         * the only exception for this is is RDH-10 where an extra round of hashing
         * must be done before using PKCS#1-PSS */
        
        try {
            Signature sign=getSignatureInstance();
            sign.initVerify((PublicKey)(getInstSigKey().key));
            sign.update(data);
            return sign.verify(sig);
        } catch (Exception ex) {
            throw new HBCI_Exception("*** verification of message signature failed",ex); 
        }
    }

    private byte[] encryptMessage(byte[] plainMsg,SecretKey msgkey)
    {
        try {
        	final String provider = CryptUtils.getSecurityProvider();
        	Cipher cipher = provider == null ? Cipher.getInstance("DESede/CBC/NoPadding") : Cipher.getInstance("DESede/CBC/NoPadding", provider);
            byte[] iv=new byte[8];
            Arrays.fill(iv,(byte)(0));
            IvParameterSpec spec=new IvParameterSpec(iv);
            cipher.init(Cipher.ENCRYPT_MODE,msgkey,spec);

            return cipher.doFinal(plainMsg);
        } catch (Exception ex) {
            throw new HBCI_Exception("*** can not encrypt message",ex);
        }
    }
    
    private byte[] encryptKey(SecretKey msgkey)
    {
        try {
            // schluessel als byte-array abspeichern

        	final String provider = CryptUtils.getSecurityProvider();
        	SecretKeyFactory factory = provider==null ? SecretKeyFactory.getInstance("DESede") : SecretKeyFactory.getInstance("DESede", provider);
            DESedeKeySpec spec=(DESedeKeySpec)(factory.getKeySpec(msgkey,DESedeKeySpec.class));
            byte[] plainKey=spec.getKey(); // plainKey ist der DESede-Key

            // abhängig von der Länge des inst-enc-keys
            int    cryptDataSize=getCryptDataSize(getInstEncKey().key);
            byte[] plainText=new byte[cryptDataSize];
            Arrays.fill(plainText,(byte)(0));
            System.arraycopy(plainKey,0,plainText,plainText.length-16,16);
            BigInteger m=new BigInteger(+1,plainText);

            Key k=getInstEncKey().key;
            BigInteger c=m.modPow(((RSAPublicKey)(k)).getPublicExponent(),
                                  ((RSAPublicKey)(k)).getModulus());
 
            byte[] result=c.toByteArray();
            result=checkForCryptDataSize(result, cryptDataSize);
            return result;
        } catch (Exception ex) {
            throw new HBCI_Exception("*** can not encrypt message key",ex);
        }
    }

    public byte[][] encrypt(byte[] plainMsg)
    {
        try {
            SecretKey msgkey=createMsgKey();
            byte[] cryptMsg=encryptMessage(plainMsg,msgkey);
            byte[] cryptKey=encryptKey(msgkey);

            byte[][] ret=new byte[2][];
            ret[0]=cryptKey;
            ret[1]=cryptMsg;

            return ret;
        } catch (Exception ex) {
            throw new HBCI_Exception("*** error while encrypting",ex);
        }
    }

    public byte[] decrypt(byte[] cryptedKey,byte[] cryptedMsg)
    {
        try {
            // key entschluesseln
            Key k=getMyPrivateEncKey().key;
            
            byte[] plainKey;
            if (k instanceof RSAPrivateKey) {
                HBCIUtils.log("decrypting message key with (n,d)-algorithm",HBCIUtils.LOG_DEBUG);
                BigInteger exponent=((RSAPrivateKey)(k)).getPrivateExponent();
                BigInteger modulus=((RSAPrivateKey)(k)).getModulus();

                BigInteger c=new BigInteger(+1,cryptedKey);
                plainKey=c.modPow(exponent,modulus).toByteArray();
            } else {
                HBCIUtils.log("decrypting message key with (p,q,dP,dQ,qInv)-algorithm",HBCIUtils.LOG_DEBUG);
                BigInteger p=((RSAPrivateCrtKey2)k).getP();
                BigInteger q=((RSAPrivateCrtKey2)k).getQ();
                BigInteger dP=((RSAPrivateCrtKey2)k).getdP();
                BigInteger dQ=((RSAPrivateCrtKey2)k).getdQ();
                BigInteger qInv=((RSAPrivateCrtKey2)k).getQInv();
        
                BigInteger c=new BigInteger(+1,cryptedKey);
                BigInteger m1=c.modPow(dP,p);
                BigInteger m2=c.modPow(dQ,q);
                BigInteger h=m1.subtract(m2).multiply(qInv).mod(p); 
                plainKey=m2.add(q.multiply(h)).toByteArray();
            }

            byte[] realPlainKey=new byte[24];
            System.arraycopy(plainKey,plainKey.length-16,realPlainKey,0,16);
            System.arraycopy(plainKey,plainKey.length-16,realPlainKey,16,8);

            DESedeKeySpec spec=new DESedeKeySpec(realPlainKey);
        	final String provider = CryptUtils.getSecurityProvider();
        	SecretKeyFactory fac = provider==null ? SecretKeyFactory.getInstance("DESede") : SecretKeyFactory.getInstance("DESede", provider);
            SecretKey key=fac.generateSecret(spec);

            // nachricht entschluesseln
        	Cipher cipher = provider == null ? Cipher.getInstance("DESede/CBC/NoPadding") : Cipher.getInstance("DESede/CBC/NoPadding", provider);
            byte[] ivarray=new byte[8];
            Arrays.fill(ivarray,(byte)(0));
            IvParameterSpec iv=new IvParameterSpec(ivarray);
            cipher.init(Cipher.DECRYPT_MODE,key,iv);
            return cipher.doFinal(cryptedMsg);
        } catch (Exception ex) {
            throw new HBCI_Exception("*** error while decrypting message",ex);
        }
    }
    
    private int getKeySizeByProfile()
    {
        int ret=-1;
        int profile=Integer.parseInt(getProfileVersion());
        
        switch (profile) {
        case 1:
            ret=768;
            break;
        case 2:
            ret=2048;
            break;
        case 10:
            HBCIKey k=getInstSigKey();
            if (k==null) {
                k=getInstEncKey();
            }
            if (k!=null) {
                RSAPublicKey pkey=(RSAPublicKey)k.key;
                ret = pkey.getModulus().bitLength();
            } else {
                ret=4096;
            }
            break;
        default:
            throw new HBCI_Exception("*** dont know which keysize to use for profile rdh-"+profile);
        }
        
        HBCIUtils.log("using keysize "+ret+" bits for newly generated keys",HBCIUtils.LOG_DEBUG);
        return ret;
    }

    public HBCIKey[][] generateNewUserKeys() 
    {
        HBCIKey[] newSigKey=null;
        HBCIKey[] newEncKey=null;
        try {
            HBCIUtils.log("Erzeuge neue Benutzerschlüssel",HBCIUtils.LOG_INFO);

            String blz=getBLZ();
            String country=getCountry();
            String userid=getUserId();
            String profileVersion=getProfileVersion();

            newSigKey=new HBCIKey[2];
            newEncKey=new HBCIKey[2];
            
            String num=hasMySigKey()?getMyPublicSigKey().num:profileVersion;
            String version=hasMySigKey()?getMyPublicSigKey().version:"0";
            version=Integer.toString(Integer.parseInt(version)+1);
            
            int keySize=getKeySizeByProfile();

            // TODO: auch dig key neu generieren?
            for (int i=0;i<2;i++) {
                KeyPairGenerator keygen=KeyPairGenerator.getInstance("RSA");
                // die schlüssellänge ist vom sicherheitsprofil abhängig 
                keygen.initialize(keySize);
                KeyPair pair=keygen.generateKeyPair();

                if (i==0) {
                    newSigKey[0]=new HBCIKey(country,blz,userid,num,version,pair.getPublic());
                    newSigKey[1]=new HBCIKey(country,blz,userid,num,version,pair.getPrivate());
                } else {
                    newEncKey[0]=new HBCIKey(country,blz,userid,num,version,pair.getPublic());
                    newEncKey[1]=new HBCIKey(country,blz,userid,num,version,pair.getPrivate());
                }
            }
        } catch (Exception ex) {
            throw new HBCI_Exception(HBCIUtilsInternal.getLocMsg("EXCMSG_GENKEYS_ERR"),ex);
        }
        
        HBCIKey[][] ret=new HBCIKey[3][];
        ret[0]=newSigKey;
        ret[1]=newEncKey;
        // TODO: dig keys
        ret[2]=null;
        
        return ret;
    }
}
