/* $Id: RDHXFile.java,v 1.1 2011/05/04 22:37:48 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.rdhXfile;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;
import javax.crypto.spec.SecretKeySpec;
import org.kapott.cryptalgs.PBKDF2;
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.kapott.hbci.passport.rdhXfile.HBCIAccount.UserKeys;
public class RDHXFile
{
private List<TLV> fields;
private byte[] passphrase;
public RDHXFile(byte[] passphrase)
{
this.fields=new ArrayList<TLV>();
this.passphrase=passphrase;
}
public RDHXFile(byte[] data, byte[] passphrase)
{
this(passphrase);
// alle felder extrahieren und zu filecontent-content hinzuf�gen
int posi=0;
int len=data.length;
while (posi<len) {
TLV tlv=new TLV(data,posi);
long tag=tlv.getTag();
if (tag==0x4e56L) {
// diskhead (tag=0x564e, len=26|28) (1..1)
HBCIUtils.log("found diskhead field",HBCIUtils.LOG_INTERN);
tlv=new FileHeader(tlv);
} else if (tag==0x564bL) {
// bankdata (tag=0x4b56, len=..) (0..n)
HBCIUtils.log("found hbciaccount field",HBCIUtils.LOG_INTERN);
tlv=new HBCIAccount(tlv);
} else if (tag==0x53d6L) {
// bankkeys (tag=0xd653, len=..) (0..n)
HBCIUtils.log("found bankkeys field",HBCIUtils.LOG_INTERN);
tlv=new BankKeys(tlv);
} else if (tag==0x4452L) {
// datum (tag=0x5244, len=14) (1..1)
HBCIUtils.log("found date field",HBCIUtils.LOG_INTERN);
tlv=new DateField(tlv);
} else if (tag==0x4d44L) {
// mac (tag=0x444d, len=20) (1..1)
HBCIUtils.log("found mac field",HBCIUtils.LOG_INTERN);
tlv=new MACField(tlv);
} else {
throw new HBCI_Exception("*** invalid field tag found: 0x"+Long.toString(tlv.getTag(),16));
}
addField(tlv);
posi+=4+tlv.getLength();
}
if (getField(FileHeader.class)==null) {
// ohne header-field geht gar nichts
throw new HBCI_Exception("*** RDH-2/10-file does not contain a header field - aborting");
}
MACField macfield=(MACField)getField(MACField.class);
if (macfield!=null) {
byte[] storedMac=macfield.getMac();
byte[] calculatedMac=calculateMAC();
boolean macOK=Arrays.equals(storedMac,calculatedMac);
HBCIUtils.log("MAC field ok: "+macOK,HBCIUtils.LOG_DEBUG);
if (!macOK) {
throw new InvalidPassphraseException();
}
} else {
HBCIUtils.log("RDH-2/10-file does not contain a MAC field - ignoring this for now",HBCIUtils.LOG_ERR);
}
// decrypt private user keys
try {
// calculate decryption key
FileHeader fileHeader=(FileHeader)getField(FileHeader.class);
String algname=(fileHeader.getProfileVersion()==2)?"HmacSHA1":"HmacSHA256";
byte[] derivedKey=deriveKey(24, algname);
SecretKeyFactory keyfac=SecretKeyFactory.getInstance("DESede");
DESedeKeySpec desKeyspec=new DESedeKeySpec(derivedKey);
SecretKey key=keyfac.generateSecret(desKeyspec);
// loop through all userkeys to decrypt them
TLV[] accounts=getFields(HBCIAccount.class);
for (int i=0;i<accounts.length;i++) {
HBCIAccount account=(HBCIAccount)accounts[i];
List<UserKeys> userkeys=account.getUserKeys();
for (Iterator<UserKeys> j=userkeys.iterator();j.hasNext();) {
HBCIAccount.UserKeys userkey= j.next();
userkey.decrypt(key);
HBCIUtils.log(userkey.toString(),HBCIUtils.LOG_INTERN);
}
}
} catch (Exception e) {
throw new HBCI_Exception(e);
}
}
public byte[] getPassphrase()
{
return this.passphrase;
}
public void setPassphrase(byte[] passphrase)
{
this.passphrase = passphrase;
}
public void addField(TLV field)
{
this.fields.add(field);
}
public TLV getField(Class cl)
{
TLV ret=null;
for (Iterator<TLV> i=fields.iterator();i.hasNext();) {
TLV tlv= i.next();
if (tlv.getClass().equals(cl)) {
ret=tlv;
break;
}
}
return ret;
}
public TLV[] getFields(Class cl)
{
List<TLV> ret=new ArrayList<TLV>();
for (Iterator<TLV> i=fields.iterator();i.hasNext();) {
TLV tlv= i.next();
if (tlv.getClass().equals(cl)) {
ret.add(tlv);
}
}
return ret.toArray(new TLV[ret.size()]);
}
public byte[] getFileData(int profileVersion)
{
// update DateField
DateField dateField=(DateField)getField(DateField.class);
if (dateField==null) {
dateField=new DateField();
addField(dateField);
}
dateField.setDate(new Date());
// check for existance of required fields
// HeaderField
FileHeader headerField=(FileHeader)getField(FileHeader.class);
if (headerField==null) {
headerField=new FileHeader();
addField(headerField);
headerField.setNofIterations(10000);
headerField.setRandomSalt();
headerField.setVersion(1);
}
headerField.setProfileVersion(profileVersion);
// MACField
MACField macField=(MACField)getField(MACField.class);
if (macField==null) {
macField=new MACField();
addField(macField);
}
// encrypt private user keys
try {
// calculate encryption key
FileHeader fileHeader=(FileHeader)getField(FileHeader.class);
String algname=(fileHeader.getProfileVersion()==2)?"HmacSHA1":"HmacSHA256";
byte[] derivedKey=deriveKey(24, algname);
SecretKeyFactory keyfac=SecretKeyFactory.getInstance("DESede");
DESedeKeySpec desKeyspec=new DESedeKeySpec(derivedKey);
SecretKey key=keyfac.generateSecret(desKeyspec);
// loop through all userkeys to decrypt them
TLV[] accounts=getFields(HBCIAccount.class);
for (int i=0;i<accounts.length;i++) {
HBCIAccount account=(HBCIAccount)accounts[i];
List<UserKeys> userkeys=account.getUserKeys();
for (Iterator<UserKeys> j=userkeys.iterator();j.hasNext();) {
HBCIAccount.UserKeys userkey= j.next();
userkey.encrypt(key);
}
}
} catch (Exception e) {
throw new HBCI_Exception(e);
}
// reorder fields to standard order
List<TLV> newFields=new ArrayList<TLV>();
Class[] order=new Class[] {FileHeader.class, HBCIAccount.class,
BankKeys.class, DateField.class,
MACField.class};
for (int i=0; i<order.length; i++) {
Class c=order[i];
TLV[] fields=getFields(c);
newFields.addAll(Arrays.asList(fields));
}
this.fields = newFields;
// update all rawdata fields
for (Iterator<TLV> i=this.fields.iterator();i.hasNext();) {
TLV tlv= i.next();
tlv.updateData();
}
// update mac field
byte[] mac=calculateMAC();
MACField macfield=(MACField)getField(MACField.class);
macfield.setMac(mac);
// collect filedata
try {
ByteArrayOutputStream os=new ByteArrayOutputStream();
for (Iterator<TLV> i=this.fields.iterator();i.hasNext();) {
TLV tlv= i.next();
os.write(tlv.getRawData());
}
byte[] ret=os.toByteArray();
os.close();
return ret;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public HBCIKey getBankSigKey(HBCIAccount account)
{
return getBankKey(account,"S");
}
public void setBankSigKey(HBCIAccount account, HBCIKey key)
{
setBankKey(account,"S",key);
}
public HBCIKey getBankEncKey(HBCIAccount account)
{
return getBankKey(account,"V");
}
public void setBankEncKey(HBCIAccount account, HBCIKey key)
{
setBankKey(account,"V",key);
}
private HBCIKey getBankKey(HBCIAccount account,String keytype)
{
HBCIKey ret=null;
String blz=account.getBLZ();
String country=account.getCountry();
TLV[] keyfields=getFields(BankKeys.class);
for (int i=0;i<keyfields.length;i++) {
BankKeys bankkeys=(BankKeys)keyfields[i];
if (bankkeys.getCountry().equals(country) &&
bankkeys.getBLZ().equals(blz) &&
bankkeys.getKeyType().equals(keytype))
{
ret=bankkeys.getHBCIKey();
break;
}
}
return ret;
}
private void setBankKey(HBCIAccount account,String keytype,HBCIKey key)
{
if (key!=null) {
String blz=account.getBLZ();
String country=account.getCountry();
TLV[] keyfields=getFields(BankKeys.class);
boolean found=false;
for (int i=0;i<keyfields.length;i++) {
BankKeys bankkeys=(BankKeys)keyfields[i];
if (bankkeys.getCountry().equals(country) &&
bankkeys.getBLZ().equals(blz) &&
bankkeys.getKeyType().equals(keytype))
{
bankkeys.setKey(keytype,key);
account.setKeyStatus((byte)(account.getKeyStatus()|0x04));
found=true;
break;
}
}
if (!found) {
BankKeys bankkeys=new BankKeys();
addField(bankkeys);
bankkeys.setCountry(account.getCountry());
bankkeys.setBLZ(account.getBLZ());
bankkeys.setKey(keytype,key);
account.setKeyStatus((byte)(account.getKeyStatus()|0x04));
}
}
}
private byte[] getHashData()
{
try {
ByteArrayOutputStream os=new ByteArrayOutputStream();
for (Iterator<TLV> i=this.fields.iterator();i.hasNext();) {
TLV tlv= i.next();
if (!tlv.getClass().equals(MACField.class)) {
tlv.updateData();
os.write(tlv.getRawData());
}
}
byte[] ret=os.toByteArray();
os.close();
return ret;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private byte[] deriveKey(int dkLen, String algname)
{
// key ableiten
FileHeader diskhead=(FileHeader)getField(FileHeader.class);
HBCIUtils.log(
"calculating key with alg "+algname+" and length "+dkLen,
HBCIUtils.LOG_DEBUG);
byte[] derivedKey=PBKDF2.deriveKey(diskhead.getSalt(),
diskhead.getNofIterations(),
getPassphrase(),
dkLen, algname);
return derivedKey;
}
private byte[] calculateMAC()
{
try {
FileHeader header=(FileHeader)getField(FileHeader.class);
int pversion=header.getProfileVersion();
int keysize=(pversion==2)?20:32;
HBCIUtils.log("using "+keysize+"-byte-key for MAC calculation", HBCIUtils.LOG_DEBUG);
// TODO: HmacSHA256 is available only in Java-6++
String algname=(pversion==2)?"HmacSHA1":"HmacSHA256";
HBCIUtils.log("MAC algorithm is "+algname, HBCIUtils.LOG_DEBUG);
byte[] derivedKey=deriveKey(keysize, algname);
SecretKeySpec keyspec=new SecretKeySpec(derivedKey, algname);
Mac mac=Mac.getInstance(algname);
mac.init(keyspec);
byte[] hashdata=getHashData();
byte[] calculatedMac=mac.doFinal(hashdata);
return calculatedMac;
} catch (Exception e) {
throw new HBCI_Exception(e);
}
}
}