All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.kapott.hbci.manager.FlickerCode Maven / Gradle / Ivy

Go to download

HBCI4j - Home Banking Computer Interface for Java - Clone from https://github.com/hbci4j/hbci4java

There is a newer version: 3.5.46
Show newest version
/**********************************************************************
 * $Source: /cvsroot/hibiscus/hbci4java/src/org/kapott/hbci/manager/FlickerCode.java,v $
 * $Revision: 1.9 $
 * $Date: 2011/06/24 16:53:23 $
 * $Author: willuhn $
 *
 * Copyright (c) by willuhn - software & services
 * All rights reserved
 *
 **********************************************************************/

package org.kapott.hbci.manager;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

/**
 * Implementierung des Flicker-Codes fuer optisches ChipTAN.
 * Basiert auf der Javascript-Implementierung von
 * http://6xq.net/media/00/20/flickercode.html
 * 

* Die Javascript-Implementierung war jedoch nicht mehr aktuell (basiert auf HHD 1.3). */ @Slf4j public class FlickerCode { /** * Die Anzahl der Bytes, in der die Laenge des Challenge bei HHD 1.4 steht. * Bei HHD 1.3 war das noch 2 Zeichen lang. * Wenn der Flicker-Code nicht in "Challenge HHDuc" uebertragen wurde * sondern direkt im Freitext-Challenge, koennen wir das Problem umgehen, * indem wir in clean() einfach eine "0" vorn anhaengen. * Wenn er aber tatsaechlich im "Challenge HHDuc" steht, kann man dem * Code nicht ansehen, ob es ein HHD 1.3-Code ist. In dem Fall hilft nur * Try&Error. Also mit HHD 1.4 parsen. Und wenn das fehlschlaegt, dann * HHD 1.3 versuchen. */ private final static int LC_LENGTH_HHD14 = 3; /** * Die Anzahl der Bytes, in der die Laenge des Challenge bei HHD 1.3 steht. */ private final static int LC_LENGTH_HHD13 = 2; /** * Default-Laenge der LDE-Laengen-Angabe. */ private final static int LDE_LENGTH_DEFAULT = 2; /** * Fallback-Laenge der LDE-Laengen-Angabe bei der Sparda. */ private final static int LDE_LENGTH_SPARDA = 3; /** * Die Position des Bits, welches das Encoding enthaelt. */ private final static int BIT_ENCODING = 6; /** * Die Position des Bits, welches festlegt, ob ein Controlbyte folgt. */ private final static int BIT_CONTROLBYTE = 7; /** * Die HHD-Version. */ public HHDVersion version = null; /** * Laenge des gesamten Codes. */ public int lc = 0; /** * Der Startcode. */ public Startcode startCode = new Startcode(); /** * Datenelement 1. */ public DE de1 = new DE(); /** * Datenelement 2. */ public DE de2 = new DE(); /** * Datenelement 3. */ public DE de3 = new DE(); /** * Der Rest des Codes. Mit dem koennen wir nichts anfangen */ public String rest = null; /** * ct. * Parameterloser Konstruktor zum manuellen Zusammenstecken eines Codes. */ public FlickerCode() { } /** * ct. * Parst den HHDuc-Code aus dem uebergebenen Code. * * @param code der zu parsende Code. */ public FlickerCode(String code) { // Wir versuchen es erstmal als HHD 1.4 try { try { parse(code, HHDVersion.HHD14); } catch (Exception e) { // Wir versuchen den Sparda-Workaround mit 3 Zeichen langem LDE parse(code, HHDVersion.HHD14, LDE_LENGTH_SPARDA); } } catch (Exception e) { // OK, dann HHD 1.3 parse(code, HHDVersion.HHD13); } } /** * 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 Flickercode oder NULL. */ public static FlickerCode tryParse(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); code.render(); // testweise rendern return code; } catch (Exception e) { log.debug("unable to parse Challenge HHDuc " + hhduc + ":" + HBCIUtils.exception2String(e)); } } // 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); code.render(); // testweise rendern return code; } 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. log.debug("challenge contains no HHDuc (no problem in most cases):" + HBCIUtils.exception2String(e)); } } // Ne, definitiv kein Flicker-Code. return null; } /** * Wandelt die Zahl in Hex-Schreibweise um und fuellt links mit Nullen auf, bis die Laenge "len" erreicht ist. * * @param n die Zahl. * @param len die zu erreichende Laenge. * @return die links mit Nullen aufgefuellte Zahl in HEX-Schreibweise. */ private static String toHex(int n, int len) { String s = Integer.toString(n, 16).toUpperCase(); while (s.length() < len) s = "0" + s; return s; } /** * Wandelt alle Zeichen des String gemaess des jeweiligen ASCII-Wertes in HEX-Codierung um. * Beispiel: Das Zeichen "0" hat den ASCII-Wert "30" in Hexadezimal-Schreibweise. * * @param s der umzuwandelnde String. * @return der codierte String. */ private static String toHex(String s) { StringBuffer sb = new StringBuffer(); char[] chars = s.toCharArray(); for (char c : chars) { sb.append(toHex(c, 2)); } return sb.toString(); } /** * Berechnet die Quersumme. * * @param n die Zahl, deren Quersumme errechnet werden soll. * @return die Quersumme. */ private static int quersumme(int n) { int q = 0; while (n != 0) { q += n % 10; n = (int) Math.floor(n / 10); } return q; } /** * Liefert die Summe der Bit-Wertigkeiten fuer die genannten Bits * (beginndend bei 0 und beim kleinsten Bit, angegebens inclusive). *

* Beispiel: * num = 156 (-> 10011100) * bits = 5 *

* Es wird die Summe der Bitwertigkeiten 2^0 bis 2^5 errechnet. * Also der Wert von **011100 = 2^4+2^3+s^2 = 28 * * @param num Zahl, aus der die Summe berechnet werden soll. * @param bits Anzahl der Bits (beginnend bei 0 und beim kleinsten Bit, angegebenes inclusive), deren Wertigkeit * addiert werden soll. * @return der errechnete Wert. */ private static int getBitSum(int num, int bits) { int sum = 0; for (int i = 0; i <= bits; ++i) sum += (num & (1 << i)); return sum; } /** * Prueft, ob in der genannten Zahl das angegebene Bit gesetzt ist. * * @param num die zu pruefende Zahl. * @param bit die Nummer des zu pruefenden Bits. * Wobei "0" das kleinste (rechts) und "7" das groesste (links) Bit ist. * @return true, wenn das Bit gesetzt ist. */ private static boolean isBitSet(int num, int bit) { return (num & (1 << bit)) != 0; } /** * Parst den Code mit der angegebenen HHD-Version. * * @param code der zu parsende Code. * @param version die HHD-Version. */ private void parse(String code, HHDVersion version) { this.parse(code, version, LDE_LENGTH_DEFAULT); } /** * Parst den Code mit der angegebenen HHD-Version. * * @param code der zu parsende Code. * @param version die HHD-Version. * @param ldeLen explizite Angabe der Laenge des LDE. */ private void parse(String code, HHDVersion version, int ldeLen) { reset(); code = clean(code); // 1. LC ermitteln. Banales ASCII { int len = version == HHDVersion.HHD14 ? LC_LENGTH_HHD14 : LC_LENGTH_HHD13; this.lc = Integer.parseInt(code.substring(0, len)); code = code.substring(len); // und abschneiden } // 2. Startcode/Control-Bytes code = this.startCode.parse(code); // 3. LDE/DE 1-3 code = this.de1.parse(code, ldeLen); code = this.de2.parse(code, ldeLen); code = this.de3.parse(code, ldeLen); // 4. Den Rest speichern wir hier. this.rest = code.length() > 0 ? code : null; } /** * Entfernt das CHLGUC0026....CHLGTEXT aus dem Code, falls vorhanden. * Das sind HHD 1.3-Codes, die nicht im "Challenge HHDuc" uebertragen * wurden sondern direkt im Challenge-Freitext, * * @param code * @return */ private String clean(String code) { code = code.replaceAll(" ", ""); // Alle Leerzeichen entfernen code = code.trim(); // Whitespaces entfernen // Jetzt checken, ob die beiden Tokens enthalten sind int t1Start = code.indexOf("CHLGUC"); int t2Start = code.indexOf("CHLGTEXT"); if (t1Start == -1 || t2Start == -1 || t2Start <= t1Start) return code; // Ne, nicht enthalten // Erstmal den 2. Token abschneiden code = code.substring(0, t2Start); // Dann alles abschneiden bis zum Beginn von "CHLGUC" code = code.substring(t1Start); // Wir haben eigentlich nicht nur "CHLGUC" sondern "CHLGUC0026" // Wobei die 4 Zahlen sicher variieren koennen. Wir schneiden einfach alles ab. code = code.substring(10); // Jetzt vorn noch ne "0" dran haengen, damit LC wieder 3-stellig ist - wie bei HHD 1.4 return "0" + code; } /** * Rendert den flickerfaehigen Code aus dem Challenge im HHD-Format. * * @return der neu generierte Flicker-Code. */ public String render() { // 1. Payload ermitteln String s = createPayload(); // 2. Luhn-Checksumme neu berechnen String luhn = createLuhnChecksum(); // 3. XOR-Checksumme neu berechnen String xor = createXORChecksum(s); // 4. Alles zusammenbauen und zurueckliefern return s + luhn + xor; } /** * Generiert den Payload neu. * Das ist der komplette Code, jedoch ohne Pruefziffern am Ende. * * @return der neu generierte Payload. */ private String createPayload() { StringBuffer sb = new StringBuffer(); // 1. Laenge Startcode sb.append(this.startCode.renderLength()); // 2. Die Control-Bytes for (Integer i : this.startCode.controlBytes) { sb.append(toHex(i, 2)); } // 3. Der Startcode sb.append(this.startCode.renderData()); // 4. DEs anhaengen. DE[] deList = new DE[]{this.de1, this.de2, this.de3}; for (int i = 0; i < deList.length; ++i) { DE de = deList[i]; sb.append(de.renderLength()); sb.append(de.renderData()); } String s = sb.toString(); // 5. Laenge neu berechnen und vorn dran haengen int len = s.length(); len += 2; // die zwei Zeichen am Ende mit den Pruefsummen muessen wir noch mit reinrechnen. len = len / 2; // Anzahl der Bytes. Jedes Byte sind 2 Zeichen. String lc = toHex(len, 2); return (lc + s); } /** * Berechnet die XOR-Checksumme fuer den Code neu. * * @param payload * @return die XOR-Checksumme im Hex-Format. */ private String createXORChecksum(String payload) { int xorsum = 0; for (int i = 0; i < payload.length(); ++i) { xorsum ^= Integer.parseInt(Character.toString(payload.charAt(i)), 16); } return toHex(xorsum, 1); } /** * Berechnet die Luhn-Pruefziffer neu. * * @return die Pruefziffer im Hex-Format. */ private String createLuhnChecksum() { //////////////////////////////////////////////////////////////////////////// // Schritt 1: Payload ermitteln StringBuffer sb = new StringBuffer(); // a) Controlbytes for (Integer i : this.startCode.controlBytes) sb.append(toHex(i, 2)); // b) Startcode sb.append(this.startCode.renderData()); // c) DEs if (this.de1.data != null) sb.append(this.de1.renderData()); if (this.de2.data != null) sb.append(this.de2.renderData()); if (this.de3.data != null) sb.append(this.de3.renderData()); String payload = sb.toString(); // //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// // Schritt 2: Pruefziffer berechnen int luhnsum = 0; int i = 0; for (i = 0; i < payload.length(); i += 2) { luhnsum += (1 * Integer.parseInt(Character.toString(payload.charAt(i)), 16)) + quersumme(2 * Integer.parseInt(Character.toString(payload.charAt(i + 1)), 16)); } // Ermittelt, wieviel zu "luhnsum" addiert werden muss, um auf die // naechste Zahl zu kommen, die durch 10 teilbar ist // Beispiel: // luhnsum = 129 modulo 10 -> 9 // 10 - 9 = 1 // also 129 + 1 = 130 int mod = luhnsum % 10; if (mod == 0) return "0"; // Siehe "Schritt 3" in tan_hhd_uc_v14.pdf, Seite 17 int rest = 10 - mod; int sum = luhnsum + rest; // Von dieser Summe ziehen wir die berechnete Summe ab // Beispiel: // 130 - 129 = 1 // 1 -> ist die Luhn-Checksumme. int luhn = sum - luhnsum; return toHex(luhn, 1); // //////////////////////////////////////////////////////////////////////////// } /** * @see java.lang.Object#toString() */ public String toString() { StringBuffer sb = new StringBuffer(); sb.append("VERSION:\n" + this.version + "\n"); sb.append("LC: " + this.lc + "\n"); sb.append("Startcode:\n" + this.startCode + "\n"); sb.append("DE1:\n" + this.de1 + "\n"); sb.append("DE2:\n" + this.de2 + "\n"); sb.append("DE3:\n" + this.de3 + "\n"); sb.append("CB : " + this.rest + "\n"); return sb.toString(); } /** * Resettet den Code. */ private void reset() { this.version = null; this.lc = 0; this.startCode = new Startcode(); this.de1 = new DE(); this.de2 = new DE(); this.de3 = new DE(); this.rest = null; } ////////////////////////////////////////////////////////////////////////////// // Hilfsfunktionen fuer die Berechnungen /** * @see java.lang.Object#equals(java.lang.Object) */ public boolean equals(Object obj) { if (!(obj instanceof FlickerCode)) return false; FlickerCode other = (FlickerCode) obj; if (this.lc != other.lc) return false; if (!this.startCode.equals(other.startCode)) return false; if (!this.de1.equals(other.de1)) return false; if (!this.de2.equals(other.de2)) return false; if (!this.de3.equals(other.de3)) return false; if (this.rest == null) return (other.rest == null); return this.rest.equals(other.rest); } /** * Versionskennung. */ public enum HHDVersion { /** * HHD-Version 1.4 */ HHD14, /** * HHD-Version 1.3 */ HHD13 } /** * Das Encoding der Nutzdaten. */ public enum Encoding { /** * ASC-Encoding. */ ASC, /** * BCD-Encoding. */ BCD, } /** * Bean fuer die Eigenschaften eines einzelnen DE. */ public class DE { /** * Die tatsaechliche Laenge des DE. * Bereinigt um ggf. vorhandene Control-Bits. */ public int length = 0; /** * Die Laengen-Angabe des DE im Roh-Format. * Sie kann noch Control-Bits enthalten, sollte daher * also NICHT fuer Laengenberechnungen verwendet werden. * In dem Fall stattdessen length verwenden. */ public int lde = 0; /** * Die Laenge des LDE. */ public int ldeLen = 0; /** * Das Encoding der Nutzdaten. * Per Definition ist im Challenge HHDuc dieses Bit noch NICHT gesetzt. * Das Encoding passiert erst beim Rendering. */ public Encoding encoding = null; /** * Die eigentlichen Nutzdaten des DE. */ public String data = null; /** * Parst das DE am Beginn des uebergebenen Strings. * * @param s der String, dessen Anfang das DE enthaelt. * @return der Reststring. */ String parse(String s) { return this.parse(s, LDE_LENGTH_DEFAULT); } /** * Parst das DE am Beginn des uebergebenen Strings. * * @param s der String, dessen Anfang das DE enthaelt. * @param ldeLen explizite Angabe der Laenge des LDE. * @return der Reststring. */ String parse(String s, int ldeLen) { // Nichts mehr zum Parsen da if (s == null || s.length() == 0) return s; // LDE ermitteln (dezimal) this.lde = Integer.parseInt(s.substring(0, ldeLen)); s = s.substring(ldeLen); // und abschneiden this.ldeLen = ldeLen; // Control-Bits abschneiden. Die Laengen-Angabe steht nur in den Bits 0-5. // In den Bits 6 und 7 stehen Steuer-Informationen this.length = getBitSum(this.lde, 5); // Bit 0-5 // Encoding gibts hier noch nicht. Das passiert erst beim Rendern // Nutzdaten ermitteln this.data = s.substring(0, this.length); s = s.substring(this.length); // und abschneiden return s; } /** * Rendert die Laengenangabe fuer die Uebertragung via Flickercode. * * @return die codierten Nutzdaten. * Wenn das DE keine Nutzdaten enthaelt, wird "" zurueck gegeben. */ String renderLength() { // Keine Daten enthalten. Dann muessen wir auch nichts weiter // beruecksichtigen. // Laut Belegungsrichtlinien TANve1.4 mit Erratum 1-3 final version vom 2010-11-12.pdf // duerfen im "ChallengeHHDuc" eigentlich keine leeren DEs enthalten // sein. Daher geben wir in dem Fall "" zurueck und nicht "00" wie in // tan_hhd_uc_v14.pdf angegeben. Denn mit "00" wollte es mein TAN-Generator nicht // lesen. Kann aber auch sein, dass der einfach nicht HHD 1.4 tauglich ist if (this.data == null) return ""; Encoding enc = this.getEncoding(); // Die wollen die Anzahl der Bytes, nicht die Laenge der Zeichen int len = renderData().length() / 2; // A) BCD -> Muss nichts weiter codiert werden. if (enc == Encoding.BCD) return toHex(len, 2); // B) ASC -> Encoding-Bit reincodieren // HHD 1.4 -> in das Bit-Feld codieren if (FlickerCode.this.version == HHDVersion.HHD14) { len = len + (1 << BIT_ENCODING); return toHex(len, 2); } // HHD 1.3 -> nur ne 1 im linken Halbbyte schicken return "1" + toHex(len, 1); } /** * Liefert das zu verwendende Encoding fuer die Uebertragung via Flickercode. * Im Normalfall (also nach dem Parsen eines HHDuc) ist kein Encoding angegeben * (im Challenge HHDuc ist das per Definition nie gesetzt) machen wir ASC. * Es sei denn, das Encoding wurde explizit auf BCD gesetzt. * * @return das fuer das Rendering zu verwendende Encoding. */ Encoding getEncoding() { if (this.data == null) return Encoding.BCD; // Explizit angegeben if (this.encoding != null) return this.encoding; // Siehe tan_hhd_uc_v14.pdf, letzter Absatz in B.2.3 // Bei SEPA-Auftraegen koennen auch Buchstaben in BIC/IBAN vorkommen. // In dem Fall muss auch ASC-codiert werden. Also machen wir BCD nur // noch dann, wenn ausschliesslich Zahlen drin stehen. // Das macht subsembly auch so // http://www.onlinebanking-forum.de/phpBB2/viewtopic.php?p=75602#75602 if (this.data.matches("[0-9]{1,}")) return Encoding.BCD; return Encoding.ASC; } /** * Rendert die Nutzdaten fuer die Uebertragung via Flickercode. * * @return die codierten Nutzdaten. * Wenn das DE keine Nutzdaten enthaelt, wird "" zurueck gegeben. */ String renderData() { if (this.data == null) return ""; Encoding enc = this.getEncoding(); if (enc == Encoding.ASC) return toHex(this.data); // Bei BCD-Encoding noch mit "F" auf Byte-Grenze ergenzen String s = this.data; if (s.length() % 2 == 1) s += "F"; return s; } /** * @see java.lang.Object#toString() */ public String toString() { StringBuffer sb = new StringBuffer(); sb.append(" Length : " + this.length + "\n"); sb.append(" LDE : " + this.lde + "\n"); if (this.length > 0) sb.append(" LDE len : " + this.ldeLen + "\n"); sb.append(" Data : " + this.data + "\n"); sb.append(" Encoding: " + this.encoding + "\n"); return sb.toString(); } /** * @see java.lang.Object#equals(java.lang.Object) */ public boolean equals(Object obj) { if (!(obj instanceof DE)) return false; return this.toString().equals(obj.toString()); } } /** * Bean fuer die Eigenschaften des Startcodes. * Selbstverstaendlich sind hier so einige Sachen anders codiert als im DE. * Waer ja auch zu einfach sonst. * Die Laengen-Angabe ist anders codiert (hex statt dec). Und nach der * Laenge kommen nicht sofort die Nutzdaten sondern erst noch die Control-Bytes. */ public class Startcode extends DE { /** * Die Control-Bytes. * In der Regel sollte das nur eines sein. */ public List controlBytes = new ArrayList(); /** * Parst das DE am Beginn des uebergebenen Strings. * * @param s der String, dessen Anfang das DE enthaelt. * @return der Reststring. * @see org.kapott.hbci.manager.FlickerCode.DE#parse(java.lang.String) */ @Override String parse(String s) { // 1. LDE ermitteln (hex) this.lde = Integer.parseInt(s.substring(0, 2), 16); s = s.substring(2); // und abschneiden // 2. tatsaechliche Laenge ermitteln this.length = getBitSum(this.lde, 5); // Bit 0-5 // Encoding gibts hier noch nicht. // Das passiert erst beim Rendern // Wenn kein Control-Byte vorhanden ist, muss es HHD 1.3 sein FlickerCode.this.version = HHDVersion.HHD13; // 3. Control-Byte ermitteln, falls vorhanden if (isBitSet(this.lde, BIT_CONTROLBYTE)) { FlickerCode.this.version = HHDVersion.HHD14; // Es darf maximal 9 Controlbytes geben for (int i = 0; i < 10; ++i) { // 2 Zeichen, Hex int controlByte = Integer.parseInt(s.substring(0, 2), 16); this.controlBytes.add(controlByte); s = s.substring(2); // und abschneiden // Solange beim Controlbyte das groesste Bit gesetzt ist, // folgen weitere if (!isBitSet(controlByte, BIT_CONTROLBYTE)) break; } } // 4. Startcode ermitteln this.data = s.substring(0, this.length); s = s.substring(this.length); // und abschneiden return s; } /** * @see org.kapott.hbci.manager.FlickerCode.DE#renderLength() * Ueberschrieben, weil wir hier noch reincodieren muessen, ob ein Controlbyte folgt. */ String renderLength() { String s = super.renderLength(); // HHD 1.3 -> gibt keine Controlbytes if (FlickerCode.this.version == HHDVersion.HHD13) return s; // HHD 1.4 -> aber keine Controlbytes vorhanden if (this.controlBytes.size() == 0) return s; // Controlbytes reincodieren int len = Integer.parseInt(s, 16); if (this.controlBytes.size() > 0) len += (1 << BIT_CONTROLBYTE); return toHex(len, 2); } /** * @see org.kapott.hbci.manager.FlickerCode.DE#toString() */ public String toString() { StringBuffer sb = new StringBuffer(super.toString()); sb.append(" Controlbytes: " + this.controlBytes + "\n"); return sb.toString(); } /** * @see org.kapott.hbci.manager.FlickerCode.DE#equals(java.lang.Object) */ public boolean equals(Object obj) { if (!(obj instanceof Startcode)) return false; return this.toString().equals(obj.toString()); } } ////////////////////////////////////////////////////////////////////////////// }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy