/* $Id: HBCIPassportPinTan.java,v 1.6 2012/03/13 22:07:43 willuhn Exp $
This file is part of HBCI4Java
Copyright (C) 2001-2008 Stefan Palme
HBCI4Java 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 2 of the License, or
(at your option) any later version.
HBCI4Java 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 this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.kapott.hbci.passport;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.StreamCorruptedException;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.SecretKey;
import javax.crypto.spec.PBEParameterSpec;
import org.kapott.hbci.callback.HBCICallback;
import org.kapott.hbci.exceptions.HBCI_Exception;
import org.kapott.hbci.exceptions.InvalidPassphraseException;
import org.kapott.hbci.manager.FlickerCode;
import org.kapott.hbci.manager.HBCIUtils;
import org.kapott.hbci.manager.HBCIUtilsInternal;
import org.kapott.hbci.manager.HHDVersion;
import org.kapott.hbci.manager.HHDVersion.Type;
import org.kapott.hbci.manager.LogFilter;
import org.kapott.hbci.security.Sig;
/** <p>Passport-Klasse f�r HBCI mit PIN/TAN. Dieses Sicherheitsverfahren wird erst
in FinTS 3.0 spezifiziert, von einigen Banken aber schon mit fr�heren HBCI-Versionen
angeboten.</p><p>
Bei diesem Verfahren werden die Nachrichten auf HBCI-Ebene nicht mit kryptografischen
Verfahren signiert oder verschl�sselt. Als "Signatur" werden statt dessen TANs
zusammen mit einer PIN verwendet. Die PIN wird dabei in <em>jeder</em> HBCI-Nachricht als
Teil der "Signatur" eingef�gt, doch nicht alle Nachrichten ben�tigen eine TAN.
Eine TAN wird nur bei der �bermittlung bestimmter Gesch�ftsvorf�lle ben�tigt. Welche
GV das konkret sind, ermittelt <em>HBCI4Java</em> automatisch aus den BPD. F�r jeden GV, der
eine TAN ben�tigt, wird diese via Callback abgefragt und in die Nachricht eingef�gt.</p><p>
Die Verschl�sselung der Nachrichten bei der �bertragung erfolgt auf einer h�heren
Transportschicht. Die Nachrichten werden n�mlich nicht direkt via TCP/IP �bertragen,
sondern in das HTTP-Protokoll eingebettet. Die Verschl�sselung der �bertragenen Daten
erfolgt dabei auf HTTP-Ebene (via SSL = HTTPS).</p><p>
Wie auch bei {@link org.kapott.hbci.passport.HBCIPassportRDH} wird eine "Schl�sseldatei"
verwendet. In dieser werden allerdings keine kryptografischen Schl�ssel abgelegt, sondern
lediglich die Zugangsdaten f�r den HBCI-Server (Hostadresse, Nutzerkennung, usw.) sowie
einige zus�tzliche Daten (BPD, UPD, zuletzt benutzte HBCI-Version). Diese Datei wird
vor dem Abspeichern verschl�sselt. Vor dem Erzeugen bzw. erstmaligen Einlesen wird via
Callback nach einem Passwort gefragt, aus welchem der Schl�ssel f�r die Verschl�sselung
der Datei berechnet wird</p>*/
public class HBCIPassportPinTan
extends AbstractPinTanPassport
{
private String filename;
private SecretKey passportKey;
private final static byte[] CIPHER_SALT={(byte)0x26,(byte)0x19,(byte)0x38,(byte)0xa7,
(byte)0x99,(byte)0xbc,(byte)0xf1,(byte)0x55};
private final static int CIPHER_ITERATIONS=987;
public HBCIPassportPinTan(Object init,int dummy)
{
super(init);
}
public HBCIPassportPinTan(Object initObject)
{
this(initObject,0);
String header="client.passport.PinTan.";
String fname=HBCIUtils.getParam(header+"filename");
boolean init=HBCIUtils.getParam(header+"init","1").equals("1");
setFileName(fname);
setCertFile(HBCIUtils.getParam(header+"certfile"));
setCheckCert(HBCIUtils.getParam(header+"checkcert","1").equals("1"));
setProxy(HBCIUtils.getParam(header+"proxy",""));
setProxyUser(HBCIUtils.getParam(header+"proxyuser",""));
setProxyPass(HBCIUtils.getParam(header+"proxypass",""));
if (init) {
this.read();
if (askForMissingData(true,true,true,true,true,true,true))
saveChanges();
}
}
/**
* Gibt den Dateinamen der Schl�sseldatei zur�ck.
* @return Dateiname der Schl�sseldatei
*/
public String getFileName()
{
return filename;
}
/**
* Speichert den Dateinamen der Passport-Datei.
* @param filename
*/
public void setFileName(String filename)
{
this.filename=filename;
}
/**
* @see org.kapott.hbci.passport.HBCIPassportInternal#resetPassphrase()
*/
public void resetPassphrase()
{
passportKey=null;
}
/**
* Erzeugt die Passport-Datei wenn noetig.
* In eine extra Funktion ausgelagert, damit es von abgeleiteten Klassen ueberschrieben werden kann.
*/
protected void create()
{
String fname = this.getFileName();
if (fname==null) {
throw new NullPointerException("client.passport.PinTan.filename must not be null");
}
File file = new File(fname);
if (file.exists() && file.isFile() && file.canRead())
return;
HBCIUtils.log("have to create new passport file",HBCIUtils.LOG_WARN);
askForMissingData(true,true,true,true,true,true,true);
saveChanges();
}
/**
* Liest die Daten aus der Passport-Datei ein.
* In eine extra Funktion ausgelagert, damit es von abgeleiteten Klassen ueberschrieben werden kann.
* Zum Beispiel, um eine andere Art der Persistierung zu implementieren.
*/
protected void read()
{
create();
String fname = this.getFileName();
if (fname==null) {
throw new NullPointerException("client.passport.PinTan.filename must not be null");
}
HBCIUtils.log("loading data from file " + fname,HBCIUtils.LOG_DEBUG);
ObjectInputStream o = null;
try
{
int retries = Integer.parseInt(HBCIUtils.getParam("client.retries.passphrase","3"));
while (true) {
if (passportKey == null)
passportKey = calculatePassportKey(FOR_LOAD);
PBEParameterSpec paramspec=new PBEParameterSpec(CIPHER_SALT,CIPHER_ITERATIONS);
Cipher cipher=Cipher.getInstance("PBEWithMD5AndDES");
cipher.init(Cipher.DECRYPT_MODE,passportKey,paramspec);
o = null;
try
{
o=new ObjectInputStream(new CipherInputStream(new FileInputStream(fname),cipher));
}
catch (StreamCorruptedException e)
{
passportKey=null;
retries--;
if (retries<=0)
throw new InvalidPassphraseException();
}
if (o!=null)
break;
}
setCountry((String)(o.readObject()));
setBLZ((String)(o.readObject()));
setHost((String)(o.readObject()));
setPort((Integer)(o.readObject()));
setUserId((String)(o.readObject()));
setSysId((String)(o.readObject()));
setBPD((Properties)(o.readObject()));
setUPD((Properties)(o.readObject()));
setHBCIVersion((String)o.readObject());
setCustomerId((String)o.readObject());
setFilterType((String)o.readObject());
try {
setAllowedTwostepMechanisms((List<String>)o.readObject());
try
{
setCurrentTANMethod((String)o.readObject());
}
catch (Exception e)
{
HBCIUtils.log("no current secmech found in passport file - automatically upgrading to new file format", HBCIUtils.LOG_WARN);
}
}
catch (Exception e)
{
HBCIUtils.log("no list of allowed secmechs found in passport file - automatically upgrading to new file format", HBCIUtils.LOG_WARN);
}
}
catch (Exception e)
{
throw new HBCI_Exception("*** loading of passport file failed",e);
}
try
{
o.close();
}
catch (Exception e)
{
HBCIUtils.log(e);
}
}
/**
* @see org.kapott.hbci.passport.HBCIPassport#saveChanges()
*/
@Override
public void saveChanges()
{
File passportfile = new File(getFileName());
File tempfile = null;
try
{
if (passportKey==null)
passportKey=calculatePassportKey(FOR_SAVE);
PBEParameterSpec paramspec=new PBEParameterSpec(CIPHER_SALT,CIPHER_ITERATIONS);
Cipher cipher=Cipher.getInstance("PBEWithMD5AndDES");
cipher.init(Cipher.ENCRYPT_MODE,passportKey,paramspec);
File directory = passportfile.getAbsoluteFile().getParentFile();
String prefix = passportfile.getName()+"_";
tempfile = File.createTempFile(prefix,"",directory);
HBCIUtils.log("writing to passport file " + tempfile, HBCIUtils.LOG_DEBUG);
ObjectOutputStream o = new ObjectOutputStream(new CipherOutputStream(new FileOutputStream(tempfile),cipher));
o.writeObject(getCountry());
o.writeObject(getBLZ());
o.writeObject(getHost());
o.writeObject(getPort());
o.writeObject(getUserId());
o.writeObject(getSysId());
o.writeObject(getBPD());
o.writeObject(getUPD());
o.writeObject(getHBCIVersion());
o.writeObject(getCustomerId());
o.writeObject(getFilterType());
// hier auch gew�hltes zweischritt-verfahren abspeichern
List<String> l = getAllowedTwostepMechanisms();
HBCIUtils.log("saving two step mechs: " + l, HBCIUtils.LOG_DEBUG);
o.writeObject(l);
String s=getCurrentTANMethod(false);
HBCIUtils.log("saving current tan method: "+s, HBCIUtils.LOG_DEBUG);
o.writeObject(s);
HBCIUtils.log("closing output stream", HBCIUtils.LOG_DEBUG);
o.close();
this.safeReplace(passportfile,tempfile);
}
catch (HBCI_Exception he)
{
throw he;
}
catch (Exception e)
{
throw new HBCI_Exception("*** saving of passport file failed",e);
}
}
/**
* @see org.kapott.hbci.passport.HBCIPassportInternal#hash(byte[])
*/
@Override
public byte[] hash(byte[] data)
{
/* there is no hashing before signing, so we return the original message,
* which will later be "signed" by sign() */
return data;
}
/**
* @see org.kapott.hbci.passport.HBCIPassportInternal#sign(byte[])
*/
@Override
public byte[] sign(byte[] data)
{
try {
// TODO: wenn die eingegebene PIN falsch war, muss die irgendwie
// resettet werden, damit wieder danach gefragt wird
if (getPIN()==null) {
StringBuffer s=new StringBuffer();
HBCIUtilsInternal.getCallback().callback(this,
HBCICallback.NEED_PT_PIN,
HBCIUtilsInternal.getLocMsg("CALLB_NEED_PTPIN"),
HBCICallback.TYPE_SECRET,
s);
if (s.length()==0) {
throw new HBCI_Exception(HBCIUtilsInternal.getLocMsg("EXCMSG_PINZERO"));
}
setPIN(s.toString());
LogFilter.getInstance().addSecretData(getPIN(),"X",LogFilter.FILTER_SECRETS);
}
String tan="";
// tan darf nur beim einschrittverfahren oder bei
// PV=1 und passport.contains(challenge) und tan-pflichtiger auftrag oder bei
// PV=2 und passport.contains(challenge+reference) und HKTAN
// ermittelt werden
String pintanMethod=getCurrentTANMethod(false);
if (pintanMethod.equals(Sig.SECFUNC_SIG_PT_1STEP)) {
// nur beim normalen einschritt-verfahren muss anhand der segment-
// codes ermittelt werden, ob eine tan ben�tigt wird
HBCIUtils.log("onestep method - checking GVs to decide whether or not we need a TAN",HBCIUtils.LOG_DEBUG);
// segment-codes durchlaufen
String codes=collectSegCodes(new String(data,"ISO-8859-1"));
StringTokenizer tok=new StringTokenizer(codes,"|");
while (tok.hasMoreTokens()) {
String code=tok.nextToken();
String info=getPinTanInfo(code);
if (info.equals("J")) {
// f�r dieses segment wird eine tan ben�tigt
HBCIUtils.log("the job with the code "+code+" needs a TAN",HBCIUtils.LOG_DEBUG);
if (tan.length()==0) {
// noch keine tan bekannt --> callback
StringBuffer s=new StringBuffer();
try
{
HBCIUtilsInternal.getCallback().callback(this,
HBCICallback.NEED_PT_TAN,
HBCIUtilsInternal.getLocMsg("CALLB_NEED_PTTAN"),
HBCICallback.TYPE_TEXT,
s);
}
catch (HBCI_Exception e)
{
throw e;
}
catch (Exception e)
{
throw new HBCI_Exception(e);
}
if (s.length()==0) {
throw new HBCI_Exception(HBCIUtilsInternal.getLocMsg("EXCMSG_TANZERO"));
}
tan=s.toString();
} else {
HBCIUtils.log("there should be only one job that needs a TAN!",HBCIUtils.LOG_WARN);
}
} else if (info.equals("N")) {
HBCIUtils.log("the job with the code "+code+" does not need a TAN",HBCIUtils.LOG_DEBUG);
} else if (info.length()==0) {
// TODO: ist das hier dann nicht ein A-Segment? In dem Fall
// w�re diese Warnung �berfl�ssig
HBCIUtils.log("the job with the code "+code+" seems not to be allowed with PIN/TAN",HBCIUtils.LOG_WARN);
}
}
} else {
HBCIUtils.log("twostep method - checking passport(challenge) to decide whether or not we need a TAN",HBCIUtils.LOG_DEBUG);
Properties secmechInfo=getCurrentSecMechInfo();
// gespeicherte challenge aus passport holen
String challenge=(String)getPersistentData("pintan_challenge");
setPersistentData("pintan_challenge",null);
if (challenge==null)
{
// es gibt noch keine challenge
HBCIUtils.log("will not sign with a TAN, because there is no challenge",HBCIUtils.LOG_DEBUG);
}
else
{
HBCIUtils.log("found challenge in passport, so we ask for a TAN",HBCIUtils.LOG_DEBUG);
// willuhn 2011-05-27 Wir versuchen, den Flickercode zu ermitteln und zu parsen
String hhduc = (String) getPersistentData("pintan_challenge_hhd_uc");
setPersistentData("pintan_challenge_hhd_uc",null); // gleich wieder aus dem Passport loeschen
HHDVersion hhd = HHDVersion.find(secmechInfo);
HBCIUtils.log("detected HHD version: " + hhd,HBCIUtils.LOG_DEBUG);
final StringBuffer payload = new StringBuffer();
final String msg = secmechInfo.getProperty("name")+"\n"+secmechInfo.getProperty("inputinfo")+"\n\n"+challenge;
if (hhd.getType() == Type.PHOTOTAN)
{
// Bei PhotoTAN haengen wir ungeparst das HHDuc an. Das kann dann auf
// Anwendungsseite per MatrixCode geparst werden
payload.append(hhduc);
HBCIUtilsInternal.getCallback().callback(this,HBCICallback.NEED_PT_PHOTOTAN,msg,HBCICallback.TYPE_TEXT,payload);
}
else
{
// willuhn 2011-05-27: Flicker-Code anhaengen, falls vorhanden
String flicker = parseFlickercode(challenge,hhduc);
if (flicker != null)
payload.append(flicker);
HBCIUtilsInternal.getCallback().callback(this,HBCICallback.NEED_PT_TAN,msg,HBCICallback.TYPE_TEXT,payload);
}
setPersistentData("externalid",null); // External-ID aus Passport entfernen
if (payload == null || payload.length()==0) {
throw new HBCI_Exception(HBCIUtilsInternal.getLocMsg("EXCMSG_TANZERO"));
}
tan=payload.toString();
}
}
if (tan.length()!=0) {
LogFilter.getInstance().addSecretData(tan,"X",LogFilter.FILTER_SECRETS);
}
return (getPIN()+"|"+tan).getBytes("ISO-8859-1");
} catch (Exception ex) {
throw new HBCI_Exception("*** signing failed",ex);
}
}
/**
* Versucht, aus Challenge und Challenge HHDuc den Flicker-Code zu extrahieren
* und ihn in einen flickerfaehigen Code umzuwandeln.
* Nur wenn tatsaechlich ein gueltiger Code enthalten ist, der als
* HHDuc-Code geparst und in einen Flicker-Code umgewandelt werden konnte,
* liefert die Funktion den Code. Sonst immer NULL.
* @param challenge der Challenge-Text. Das DE "Challenge HHDuc" gibt es
* erst seit HITAN4. Einige Banken haben aber schon vorher optisches chipTAN
* gemacht. Die haben das HHDuc dann direkt im Freitext des Challenge
* mitgeschickt (mit String-Tokens zum Extrahieren markiert). Die werden vom
* FlickerCode-Parser auch unterstuetzt.
* @param hhduc das echte Challenge HHDuc.
* @return der geparste und in Flicker-Format konvertierte Code oder NULL.
*/
private String parseFlickercode(String challenge, String hhduc)
{
// 1. Prioritaet hat hhduc. Gibts aber erst seit HITAN4
if (hhduc != null && hhduc.trim().length() > 0)
{
try
{
FlickerCode code = new FlickerCode(hhduc);
return code.render();
}
catch (Exception e)
{
HBCIUtils.log("unable to parse Challenge HHDuc " + hhduc + ":" + HBCIUtils.exception2String(e),HBCIUtils.LOG_DEBUG);
}
}
// 2. Checken, ob im Freitext-Challenge was parse-faehiges steht.
// Kann seit HITAN1 auftreten
if (challenge != null && challenge.trim().length() > 0)
{
try
{
FlickerCode code = new FlickerCode(challenge);
return code.render();
}
catch (Exception e)
{
// Das darf durchaus vorkommen, weil das Challenge auch bei manuellem
// chipTAN- und smsTAN Verfahren verwendet wird, wo gar kein Flicker-Code enthalten ist.
// Wir loggen es aber trotzdem - fuer den Fall, dass tatsaechlich ein Flicker-Code
// enthalten ist. Sonst koennen wir das nicht debuggen.
HBCIUtils.log("challenge contains no HHDuc (no problem in most cases):" + HBCIUtils.exception2String(e),HBCIUtils.LOG_DEBUG2);
}
}
// Ne, definitiv kein Flicker-Code.
return null;
}
public boolean verify(byte[] data,byte[] sig)
{
// TODO: fuer bankensignaturen fuer HITAN muss dass hier ge�ndert werden
return true;
}
public byte[][] encrypt(byte[] plainMsg)
{
try {
int padLength=plainMsg[plainMsg.length-1];
byte[] encrypted=new String(plainMsg,0,plainMsg.length-padLength,"ISO-8859-1").getBytes("ISO-8859-1");
return new byte[][] {new byte[8],encrypted};
} catch (Exception ex) {
throw new HBCI_Exception("*** encrypting message failed",ex);
}
}
public byte[] decrypt(byte[] cryptedKey,byte[] cryptedMsg)
{
try {
return new String(new String(cryptedMsg,"ISO-8859-1")+'\001').getBytes("ISO-8859-1");
} catch (Exception ex) {
throw new HBCI_Exception("*** decrypting of message failed",ex);
}
}
public void close()
{
super.close();
passportKey=null;
}
}