Die Überprüfung eines Timestamps ist die zentrale und wichtigste Funktion. Dabei wird lokal ein SHA256-Hash der Originaldatei erstellt und mit dem in der Blockchain in einer Transaktion gespeicherten Hashwert verglichen. Stimmen beide Werte überein ist der Nachweis der Echtheit des Timestamps erbracht.
Bislang haben wir zwei Möglichkeiten der Verifizierung kennengelernt. Die einfachste Möglichkeit besteht in der Onlineabfrage der Transaktion mit einem Bitcoin (Testnetz-) Explorer, wie ich es im Beispiel BitcoinJ Erzeuge einen Timestamp gezeigt habe. Die zweite Möglichkeit nutzt das Bitcoin-Netz durch eine Direktabfrage, hierfür ist der Zwischenschritt über die Erweiterung des Timestamps durchzuführen und danach kann die Überprüfung des Timestamps erfolgen.
Hier zeige ich Euch eine dritte Möglichkeit, welche die vorhandene Programmierschnittstelle („API“) eines Bitcoin (Testnetz-) Explorers benutzt. Bitte beachtet, das diese Lösung nur im Bitcoin Testnetz funktioniert.
Sicherlich gibt es elegantere Lösungen zur Auswertung der Antwort des Webservers, aber da ich hier lediglich eine einzige Information auslesen möchte (nämlich das „OP_RETURN“-Feld) habe ich die Schnittstelle nur minimal ausgewertet.
Der Vorteil dieser Lösung liegt in der Einfachheit und Schnelligkeit – nach ganz kurzer Zeit liegt eine Bestätigung über die Richtigkeit des Timestamps vor.
Wie arbeiten wir mit dem Programm? Zuerst wählen wir eine Datei aus deren Timestamp wir verifizieren wollen (die einfache Timestamp-Datei muss „neben“ der Originaldatei liegen):
Nach kurzer Wartezeit wird die Richtigkeit überprüft. Im Feld „SHA256-Hash“ wird der aktuell errechnete Hashwert angezeigt, im Feld „OP_RETURN“ ist der Hashwert aus der Transaktion:
Selbstverständlich wird eine fehlende Übereinstimmung ebenfalls in Rot angezeigt:
Zum Vergleich zeige ich Euch die Transaktion im Bitcoin (Testnetz) Explorer (Direktlink: https://tbtc.bitaps.com/0f85e90d9526ff183d4b9d6e9595a9ecedbcdd414034a0b4ec5001eb594142db):
Die Sourcecodes findet Ihr am Ende des Artikels.
Alle Quellcodes zu BitcoinJ Timestamps & OP_RETURN findet Ihr zum Download in meinem GitHub-Repository BitcoinJTimestamp, welches Ihr über diesen Link erreicht: https://github.com/java-crypto/BitcoinJTimestamp. Alle Programme sind unter Java 11 lauffähig (vermutlich auch unter Java 8) und wurden mit intelliJ IDEA entwickelt, welches für dieses Programm aber nicht notwendig ist.
Zur Nutzung der Programme benötigt Ihr diverse Bibliotheken – ladet Euch diese aus dem separaten GitHub-Archiv (https://github.com/java-crypto/BitcoinJ_Libraries) herunter und bindet Sie über Eure Entwicklungsumgebung ein.
Noch ein Wort zum Thema „Lizenz“: Das Programm steht unter unterschiedlichen Lizenzen, die Ihr bitte beachten solltet. Die von mir erstellten Beispiele selber stehen unter der „Unlicense“-Lizenz, allerdings werden zur Laufzeit diverse Bibliotheken eingebunden, welche zum Teil ganz eigene Lizenzen mitbringen. Darauf kann ich in meinen Lizenzhinweisen nicht hinweisen.
Letzte Bearbeitung: 22.03.2020
Hier der Sourcecode des Programms und der Form; den Quellcode der Klasse RedirectedFrame.java findet Ihr in meinem GitHub-Repository:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 |
/* * Herkunft/Origin: http://javacrypto.bplaced.net/ * Programmierer/Programmer: Michael Fehr * Copyright/Copyright: Michael Fehr * Lizenttext/Licence: verschiedene Lizenzen / several licenses * getestet mit/tested with: Java Runtime Environment 11.0.5 x64 * verwendete IDE/used IDE: intelliJ IDEA 2019.3.1 * Datum/Date (dd.mm.jjjj): 22.03.2020 * Funktion: Ueberprueft die Timestamp-Datei in der Bitcoin Blockchain mit einer Web-API * Function: verifies the timestamp-file in Bitcoin blockchain with a web-API * * Sicherheitshinweis/Security notice * Die Programmroutinen dienen nur der Darstellung und haben keinen Anspruch auf eine korrekte Funktion, * insbesondere mit Blick auf die Sicherheit ! * Prüfen Sie die Sicherheit bevor das Programm in der echten Welt eingesetzt wird. * The program routines just show the function but please be aware of the security part - * check yourself before using in the real world ! * * Sie benötigen diverse Bibliotheken (alle im Github-Archiv im Unterordner "libs") * You need a lot of libraries (see my Github-repository in subfolder "libs") * verwendete BitcoinJ-Bibliothek / used BitcoinJ Library: bitcoinj-core-0.15.6.jar * my Github-Repository: https://github.com/java-crypto/BitcoinJ * libs in my Github-Repo: https://github.com/java-crypto/BitcoinJ_Libraries * */ import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.*; import java.net.URL; import java.net.URLConnection; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; public class BitcoinJVerifyATimestampApi { private LocalDateTime localDateTimeStart; private LocalDateTime localDateTimeEnd; private String filenameTimestamp; private String filenameTimestampAppend = ".timestamp.txt"; private byte[] sha256File = null; private Color tfStatusBackgroundColor; public BitcoinJVerifyATimestampApi() throws IOException { System.out.println("Das Programm arbeitet im Netzwerk: org.bitcoin.test"); System.out.println("Guten Tag, zum Start bitte den Button 'waehlen Sie die Datei um den Zeitstempel zu verifizieren' druecken"); tfStatusBackgroundColor = tfStatus.getBackground(); localDateTimeStart = LocalDateTime.now(); btnFileChooser.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent actionEvent) { tfFile.setText(""); tfHash.setText(""); tfOpReturn.setText(""); tfProofFile.setText(""); tfTxId.setText(""); tfStatus.setText(""); tfStatus.setBackground(tfStatusBackgroundColor); filenameTimestamp = ""; sha256File = null; File file = chooseFile(); try { tfFile.setText(file.toString()); } catch (NullPointerException e) { } if (tfFile.getText() != "") { filenameTimestamp = tfFile.getText(); System.out.println("Datei dessen Timestamp ueberprueft werden soll: " + filenameTimestamp); try { sha256File = generateSha256Buffered(filenameTimestamp); } catch (IOException | NoSuchAlgorithmException e) { e.printStackTrace(); } tfHash.setText(bytesToHex(sha256File)); System.out.println("\nInformationen im OP_RETURN-Bereich der Transaktion werden verifiziert:"); // wir pruefen ob die timestamp-datei existiert String filenameTimestamp = tfFile.getText() + filenameTimestampAppend; tfProofFile.setText(filenameTimestamp); File fileTimestamp = new File(filenameTimestamp); System.out.println("Die Timestampdatei " + filenameTimestamp + " ist vorhanden: " + fileTimestamp.exists()); if (!fileTimestamp.exists()) { System.out.println("Die Timestampdatei ist nicht vorhanden und kann nicht verifiziert werden"); tfStatus.setText("Die Timestampdatei ist nicht vorhanden und kann nicht verifiziert werden"); tfStatus.setBackground(Color.RED); return; } if (!fileTimestamp.canRead() || !fileTimestamp.isFile()) { return; } BufferedReader in = null; String zeile = ""; try { in = new BufferedReader(new FileReader(fileTimestamp)); zeile = null; // nur eine zeile wird gelesen zeile = in.readLine(); } catch (IOException e) { e.printStackTrace(); } finally { if (in != null) try { in.close(); } catch (IOException e) { } } try { in.close(); } catch (IOException e) { e.printStackTrace(); } System.out.println(("TxId in der Datei: " + zeile)); String TxId = zeile; tfTxId.setText(TxId); // get op_return via web api String Op_Return = ""; try { Op_Return = getOpReturnFromTransactionSmartBitShort(TxId); } catch (IOException e) { e.printStackTrace(); System.out.println("Die Transaktion beinhaltet keinen OP_RETURN"); tfStatus.setText("Die Transaktion beinhaltet keinen OP_RETURN"); tfStatus.setBackground(Color.RED); return; } tfOpReturn.setText(Op_Return); boolean verifyOpReturn = tfHash.getText().equals(tfOpReturn.getText()); if (verifyOpReturn == true) { System.out.println("Der OP_Return stimmt ueberein, die Datei ist identisch"); tfStatus.setText("Der OP_Return stimmt ueberein, die Datei ist identisch"); tfStatus.setBackground(Color.GREEN); } else { System.out.println("Der OP_Return stimmt NICHT ueberein, die Datei ist NICHT identisch"); tfStatus.setText("Der OP Return stimmt nicht ueberein, die Datei ist nicht identisch"); tfStatus.setBackground(Color.RED); } } // do nothing } }); btnClose.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent actionEvent) { localDateTimeEnd = LocalDateTime.now(); tfStatus.setText("Das Programm wird beendet, bitte warten ..."); System.out.println("Date & Time at Start: " + localDateTimeStart.toString()); System.out.println("Date & Time at End: " + localDateTimeEnd.toString()); System.exit(0); } }); } private File chooseFile() { JFileChooser chooser = new JFileChooser(); if (chooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) { return chooser.getSelectedFile(); } else { return null; } } public static byte[] generateSha256Buffered(String filenameString) throws IOException, NoSuchAlgorithmException { byte[] buffer= new byte[8192]; int count; MessageDigest md = MessageDigest.getInstance("SHA-256"); BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filenameString)); while ((count = bis.read(buffer)) > 0) { md.update(buffer, 0, count); } bis.close(); return md.digest(); } public static String getOpReturnFromTransactionSmartBitShort(String transaction) throws IOException { String inputLine; String completeLine = ""; String OP_RETURN = ""; URL bc_api = new URL("https://testnet-api.smartbit.com.au/v1/blockchain/tx/" + transaction + "/op-returns"); URLConnection yc = bc_api.openConnection(); BufferedReader in; try { in = new BufferedReader(new InputStreamReader(yc.getInputStream())); } catch (FileNotFoundException e) { return OP_RETURN; } while ((inputLine = in.readLine()) != null) { completeLine = completeLine + inputLine + "\n"; } //System.out.println("Ausgabe:\n" + completeLine); // suche nach "OP_RETURN OP_PUSHBYTES int fundOpReturn = completeLine.indexOf("OP_RETURN"); if (fundOpReturn > 0) { //System.out.println("fundOpReturn: " + fundOpReturn); String OP_Return_End = "\""; int fundOpReturnEnd = completeLine.indexOf(OP_Return_End, fundOpReturn); //System.out.println("fundOpReturn End: " + fundOpReturnEnd); OP_RETURN = completeLine.substring(fundOpReturn, fundOpReturnEnd); //System.out.println("OPRETURN: " + OP_RETURN); // nun noch die eigentlichen werte extrahieren OP_RETURN = OP_RETURN.substring(10); //System.out.println("OPRETURN: " + OP_RETURN); } return OP_RETURN; } private static String bytesToHex(byte[] bytes) { StringBuffer result = new StringBuffer(); for (byte b : bytes) result.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1)); return result.toString(); } private static String getActualDateReverse() { // provides the actual date and time in this format yyyy-MM-dd_HH-mm-ss e.g. 2020-03-16_10-27-15 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"); LocalDateTime today = LocalDateTime.now(); return formatter.format(today); } public static void main(String[] args) throws IOException { JFrame frame = new JFrame("Ueberpruefe einen Timestamp API"); // umleitung der konsole in ein fenster RedirectedFrame outputFrameOutput = new RedirectedFrame("Output Frame", false, true, true, "BitcoinJ_VerifyTimestampApi_Output_" + getActualDateReverse() + ".txt", 700, 600, JFrame.DO_NOTHING_ON_CLOSE); frame.setContentPane(new BitcoinJVerifyATimestampApi().mainPanel); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.pack(); frame.setSize(600, 350); frame.setVisible(true); } private JPanel mainPanel; private JButton btnFileChooser; private JTextField tfFile; private JTextField tfHash; private JTextField tfOpReturn; private JTextField tfProofFile; private JTextField tfTxId; private JTextField tfStatus; private JButton btnClose; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
<?xml version="1.0" encoding="UTF-8"?> <form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="BitcoinJVerifyATimestampApi"> <grid id="27dc6" binding="mainPanel" layout-manager="GridLayoutManager" row-count="14" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1"> <margin top="10" left="10" bottom="10" right="10"/> <constraints> <xy x="20" y="20" width="528" height="416"/> </constraints> <properties/> <border type="none"/> <children> <component id="6a45a" class="javax.swing.JButton" binding="btnFileChooser"> <constraints> <grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/> </constraints> <properties> <enabled value="true"/> <text value="waehlen Sie die Datei um den Zeitstempel zu verifizieren"/> </properties> </component> <component id="e0fd9" class="javax.swing.JSeparator"> <constraints> <grid row="1" column="1" row-span="1" col-span="1" vsize-policy="6" hsize-policy="6" anchor="0" fill="3" indent="0" use-parent-layout="false"/> </constraints> <properties/> </component> <component id="f1a03" class="javax.swing.JTextField" binding="tfFile"> <constraints> <grid row="2" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false"> <preferred-size width="150" height="-1"/> </grid> </constraints> <properties> <editable value="false"/> </properties> </component> <component id="4d2b1" class="javax.swing.JLabel"> <constraints> <grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/> </constraints> <properties> <text value="Datei:"/> </properties> </component> <component id="93929" class="javax.swing.JSeparator"> <constraints> <grid row="3" column="1" row-span="1" col-span="1" vsize-policy="6" hsize-policy="6" anchor="0" fill="3" indent="0" use-parent-layout="false"/> </constraints> <properties/> </component> <component id="6fa8" class="javax.swing.JTextField" binding="tfHash"> <constraints> <grid row="4" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false"> <preferred-size width="150" height="-1"/> </grid> </constraints> <properties> <editable value="false"/> </properties> </component> <component id="14985" class="javax.swing.JLabel"> <constraints> <grid row="4" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/> </constraints> <properties> <text value="SHA256-Hash:"/> </properties> </component> <component id="287a2" class="javax.swing.JSeparator"> <constraints> <grid row="5" column="1" row-span="1" col-span="1" vsize-policy="6" hsize-policy="6" anchor="0" fill="3" indent="0" use-parent-layout="false"/> </constraints> <properties/> </component> <component id="f1b5f" class="javax.swing.JTextField" binding="tfOpReturn"> <constraints> <grid row="6" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false"> <preferred-size width="150" height="-1"/> </grid> </constraints> <properties> <editable value="false"/> </properties> </component> <component id="9855f" class="javax.swing.JLabel"> <constraints> <grid row="6" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/> </constraints> <properties> <text value="Op_Return:"/> </properties> </component> <component id="20d97" class="javax.swing.JSeparator"> <constraints> <grid row="7" column="1" row-span="1" col-span="1" vsize-policy="6" hsize-policy="6" anchor="0" fill="3" indent="0" use-parent-layout="false"/> </constraints> <properties/> </component> <component id="2fb3a" class="javax.swing.JTextField" binding="tfProofFile"> <constraints> <grid row="8" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false"> <preferred-size width="150" height="-1"/> </grid> </constraints> <properties> <editable value="false"/> </properties> </component> <component id="6b8af" class="javax.swing.JLabel"> <constraints> <grid row="8" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/> </constraints> <properties> <text value="Timestamp-Datei:"/> </properties> </component> <component id="c2ef" class="javax.swing.JSeparator"> <constraints> <grid row="9" column="1" row-span="1" col-span="1" vsize-policy="6" hsize-policy="6" anchor="0" fill="3" indent="0" use-parent-layout="false"/> </constraints> <properties/> </component> <component id="f9349" class="javax.swing.JTextField" binding="tfTxId"> <constraints> <grid row="10" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false"> <preferred-size width="150" height="-1"/> </grid> </constraints> <properties> <editable value="false"/> </properties> </component> <component id="d1077" class="javax.swing.JLabel"> <constraints> <grid row="10" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/> </constraints> <properties> <text value="TxId:"/> </properties> </component> <component id="49fa" class="javax.swing.JSeparator"> <constraints> <grid row="11" column="1" row-span="1" col-span="1" vsize-policy="6" hsize-policy="6" anchor="0" fill="3" indent="0" use-parent-layout="false"/> </constraints> <properties/> </component> <component id="e8ac2" class="javax.swing.JTextField" binding="tfStatus"> <constraints> <grid row="12" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false"> <preferred-size width="150" height="-1"/> </grid> </constraints> <properties> <editable value="false"/> </properties> </component> <component id="999d5" class="javax.swing.JLabel"> <constraints> <grid row="12" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/> </constraints> <properties> <text value="Status:"/> </properties> </component> <component id="82258" class="javax.swing.JButton" binding="btnClose"> <constraints> <grid row="13" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/> </constraints> <properties> <text value="Ende des Programms"/> </properties> </component> </children> </grid> </form> |