/* $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 (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;
}
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;
}
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();
passportfile.delete();
tempfile.renameTo(passportfile);
} 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(elemValue);
elem.appendChild(data);
}
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",value);
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",key.country);
createElement(doc,base,"blz",key.blz);
createElement(doc,base,"userid",key.userid);
createElement(doc,base,"keynum",key.num);
createElement(doc,base,"keyversion",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")) {
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);
}
}
// in jedem fall merken wir uns die gerade ermittelte profil-nummer
setProfileVersion(ret);
return ret;
}
}