/* $Id: HBCIPassportRDHNew.java,v 1.2 2011/05/25 10:07:20 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.CharConversionException; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.math.BigInteger; import java.security.Key; import java.security.KeyFactory; import java.security.interfaces.RSAPrivateCrtKey; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.RSAPrivateCrtKeySpec; import java.security.spec.RSAPrivateKeySpec; import java.security.spec.RSAPublicKeySpec; import java.util.Enumeration; import java.util.Properties; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; import javax.crypto.CipherOutputStream; import javax.crypto.spec.PBEParameterSpec; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.kapott.cryptalgs.RSAPrivateCrtKey2; import org.kapott.hbci.exceptions.HBCI_Exception; import org.kapott.hbci.exceptions.InvalidPassphraseException; import org.kapott.hbci.manager.HBCIKey; import org.kapott.hbci.manager.HBCIUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; /** <p>Passport-Klasse f�r RDH-Zug�nge mit Sicherheitsmedium "Datei". Bei dieser Variante werden sowohl die HBCI-Zugangsdaten wie auch die kryptografischen Schl�ssel f�r die Signierung/Verschl�sselung der HBCI-Nachrichten in einer Datei gespeichert. Der Dateiname kann dabei beliebig vorgegeben werden. Da diese Datei vertrauliche Informationen enth�lt, wird der Inhalt verschl�sselt abgespeichert. Vor dem Erzeugen bzw. Einlesen wird via Callback-Mechanismus nach einem Passwort gefragt, aus dem der Schl�ssel zur Verschl�sselung/Entschl�sselung der Schl�sseldatei berechnet wird.</p><p> Wie auch bei {@link org.kapott.hbci.passport.HBCIPassportDDV} werden in der Schl�sseldatei zus�tzliche Informationen gespeichert. Dazu geh�ren u.a. die BPD und die UPD sowie die HBCI-Version, die zuletzt mit diesem Passport benutzt wurde. Im Gegensatz zu den "Hilfsdateien" bei DDV-Passports darf die Schl�sseldatei bei RDH-Passports aber niemals manuell gel�scht werden, da dabei auch die kryptografischen Schl�ssel des Kunden verlorengehen. Diese k�nnen nicht wieder hergestellt werden, so dass in einem solchen Fall ein manuelles Zur�cksetzes des HBCI-Zuganges bei der Bank erfolgen muss!</p> <p>Die Schl�sseldateien, die <em>HBCI4Java</em> mit dieser Klasse erzeugt und verwaltet, sind <b>nicht kompatibel</b> zu den Schl�sseldateien anderer HBCI-Software (z.B. VR-NetWorld o.�.). Es ist also nicht m�glich, durch Auswahl des Sicherheitsverfahrens "RDH" oder "RDHNew" und Angabe einer schon existierenden Schl�sseldatei, die mit einer anderen HBCI-Software erstellt wurde, diese Schl�sseldatei unter <em>HBCI4Java</em> zu benutzen! Es ist jedoch im Prinzip m�glich, mit der "anderen" Software die Kundenschl�ssel sperren zu lassen und anschlie�end mit <em>HBCI4Java</em> eine v�llig neue Schl�sseldatei zu erzeugen. Das hat aber zwei Nachteile: Zum einen muss nach dem Neuerzeugen der Schl�sseldatei auch ein neuer INI-Brief erzeugt und an die Bank gesandt werden, um die neuen Schl�ssel freischalten zu lassen. Au�erdem l�sst sich nat�rlich die <em>HBCI4Java</em>-Schl�sseldatei nicht mehr in der "anderen" HBCI-Software benutzen. Ein Parallel-Betrieb verschiedener HBCI-Softwarel�sungen, die alle auf dem RDH-Verfahren mit Sicherheitsmedium "Datei" (oder Diskette) basieren, ist meines Wissens nicht m�glich.</p> <p>Ein weiterer Ausweg aus diesem Problem w�re, eine technische Beschreibung des Formates der Schl�sseldateien der "anderen" HBCI-Software zu besorgen und diese dem <a href="mailto:hbci4java@kapott.org">Autor</a> zukommen zu lassen, damit eine Passport-Variante implementiert werden kann, die mit Schl�sseldateien dieser "anderen" Software arbeiten kann.</p> @see org.kapott.hbci.tools.INILetter INILetter */ public class HBCIPassportRDHNew extends AbstractRDHSWFileBasedPassport { private String profileVersion; public HBCIPassportRDHNew(Object init,int dummy) { super(init); } public HBCIPassportRDHNew(Object initObject) { this(initObject,0); setParamHeader("client.passport.RDHNew"); String filename=HBCIUtils.getParam(getParamHeader()+".filename"); boolean init=HBCIUtils.getParam(getParamHeader()+".init","1").equals("1"); if (filename==null) { throw new NullPointerException(getParamHeader()+".filename must not be null"); } HBCIUtils.log("loading passport data from file "+filename,HBCIUtils.LOG_DEBUG); setFilename(filename); if (init) { HBCIUtils.log("loading data from file "+filename,HBCIUtils.LOG_DEBUG); setFilterType("None"); setPort(new Integer(3000)); if (!new File(filename).canRead()) { HBCIUtils.log("have to create new passport file",HBCIUtils.LOG_WARN); askForMissingData(true,true,true,true,false,true,true); saveChanges(); } try { DocumentBuilderFactory dbf=DocumentBuilderFactory.newInstance(); dbf.setValidating(false); DocumentBuilder db=dbf.newDocumentBuilder(); Element root=null; int retries=Integer.parseInt(HBCIUtils.getParam("client.retries.passphrase","3")); while (true) { // loop for entering the correct passphrase if (getPassportKey()==null) setPassportKey(calculatePassportKey(FOR_LOAD)); PBEParameterSpec paramspec=new PBEParameterSpec(CIPHER_SALT,CIPHER_ITERATIONS); Cipher cipher=Cipher.getInstance("PBEWithMD5AndDES"); cipher.init(Cipher.DECRYPT_MODE,getPassportKey(),paramspec); root=null; CipherInputStream ci=null; try { ci=new CipherInputStream(new FileInputStream(getFilename()),cipher); root=db.parse(ci).getDocumentElement(); } catch (Exception e) { // willuhn 2011-05-25 Wir lassen einen erneuten Versuch nur bei einer der beiden // folgenden Exceptions zu. // Die "CharConversionException" ist in der Praxis eine // " com.sun.org.apache.xerces.internal.impl.io.MalformedByteSequenceException: Invalid byte 2 of 2-byte UTF-8 sequence." // Sie fliegt in "db.parse()". Sprich: Der CipherInputStream kann nicht erkennen, // ob das Passwort falsch ist. Stattdessen decodiert er einfach Muell, der // anschliessend nicht als XML-Dokument gelesen werden kann if (!(e instanceof SAXException) && !(e instanceof CharConversionException)) throw e; resetPassphrase(); retries--; if (retries<=0) throw new InvalidPassphraseException(); } finally { if (ci!=null) ci.close(); } if (root!=null) break; } setCountry(getElementValue(root,"country")); setBLZ(getElementValue(root,"blz")); setHost(getElementValue(root,"host")); setPort(new Integer(getElementValue(root,"port"))); setUserId(getElementValue(root,"userid")); setCustomerId(getElementValue(root,"customerid")); setSysId(getElementValue(root,"sysid")); setSigId(new Long(getElementValue(root,"sigid"))); String rdhprofile=getElementValue(root,"rdhprofile"); setProfileVersion(rdhprofile!=null?rdhprofile:""); setHBCIVersion(getElementValue(root,"hbciversion")); setBPD(getElementProps(root,"bpd")); setUPD(getElementProps(root,"upd")); setInstSigKey(getElementKey(root,"inst","S","public")); setInstEncKey(getElementKey(root,"inst","V","public")); setMyPublicSigKey(getElementKey(root,"user","S","public")); setMyPrivateSigKey(getElementKey(root,"user","S","private")); setMyPublicEncKey(getElementKey(root,"user","V","public")); setMyPrivateEncKey(getElementKey(root,"user","V","private")); if (askForMissingData(true,true,true,true,false,true,true)) saveChanges(); } catch (HBCI_Exception e1) { throw e1; } catch (Exception e) { throw new HBCI_Exception("*** error while reading passport file",e); } } } protected String getElementValue(Element root,String name) { String ret=null; NodeList list=root.getElementsByTagName(name); if (list!=null && list.getLength()!=0) { Node content=list.item(0).getFirstChild(); if (content!=null) ret=content.getNodeValue(); } return ret != null && ret.length() > 0 ? ret : null; } protected Properties getElementProps(Element root,String name) { Properties ret=null; Node base=root.getElementsByTagName(name).item(0); if (base!=null) { ret=new Properties(); NodeList entries=base.getChildNodes(); int len=entries.getLength(); for (int i=0;i<len;i++) { Node n=entries.item(i); if (n.getNodeType()==Node.ELEMENT_NODE) { ret.setProperty(((Element)n).getAttribute("name"), ((Element)n).getAttribute("value")); } } } return ret; } protected HBCIKey getElementKey(Element root,String owner,String type,String part) throws Exception { HBCIKey ret=null; NodeList keys=root.getElementsByTagName("key"); int len=keys.getLength(); for (int i=0;i<len;i++) { Node n=keys.item(i); if (n.getNodeType()==Node.ELEMENT_NODE) { Element keynode=(Element)n; if (keynode.getAttribute("owner").equals(owner) && keynode.getAttribute("type").equals(type) && keynode.getAttribute("part").equals(part)) { Key key; if (part.equals("public")) { RSAPublicKeySpec spec=new RSAPublicKeySpec(new BigInteger(getElementValue(keynode,"modulus")), new BigInteger(getElementValue(keynode,"exponent"))); key=KeyFactory.getInstance("RSA").generatePublic(spec); } else { String modulus=getElementValue(keynode,"modulus"); String privexponent=getElementValue(keynode,"exponent"); String pubexponent=getElementValue(keynode,"pubexponent"); String p=getElementValue(keynode,"p"); String q=getElementValue(keynode,"q"); String dP=getElementValue(keynode,"dP"); String dQ=getElementValue(keynode,"dQ"); String qInv=getElementValue(keynode,"qInv"); if (privexponent==null) { // only CRT HBCIUtils.log("private "+type+" key is CRT-only",HBCIUtils.LOG_DEBUG); key=new RSAPrivateCrtKey2(new BigInteger(p), new BigInteger(q), new BigInteger(dP), new BigInteger(dQ), new BigInteger(qInv)); } else if (p==null) { // only exponent HBCIUtils.log("private "+type+" key is exponent-only",HBCIUtils.LOG_DEBUG); RSAPrivateKeySpec spec=new RSAPrivateKeySpec(new BigInteger(modulus), new BigInteger(privexponent)); key=KeyFactory.getInstance("RSA").generatePrivate(spec); } else { // complete data HBCIUtils.log("private "+type+" key is fully specified",HBCIUtils.LOG_DEBUG); RSAPrivateCrtKeySpec spec=new RSAPrivateCrtKeySpec(new BigInteger(modulus), new BigInteger(pubexponent), new BigInteger(privexponent), new BigInteger(p), new BigInteger(q), new BigInteger(dP), new BigInteger(dQ), new BigInteger(qInv)); key=KeyFactory.getInstance("RSA").generatePrivate(spec); } } ret=new HBCIKey(getElementValue(keynode,"country"), getElementValue(keynode,"blz"), getElementValue(keynode,"userid"), getElementValue(keynode,"keynum"), getElementValue(keynode,"keyversion"), key); break; } } } return ret; } /** * @see org.kapott.hbci.passport.HBCIPassport#saveChanges() */ public void saveChanges() { try { if (getPassportKey()==null) setPassportKey(calculatePassportKey(FOR_SAVE)); PBEParameterSpec paramspec=new PBEParameterSpec(CIPHER_SALT,CIPHER_ITERATIONS); Cipher cipher=Cipher.getInstance("PBEWithMD5AndDES"); cipher.init(Cipher.ENCRYPT_MODE,getPassportKey(),paramspec); DocumentBuilderFactory fac=DocumentBuilderFactory.newInstance(); fac.setValidating(false); DocumentBuilder db=fac.newDocumentBuilder(); Document doc=db.newDocument(); Element root=doc.createElement("HBCIPassportRDHNew"); createElement(doc,root,"country",getCountry()); createElement(doc,root,"blz",getBLZ()); createElement(doc,root,"host",getHost()); createElement(doc,root,"port",getPort().toString()); createElement(doc,root,"userid",getUserId()); createElement(doc,root,"customerid",getCustomerId()); createElement(doc,root,"sysid",getSysId()); createElement(doc,root,"sigid",getSigId().toString()); createElement(doc,root,"rdhprofile",getProfileVersion()); createElement(doc,root,"hbciversion",getHBCIVersion()); createPropsElement(doc,root,"bpd",getBPD()); createPropsElement(doc,root,"upd",getUPD()); createKeyElement(doc,root,"inst","S","public",getInstSigKey()); createKeyElement(doc,root,"inst","V","public",getInstEncKey()); createKeyElement(doc,root,"user","S","public",getMyPublicSigKey()); createKeyElement(doc,root,"user","S","private",getMyPrivateSigKey()); createKeyElement(doc,root,"user","V","public",getMyPublicEncKey()); createKeyElement(doc,root,"user","V","private",getMyPrivateEncKey()); TransformerFactory tfac=TransformerFactory.newInstance(); Transformer tform=tfac.newTransformer(); tform.setOutputProperty(OutputKeys.METHOD,"xml"); tform.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION,"no"); tform.setOutputProperty(OutputKeys.ENCODING,"ISO-8859-1"); tform.setOutputProperty(OutputKeys.INDENT,"yes"); File passportfile=new File(getFilename()); File directory=passportfile.getAbsoluteFile().getParentFile(); String prefix=passportfile.getName()+"_"; File tempfile=File.createTempFile(prefix,"",directory); CipherOutputStream co=new CipherOutputStream(new FileOutputStream(tempfile),cipher); tform.transform(new DOMSource(root),new StreamResult(co)); co.close(); this.safeReplace(passportfile,tempfile); } catch (HBCI_Exception e1) { throw e1; } catch (Exception e) { throw new HBCI_Exception("*** saving of passport file failed",e); } } protected void createElement(Document doc,Element root,String elemName,String elemValue) { Node elem=doc.createElement(elemName); root.appendChild(elem); Node data=doc.createTextNode(notNull(elemValue)); elem.appendChild(data); } /** * Liefert den Wert oder einen Leerstring, wenn "value" NULL ist. * @param value der Wert. * @return Leerstring oder der Wert, aber niemals NULL. */ private String notNull(String value) { return value != null ? value : ""; } protected void createPropsElement(Document doc,Element root,String elemName,Properties p) { if (p!=null) { Node base=doc.createElement(elemName); root.appendChild(base); for (Enumeration e=p.propertyNames();e.hasMoreElements();) { String key=(String)e.nextElement(); String value=p.getProperty(key); Element data=doc.createElement("entry"); data.setAttribute("name",key); data.setAttribute("value",notNull(value)); // Der Wert kann bei Properties eigentlich nicht null sein. base.appendChild(data); } } } protected void createKeyElement(Document doc,Element root,String owner,String type,String part,HBCIKey key) { if (key!=null) { Element base=doc.createElement("key"); base.setAttribute("owner",owner); base.setAttribute("type",type); base.setAttribute("part",part); root.appendChild(base); createElement(doc,base,"country",notNull(key.country)); createElement(doc,base,"blz",notNull(key.blz)); createElement(doc,base,"userid",notNull(key.userid)); createElement(doc,base,"keynum",notNull(key.num)); createElement(doc,base,"keyversion",notNull(key.version)); Element keydata=doc.createElement("keydata"); base.appendChild(keydata); byte[] e=key.key.getEncoded(); String encoded=(e!=null)?HBCIUtils.encodeBase64(e):null; String format=key.key.getFormat(); if (encoded!=null) { Element data=doc.createElement("rawdata"); data.setAttribute("format",format); data.setAttribute("encoding","base64"); keydata.appendChild(data); Node content=doc.createTextNode(encoded); data.appendChild(content); } if (part.equals("public") && key.key != null) { createElement(doc,keydata,"modulus",((RSAPublicKey)key.key).getModulus().toString()); createElement(doc,keydata,"exponent",((RSAPublicKey)key.key).getPublicExponent().toString()); } else { if (key.key instanceof RSAPrivateCrtKey) { HBCIUtils.log("saving "+type+" key as fully specified",HBCIUtils.LOG_DEBUG); createElement(doc,keydata,"modulus",((RSAPrivateCrtKey)key.key).getModulus().toString()); createElement(doc,keydata,"exponent",((RSAPrivateCrtKey)key.key).getPrivateExponent().toString()); createElement(doc,keydata,"pubexponent",((RSAPrivateCrtKey)key.key).getPublicExponent().toString()); createElement(doc,keydata,"p",((RSAPrivateCrtKey)key.key).getPrimeP().toString()); createElement(doc,keydata,"q",((RSAPrivateCrtKey)key.key).getPrimeQ().toString()); createElement(doc,keydata,"dP",((RSAPrivateCrtKey)key.key).getPrimeExponentP().toString()); createElement(doc,keydata,"dQ",((RSAPrivateCrtKey)key.key).getPrimeExponentQ().toString()); createElement(doc,keydata,"qInv",((RSAPrivateCrtKey)key.key).getCrtCoefficient().toString()); } else if (key.key instanceof RSAPrivateKey) { HBCIUtils.log("saving "+type+" key as exponent-only",HBCIUtils.LOG_DEBUG); createElement(doc,keydata,"modulus",((RSAPrivateKey)key.key).getModulus().toString()); createElement(doc,keydata,"exponent",((RSAPrivateKey)key.key).getPrivateExponent().toString()); } else if (key.key instanceof RSAPrivateCrtKey2) { HBCIUtils.log("saving "+type+" key as crt-only",HBCIUtils.LOG_DEBUG); createElement(doc,keydata,"p",((RSAPrivateCrtKey2)key.key).getP().toString()); createElement(doc,keydata,"q",((RSAPrivateCrtKey2)key.key).getQ().toString()); createElement(doc,keydata,"dP",((RSAPrivateCrtKey2)key.key).getdP().toString()); createElement(doc,keydata,"dQ",((RSAPrivateCrtKey2)key.key).getdQ().toString()); createElement(doc,keydata,"qInv",((RSAPrivateCrtKey2)key.key).getQInv().toString()); } else { HBCIUtils.log("key has none of the known types - please contact the author!",HBCIUtils.LOG_WARN); } } } } public void setProfileVersion(String version) { this.profileVersion=version; } public String getProfileVersion() { String ret=this.profileVersion; if (ret==null) { ret=""; } if (ret.length()==0) { HBCIUtils.log("have to determine my rdh-profile-version, but have no information about it yet", HBCIUtils.LOG_DEBUG); // es ist noch keine profilnummer bekannt, d.h. im passport-file // stand keine drin // das kann entweder daran liegen, dass es sich um ein "altes" // rdhnew-file handelte (in diesem fall ist die nummer "1"), // oder weil noch gar keine schl�ssel im file gespeichert sind // und damit auch kein profil feststeht - in diesem fall verwenden // wir die h�chste profil-nummer aus den BPD if (hasMySigKey()) { HBCIUtils.log("found user sig key in passport file, but no profile version, "+ "so I guess it is an old RDHnew file, which always stored RDH-1 keys", HBCIUtils.LOG_DEBUG); // es gibt Schl�ssel, aber keine profilVersion, also haben wir // gerade ein altes file gelesen, in dem diese Info noch nicht // drinstand ret="1"; } else { HBCIUtils.log("no user keys found in passport - so we use the highest available profile", HBCIUtils.LOG_DEBUG); // es gibt noch gar keine schl�ssel - also nehmen wir die // h�chste unterst�tzte profil-nummer String[][] methods=getSuppSecMethods(); int maxVersion=0; for (int i=0;i<methods.length;i++) { String method=methods[i][0]; int version=Integer.parseInt(methods[i][1]); if (method.equals("RDH") && (version==1 || version==2 || version==10)) { // es werden nur RDH-1, RDH-2 und RDH-10 betrachtet, weil // alle anderen rdh-profile nicht f�r software-l�sungen // zugelassen sind if (version>maxVersion) { maxVersion=version; } } } if (maxVersion!=0) { ret=Integer.toString(maxVersion); } HBCIUtils.log("using RDH profile "+ret+" taken from supported profiles (BPD)", HBCIUtils.LOG_DEBUG); } } if (ret == null || ret.length() == 0) { ret = HBCIUtils.getParam(getParamHeader()+".defaultprofile",null); HBCIUtils.log("unable to determine rdh-profile-version using BPD, using default version " + ret, HBCIUtils.LOG_WARN); } // in jedem fall merken wir uns die gerade ermittelte profil-nummer setProfileVersion(ret); return ret; } }