diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4165418 --- /dev/null +++ b/LICENSE @@ -0,0 +1,177 @@ +The OpenTimestamps Client is free software: you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public License as published +by the Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +The OpenTimestamps Client 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 Lesser General Public License +below for more details. + + + + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/examples/bad-stamp.txt b/examples/bad-stamp.txt new file mode 100644 index 0000000..82d7cfe --- /dev/null +++ b/examples/bad-stamp.txt @@ -0,0 +1,2 @@ +The timestamp on this file is well-formatted, but will fail Bitcoin block +header validation. diff --git a/examples/bad-stamp.txt.ots b/examples/bad-stamp.txt.ots new file mode 100644 index 0000000..897b46d Binary files /dev/null and b/examples/bad-stamp.txt.ots differ diff --git a/examples/empty b/examples/empty new file mode 100644 index 0000000..e69de29 diff --git a/examples/empty.ots b/examples/empty.ots new file mode 100644 index 0000000..c4c3ea3 Binary files /dev/null and b/examples/empty.ots differ diff --git a/examples/hello-world.txt b/examples/hello-world.txt new file mode 100644 index 0000000..980a0d5 --- /dev/null +++ b/examples/hello-world.txt @@ -0,0 +1 @@ +Hello World! diff --git a/examples/hello-world.txt.ots b/examples/hello-world.txt.ots new file mode 100644 index 0000000..d8357eb Binary files /dev/null and b/examples/hello-world.txt.ots differ diff --git a/examples/incomplete.txt b/examples/incomplete.txt new file mode 100644 index 0000000..e641709 --- /dev/null +++ b/examples/incomplete.txt @@ -0,0 +1 @@ +The timestamp on this file is incomplete, and can be upgraded. diff --git a/examples/incomplete.txt.ots b/examples/incomplete.txt.ots new file mode 100644 index 0000000..f49cd35 Binary files /dev/null and b/examples/incomplete.txt.ots differ diff --git a/examples/incomplete.txt.ots.info b/examples/incomplete.txt.ots.info new file mode 100644 index 0000000..5d0e04a --- /dev/null +++ b/examples/incomplete.txt.ots.info @@ -0,0 +1,9 @@ +File sha256 hash: 05c4f616a8e5310d19d938cfd769864d7f4ccdc2ca8b479b10af83564b097af9 +Timestamp: +append e754bf93806a7ebaa680ef7bd0114bf4 +sha256 +append b573e8850cfd9e63d1f043fbb6fc250e +sha256 +prepend 57cfa5c4 +append 6fb1ac8d4e4eb0e7 +verify PendingAttestation('https://alice.btc.calendar.opentimestamps.org') diff --git a/examples/known-and-unknown-notary.txt b/examples/known-and-unknown-notary.txt new file mode 100644 index 0000000..9de80cd --- /dev/null +++ b/examples/known-and-unknown-notary.txt @@ -0,0 +1,2 @@ +This file's timestamp has two attestations, one from a known notary, and one +from an unknown notary. diff --git a/examples/known-and-unknown-notary.txt.ots b/examples/known-and-unknown-notary.txt.ots new file mode 100644 index 0000000..992093d Binary files /dev/null and b/examples/known-and-unknown-notary.txt.ots differ diff --git a/examples/merkle1.txt b/examples/merkle1.txt new file mode 100644 index 0000000..5b74465 --- /dev/null +++ b/examples/merkle1.txt @@ -0,0 +1,2 @@ +This file is one of three different files that have been timestamped together +with a single merkle tree. (1/3) diff --git a/examples/merkle1.txt.ots b/examples/merkle1.txt.ots new file mode 100644 index 0000000..9c9ff83 Binary files /dev/null and b/examples/merkle1.txt.ots differ diff --git a/examples/merkle2.txt b/examples/merkle2.txt new file mode 100644 index 0000000..a66a551 --- /dev/null +++ b/examples/merkle2.txt @@ -0,0 +1,2 @@ +This file is one of three different files that have been timestamped together +with a single merkle tree. (2/3) diff --git a/examples/merkle2.txt.ots b/examples/merkle2.txt.ots new file mode 100644 index 0000000..9cadc72 Binary files /dev/null and b/examples/merkle2.txt.ots differ diff --git a/examples/merkle2.txt.ots.info b/examples/merkle2.txt.ots.info new file mode 100644 index 0000000..79cfa53 --- /dev/null +++ b/examples/merkle2.txt.ots.info @@ -0,0 +1,18 @@ +File sha256 hash: 8bd5a5f07b4451c29756df5eb51d194fb5b20c7e89812d877bbad30d871c582f +Timestamp: +append b63d8f213d047298b8ab4595acd8e5d0 +sha256 +prepend ae59d2c0d2f5efa97df8f3cca7e85845880c102237f1a6a1b0b4c6a5ab77f494 +sha256 +append 026356e7972f023930ec84c213adedc4050460973935bbd2f4df3d7bd5dec55f +sha256 + -> append 2e12050afd7a10ea4f591ed717d35de6 + sha256 + prepend 57d982df + append b1f26e2e55590477 + verify PendingAttestation('https://alice.btc.calendar.opentimestamps.org') + -> append 4aaade9c2ffb853ccff9c07681d019fd + sha256 + prepend 57d982e0 + append 6644ef713071762a + verify PendingAttestation('https://bob.btc.calendar.opentimestamps.org') diff --git a/examples/merkle3.txt b/examples/merkle3.txt new file mode 100644 index 0000000..2fd9fa0 --- /dev/null +++ b/examples/merkle3.txt @@ -0,0 +1,2 @@ +This file is one of three different files that have been timestamped together +with a single merkle tree. (3/3) diff --git a/examples/merkle3.txt.ots b/examples/merkle3.txt.ots new file mode 100644 index 0000000..6bb674e Binary files /dev/null and b/examples/merkle3.txt.ots differ diff --git a/java-opentimestamps.iml b/java-opentimestamps.iml new file mode 100644 index 0000000..557d0fd --- /dev/null +++ b/java-opentimestamps.iml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/BitcoinBlockHeaderAttestation.java b/src/BitcoinBlockHeaderAttestation.java new file mode 100644 index 0000000..6149024 --- /dev/null +++ b/src/BitcoinBlockHeaderAttestation.java @@ -0,0 +1,56 @@ +/** + * Created by luca on 25/02/2017. + */ + +/** + * Bitcoin Block Header Attestation. + * The commitment digest will be the merkleroot of the blockheader. + * The block height is recorded so that looking up the correct block header in + * an external block header database doesn't require every header to be stored + * locally (33MB and counting). (remember that a memory-constrained local + * client can save an MMR that commits to all blocks, and use an external service to fill + * in pruned details). + * Otherwise no additional redundant data about the block header is recorded. + * This is very intentional: since the attestation contains (nearly) the + * absolute bare minimum amount of data, we encourage implementations to do + * the correct thing and get the block header from a by-height index, check + * that the merkleroots match, and then calculate the time from the header + * information. Providing more data would encourage implementations to cheat. + * Remember that the only thing that would invalidate the block height is a + * reorg, but in the event of a reorg the merkleroot will be invalid anyway, + * so there's no point to recording data in the attestation like the header + * itself. At best that would just give us extra confirmation that a reorg + * made the attestation invalid; reorgs deep enough to invalidate timestamps are + * exceptionally rare events anyway, so better to just tell the user the timestamp + * can't be verified rather than add almost-never tested code to handle that case + * more gracefully. + * @extends TimeAttestation + */ +class BitcoinBlockHeaderAttestation extends TimeAttestation { + + public static byte[] _TAG ={(byte)0x05, (byte)0x88, (byte)0x96, (byte)0x0d, (byte)0x73, (byte)0xd7, (byte)0x19, (byte)0x01}; + + @Override + public byte[] _TAG() { + return BitcoinBlockHeaderAttestation._TAG; + } + int height = 0; + + BitcoinBlockHeaderAttestation(int height_) { + super(); + this.height = height_; + } + + public static BitcoinBlockHeaderAttestation deserialize(StreamDeserializationContext ctxPayload) { + int height = ctxPayload.readVaruint(); + return new BitcoinBlockHeaderAttestation(height); + } + + @Override + public void serializePayload(StreamSerializationContext ctx) { + ctx.writeVaruint(this.height); + } + public String toString() { + return "BitcoinBlockHeaderAttestation(" + this.height + ")"; + } +} diff --git a/src/Calendar.java b/src/Calendar.java new file mode 100644 index 0000000..2eba48a --- /dev/null +++ b/src/Calendar.java @@ -0,0 +1,149 @@ + +/** + * Calendar module. + * @module Calendar + * @author EternityWall + * @license LPGL3 + */ + +import com.sun.xml.internal.ws.util.ByteArrayBuffer; + +import java.net.*; +import java.io.*; + +/** Class representing Remote Calendar server interface */ +public class Calendar { + + String url; + + /** + * Create a RemoteCalendar. + * @param {string} url - The server url. + */ + Calendar(String url) { + this.url = url; + } + + + /** + * Submitting a digest to remote calendar. Returns a Timestamp committing to that digest. + * @param {byte[]} digest - The digest hash to send. + */ + public Timestamp submit(byte[] digest) { + ByteArrayBuffer byteArrayBuffer = null; + try { + + URL obj = new URL(url + "/digest"); + HttpURLConnection con = (HttpURLConnection) obj.openConnection(); + con.setRequestMethod("POST"); + + //add request header + con.setRequestProperty("Accept", "application/vnd.opentimestamps.v1"); + con.setRequestProperty("User-Agent", "java-opentimestamps"); + con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + + // Send post request + con.setDoOutput(true); + DataOutputStream wr = new DataOutputStream(con.getOutputStream()); + wr.writeBytes(new String(digest)); + wr.flush(); + wr.close(); + + // Response + int responseCode = con.getResponseCode(); + InputStream inputStream =con.getInputStream(); + BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream); + byteArrayBuffer = new ByteArrayBuffer(); + int current; + while ((current = bufferedInputStream.read()) != -1) { + byteArrayBuffer.write((byte) current); + } + byteArrayBuffer.close(); + + // Response Hanlder + byte[] body = byteArrayBuffer.getRawData(); + if (body.length > 10000) { + System.err.print("Calendar response exceeded size limit"); + return null; + } + + StreamDeserializationContext ctx = new StreamDeserializationContext(body); + Timestamp timestamp = Timestamp.deserialize(ctx, digest); + return timestamp; + + } catch (ProtocolException e) { + e.printStackTrace(); + return null; + } catch (IOException e) { + e.printStackTrace(); + return null; + } catch (Exception e) { + e.printStackTrace(); + return null; + } finally { + try { + byteArrayBuffer.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + + /** + * Get a timestamp for a given commitment. + * @param {byte[]} commitment - The digest hash to send. + */ + public Timestamp getTimestamp(byte[] commitment) { + ByteArrayBuffer byteArrayBuffer = null; + try { + + URL obj = new URL(url + "/timestamp/" + Utils.bytesToHex(commitment)); + HttpURLConnection con = (HttpURLConnection) obj.openConnection(); + con.setRequestMethod("GET"); + + //add request header + con.setRequestProperty("Accept", "application/vnd.opentimestamps.v1"); + con.setRequestProperty("User-Agent", "java-opentimestamps"); + con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + + // Response + int responseCode = con.getResponseCode(); + InputStream inputStream =con.getInputStream(); + BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream); + byteArrayBuffer = new ByteArrayBuffer(); + int current; + while ((current = bufferedInputStream.read()) != -1) { + byteArrayBuffer.write((byte) current); + } + byteArrayBuffer.close(); + + // Response Hanlder + byte[] body = byteArrayBuffer.getRawData(); + if (body.length > 10000) { + System.err.print("Calendar response exceeded size limit"); + return null; + } + + StreamDeserializationContext ctx = new StreamDeserializationContext(body); + Timestamp timestamp = Timestamp.deserialize(ctx, commitment); + return timestamp; + + } catch (ProtocolException e) { + e.printStackTrace(); + return null; + } catch (IOException e) { + e.printStackTrace(); + return null; + } catch (Exception e) { + e.printStackTrace(); + return null; + } finally { + try { + byteArrayBuffer.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/src/DetachedTimestampFile.java b/src/DetachedTimestampFile.java new file mode 100644 index 0000000..a9bc7fb --- /dev/null +++ b/src/DetachedTimestampFile.java @@ -0,0 +1,116 @@ + +/** + * Detached Timestamp File module. + * @module DetachedTimestampFile + * @author EternityWall + * @license LPGL3 + */ + +/** + * Class representing Detached Timestamp File. + * A file containing a timestamp for another file. + * Contains a timestamp, along with a header and the digest of the file. + */ +class DetachedTimestampFile { + + /** + * Header magic bytes + * Designed to be give the user some information in a hexdump, while being identified as 'data' by the file utility. + * @type {int[]} + * @default \x00OpenTimestamps\x00\x00Proof\x00\xbf\x89\xe2\xe8\x84\xe8\x92\x94 + */ + static byte[] HEADER_MAGIC = {(byte)0x00, (byte)0x4f, (byte)0x70, (byte)0x65, (byte)0x6e, (byte)0x54, (byte)0x69,(byte)0x6d, (byte)0x65, (byte)0x73, + (byte)0x74, (byte)0x61, (byte)0x6d, (byte)0x70, (byte)0x73, (byte)0x00, (byte)0x00, (byte)0x50, (byte)0x72, (byte)0x6f, (byte)0x6f, (byte)0x66, (byte)0x00, + (byte)0xbf, (byte)0x89, (byte)0xe2, (byte)0xe8, (byte)0x84, (byte)0xe8, (byte)0x92, (byte)0x94}; + + /** + * While the git commit timestamps have a minor version, probably better to + * leave it out here: unlike Git commits round-tripping is an issue when + * timestamps are upgraded, and we could end up with bugs related to not + * saving/updating minor version numbers correctly. + * @type {int} + * @default 1 + */ + static byte MAJOR_VERSION = 1; + // const MIN_FILE_DIGEST_LENGTH = 20; + // const MAX_FILE_DIGEST_LENGTH = 32; + + Op fileHashOp; + Timestamp timestamp; + + DetachedTimestampFile(Op fileHashOp, Timestamp timestamp) { + this.fileHashOp = fileHashOp; + this.timestamp = timestamp; + } + + /** + * The digest of the file that was timestamped. + * @return {byte} The message inside the timestamp. + */ + public byte[] fileDigest() { + return this.timestamp.msg; + } + + /** + * Serialize a Timestamp File. + * @param {StreamSerializationContext} ctx - The stream serialization context. + * @return {byte[]} The serialized DetachedTimestampFile object. + */ + public void serialize(StreamSerializationContext ctx) { + ctx.writeBytes(HEADER_MAGIC); + ctx.writeVaruint(MAJOR_VERSION); + this.fileHashOp.serialize(ctx); + ctx.writeBytes(this.timestamp.msg); + this.timestamp.serialize(ctx); + } + + /** + * Deserialize a Timestamp File. + * @param {StreamDeserializationContext} ctx - The stream deserialization context. + * @return {DetachedTimestampFile} The generated DetachedTimestampFile object. + */ + public static DetachedTimestampFile deserialize(StreamDeserializationContext ctx) { + ctx.assertMagic(HEADER_MAGIC); + ctx.readVaruint(); + + OpCrypto fileHashOp = (OpCrypto) OpCrypto.deserialize(ctx) ; + byte[] fileHash = ctx.readBytes(fileHashOp._DIGEST_LENGTH()); + Timestamp timestamp = Timestamp.deserialize(ctx, fileHash); + + ctx.assertEof(); + return new DetachedTimestampFile(fileHashOp, timestamp); + } + + /** + * Read the Detached Timestamp File from bytes. + * @param {Op} fileHashOp - The file hash operation. + * @param {StreamDeserializationContext} ctx - The stream deserialization context. + * @return {DetachedTimestampFile} The generated DetachedTimestampFile object. + */ + public static DetachedTimestampFile fromBytes(OpCrypto fileHashOp, StreamDeserializationContext ctx) { + byte[] fdHash = fileHashOp.hashFd(ctx); + return new DetachedTimestampFile(fileHashOp, new Timestamp(fdHash)); + } + + /** + * Read the Detached Timestamp File from hash. + * @param {Op} fileHashOp - The file hash operation. + * @param {int[]} fdHash - The hash file. + * @return {DetachedTimestampFile} The generated DetachedTimestampFile object. + */ + public static DetachedTimestampFile fromHash(Op fileHashOp, byte[] fdHash) { + return new DetachedTimestampFile(fileHashOp, new Timestamp(fdHash)); + } + + /** + * Print the object. + * @return {string} The output. + */ + public String toString() { + String output = "DetachedTimestampFile\n"; + output += "fileHashOp: " + this.fileHashOp.toString() + '\n'; + output += "timestamp: " + this.timestamp.toString() + '\n'; + return output; + } + +} diff --git a/src/Insight.java b/src/Insight.java new file mode 100644 index 0000000..b7df988 --- /dev/null +++ b/src/Insight.java @@ -0,0 +1,129 @@ +/** + * Insight module. + * @module Insight + * @author EternityWall + * @license LPGL3 + */ + +import com.oracle.javafx.jmx.json.JSONFactory; +import com.sun.xml.internal.ws.util.ByteArrayBuffer; +import org.json.JSONObject; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.ProtocolException; +import java.net.URL; + +/** Class used to query Insight API */ +class Insight { + + String urlBlockindex; + String urlBlock; + + + /** + * Create a RemoteCalendar. + */ + Insight(String url) { + this.urlBlockindex = url + "/block-index"; + this.urlBlock = url + "/block"; + + // this.urlBlockindex = 'https://search.bitaccess.co/insight-api/block-index'; + // this.urlBlock = 'https://search.bitaccess.co/insight-api/block'; + // this.urlBlock = "https://insight.bitpay.com/api/block-index/447669"; + } + + + /** + * Retrieve the block hash from the block height. + * @param {string} height - Height of the block. + */ + public InsightResponse blockhash(String height) { + try { + + URL obj = new URL(this.urlBlockindex + '/' + height); + HttpURLConnection con = (HttpURLConnection) obj.openConnection(); + con.setRequestMethod("GET"); + + //add request header + con.setRequestProperty("Accept", "application/vnd.opentimestamps.v1"); + con.setRequestProperty("User-Agent", "java-opentimestamps"); + con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + + // Response + int responseCode = con.getResponseCode(); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(con.getInputStream())); + StringBuilder stringBuilder = new StringBuilder(); + String line; + while ((line = bufferedReader.readLine()) != null) { + stringBuilder.append(line + '\n'); + } + String jsonString = stringBuilder.toString(); + + // Response Hanlder + JSONObject json = new JSONObject(jsonString); + String blockHash = json.getString("blockHash"); + InsightResponse insightResponse = new InsightResponse(); + insightResponse.setBlockHash( blockHash ); + return insightResponse; + + } catch (ProtocolException e) { + e.printStackTrace(); + return null; + } catch (IOException e) { + e.printStackTrace(); + return null; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * Retrieve the block information from the block hash. + * @param {string} height - Height of the block. + */ + public InsightResponse block(String hash) { + try { + + URL obj = new URL(this.urlBlock + '/' + hash); + HttpURLConnection con = (HttpURLConnection) obj.openConnection(); + con.setRequestMethod("GET"); + + //add request header + con.setRequestProperty("Accept", "application/vnd.opentimestamps.v1"); + con.setRequestProperty("User-Agent", "java-opentimestamps"); + con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + + // Response + int responseCode = con.getResponseCode(); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(con.getInputStream())); + StringBuilder stringBuilder = new StringBuilder(); + String line; + while ((line = bufferedReader.readLine()) != null) { + stringBuilder.append(line + '\n'); + } + String jsonString = stringBuilder.toString(); + + // Response Hanlder + JSONObject json = new JSONObject(jsonString); + String merkleroot = json.getString("merkleroot"); + String time = json.getString("time"); + InsightResponse insightResponse = new InsightResponse(); + insightResponse.setMerkleroot( merkleroot ); + insightResponse.setTime( time ); + return insightResponse; + + } catch (ProtocolException e) { + e.printStackTrace(); + return null; + } catch (IOException e) { + e.printStackTrace(); + return null; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + +} \ No newline at end of file diff --git a/src/InsightResponse.java b/src/InsightResponse.java new file mode 100644 index 0000000..e1ebf8d --- /dev/null +++ b/src/InsightResponse.java @@ -0,0 +1,32 @@ +/** + * Created by luca on 27/02/2017. + */ +public class InsightResponse { + + private String merkleroot; + private String blockHash; + private String time; + + public void setTime(String time) { + this.time = time; + } + + public String getTime() { + return time; + } + + public String getMerkleroot() { + return merkleroot; + } + + public void setMerkleroot(String merkleroot) { + this.merkleroot = merkleroot; + } + public String getBlockHash() { + return blockHash; + } + + public void setBlockHash(String blockHash) { + this.blockHash = blockHash; + } +} diff --git a/src/Op.java b/src/Op.java new file mode 100644 index 0000000..953d463 --- /dev/null +++ b/src/Op.java @@ -0,0 +1,109 @@ +import java.util.HashMap; +import java.util.Map; + +/** + * Timestamp proof operations. + * Operations are the edges in the timestamp tree, with each operation taking a message and zero or more arguments to produce a result. + */ +class Op { + + /** + * Maximum length of an Op result + * + * For a verifier, this limit is what limits the maximum amount of memory you + * need at any one time to verify a particular timestamp path; while verifying + * a particular commitment operation path previously calculated results can be + * discarded. + * + * Of course, if everything was a merkle tree you never need to append/prepend + * anything near 4KiB of data; 64 bytes would be plenty even with SHA512. The + * main need for this is compatibility with existing systems like Bitcoin + * timestamps and Certificate Transparency servers. While the pathological + * limits required by both are quite large - 1MB and 16MiB respectively - 4KiB + * is perfectly adequate in both cases for more reasonable usage. + * + * Op subclasses should set this limit even lower if doing so is appropriate + * for them. + */ + public static int _MAX_RESULT_LENGTH = 4096; + + + /** + * Maximum length of the message an Op can be applied too. + * + * Similar to the result length limit, this limit gives implementations a sane + * constraint to work with; the maximum result-length limit implicitly + * constrains maximum message length anyway. + * + * Op subclasses should set this limit even lower if doing so is appropriate + * for them. + */ + public static int _MAX_MSG_LENGTH = 4096; + + public static byte _TAG= (byte)0x00; + + public String _TAG_NAME() { + return ""; + } + + /** + * Deserialize operation from a buffer. + * @param {StreamDeserializationContext} ctx - The stream deserialization context. + * @return {Op} The subclass Operation. + */ + public static Op deserialize(StreamDeserializationContext ctx) { + byte tag = ctx.readBytes(1)[0]; + return Op.deserializeFromTag(ctx, tag); + } + + /** + * Deserialize operation from a buffer. + * @param {StreamDeserializationContext} ctx - The stream deserialization context. + * @param {int} tag - The tag of the operation. + * @return {Op} The subclass Operation. + */ + public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag) { + if (tag == OpAppend._TAG){ + return OpAppend.deserializeFromTag(ctx,tag); + }else if (tag == OpPrepend._TAG){ + return OpPrepend.deserializeFromTag(ctx,tag); + }else if (tag == OpSHA1._TAG){ + return OpSHA1.deserializeFromTag(ctx,tag); + }else if (tag == OpSHA256._TAG){ + return OpSHA256.deserializeFromTag(ctx,tag); + }else if (tag == OpRIPEMD160._TAG){ + return OpRIPEMD160.deserializeFromTag(ctx,tag); + }else { + System.err.print("Unknown operation tag: " + tag); + return null; + } + } + + /** + * Serialize operation. + * @param {StreamSerializationContext} ctx - The stream serialization context. + */ + void serialize(StreamSerializationContext ctx) { + ctx.writeByte(_TAG); + } + + /** + * Apply the operation to a message. + * Raises MsgValueError if the message value is invalid, such as it being + * too long, or it causing the result to be too long. + * @param {byte[]} msg - The message. + */ + byte[] call(byte[] msg) { + if (msg.length > _MAX_MSG_LENGTH) { + System.err.print("Error : Message too long;"); + return null; + } + + byte[] r = this.call(msg); + + if (r.length > _MAX_RESULT_LENGTH) { + System.err.print("Error : Result too long;"); + } + return r; + } +} diff --git a/src/OpAppend.java b/src/OpAppend.java new file mode 100644 index 0000000..7a71543 --- /dev/null +++ b/src/OpAppend.java @@ -0,0 +1,34 @@ + +/** + * Append a suffix to a message. + * @extends OpBinary + */ +class OpAppend extends OpBinary { + + byte[] arg; + + public static byte _TAG= (byte)0xf0; + + @Override + public String _TAG_NAME() { + return "append"; + } + + OpAppend() { + super(); + this.arg = new byte[]{}; + } + OpAppend(byte[] arg_) { + super(arg_); + this.arg = arg_; + } + + @Override + public byte[] call(byte[] msg) { + return Utils.ArraysConcat(msg,this.arg); + } + + public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag) { + return OpBinary.deserializeFromTag(ctx,tag); + } +} \ No newline at end of file diff --git a/src/OpBinary.java b/src/OpBinary.java new file mode 100644 index 0000000..b9500f4 --- /dev/null +++ b/src/OpBinary.java @@ -0,0 +1,52 @@ + +/** + * Operations that act on a message and a single argument. + * @extends OpUnary + */ +class OpBinary extends Op { + + byte[] arg; + + @Override + public String _TAG_NAME() { + return ""; + } + + OpBinary() { + super(); + this.arg = new byte[]{}; + } + OpBinary(byte[] arg_) { + super(); + this.arg = arg_; + } + + public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag) { + byte[] arg = ctx.readVarbytes(Op._MAX_RESULT_LENGTH, 1); + if (tag == OpAppend._TAG){ + return new OpAppend(arg); + }else if (tag == OpPrepend._TAG){ + return new OpPrepend(arg); + }else if (tag == OpSHA1._TAG){ + return new OpSHA1(arg); + }else if (tag == OpSHA256._TAG){ + return new OpSHA256(arg); + }else if (tag == OpRIPEMD160._TAG){ + return new OpRIPEMD160(arg); + }else { + System.err.print("Unknown operation tag: " + tag); + return null; + } + } + + @Override + public void serialize(StreamSerializationContext ctx) { + super.serialize(ctx); + ctx.writeVarbytes(this.arg); + } + + @Override + public String toString() { + return this._TAG_NAME() + ' ' + Utils.bytesToHex(this.arg); + } +} \ No newline at end of file diff --git a/src/OpCrypto.java b/src/OpCrypto.java new file mode 100644 index 0000000..cd91626 --- /dev/null +++ b/src/OpCrypto.java @@ -0,0 +1,80 @@ +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import org.bouncycastle.crypto.digests.RIPEMD160Digest; + + +/** + * Cryptographic transformations. + * These transformations have the unique property that for any length message, + * the size of the result they return is fixed. Additionally, they're the only + * type of operation that can be applied directly to a stream. + * @extends OpUnary + */ +class OpCrypto extends OpUnary { + + byte[] arg; + public byte _TAG = 0x00; + public String _TAG_NAME = ""; + + public String _HASHLIB_NAME() { + return ""; + } + + public int _DIGEST_LENGTH(){ return 0;} + + OpCrypto() { + super(); + this.arg = new byte[]{}; + } + OpCrypto(byte[] arg_) { + super(arg_); + this.arg = arg_; + } + + public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag) { + return OpUnary.deserializeFromTag(ctx,tag); + } + + @Override + public byte[] call(byte[] msg) { + + if(this._HASHLIB_NAME().equals( new OpRIPEMD160()._HASHLIB_NAME() )){ + // Only for RIPEMD160 use bouncycastle library + RIPEMD160Digest digest = new RIPEMD160Digest(); + digest.update (msg, 0, msg.length); + byte[] hash = new byte[digest.getDigestSize()]; + digest.doFinal (hash, 0); + return hash; + } else{ + // For Sha1 & Sha256 use java.security.MessageDigest library + try { + MessageDigest digest = MessageDigest.getInstance(this._HASHLIB_NAME()); + byte[] hash = digest.digest(msg); + return hash; + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + return new byte[]{}; + } + } + + } + + + public byte[] hashFd(StreamDeserializationContext ctx) { + try { + MessageDigest digest = MessageDigest.getInstance(this._HASHLIB_NAME()); + byte[] chunk = ctx.read(1048576); + while (chunk.length>0){ + digest.update(chunk); + chunk = ctx.read(1048576); + } + byte[] hash = digest.digest(); + return hash; + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + return new byte[]{}; + } + } + +} \ No newline at end of file diff --git a/src/OpPrepend.java b/src/OpPrepend.java new file mode 100644 index 0000000..7ef6c1d --- /dev/null +++ b/src/OpPrepend.java @@ -0,0 +1,34 @@ + +/** + * Prepend a prefix to a message. + * @extends OpBinary + */ +class OpPrepend extends OpBinary { + + byte[] arg; + + public static byte _TAG= (byte)0xf1; + + @Override + public String _TAG_NAME() { + return "prepend"; + } + + OpPrepend() { + super(); + this.arg = new byte[]{}; + } + OpPrepend(byte[] arg_) { + super(arg_); + this.arg = arg_; + } + + @Override + public byte[] call(byte[] msg) { + return Utils.ArraysConcat(this.arg,msg); + } + + public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag) { + return OpBinary.deserializeFromTag(ctx,tag); + } +} \ No newline at end of file diff --git a/src/OpRIPEMD160.java b/src/OpRIPEMD160.java new file mode 100644 index 0000000..3d99c88 --- /dev/null +++ b/src/OpRIPEMD160.java @@ -0,0 +1,42 @@ +/** + * Cryptographic RIPEMD160 operation + * Cryptographic operation tag numbers taken from RFC4880, although it's not + * guaranteed that they'll continue to match that RFC in the future. + * @extends CryptOp + */ +class OpRIPEMD160 extends OpCrypto { + + public static byte _TAG = 0x03; + + @Override + public String _TAG_NAME() { + return "ripemd160"; + } + + @Override + public String _HASHLIB_NAME() { + return "ripemd160"; + } + + @Override + public int _DIGEST_LENGTH(){ return 20;} + + OpRIPEMD160() { + super(); + this.arg = new byte[]{}; + } + OpRIPEMD160(byte[] arg_) { + super(arg_); + this.arg = arg_; + } + + @Override + public byte[] call(byte[] msg) { + return super.call(msg); + } + + public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag) { + return OpCrypto.deserializeFromTag(ctx,tag); + } + +} \ No newline at end of file diff --git a/src/OpSHA1.java b/src/OpSHA1.java new file mode 100644 index 0000000..5bd3f59 --- /dev/null +++ b/src/OpSHA1.java @@ -0,0 +1,51 @@ +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Cryptographic SHA1 operation + * Cryptographic operation tag numbers taken from RFC4880, although it's not + * guaranteed that they'll continue to match that RFC in the future. + * Remember that for timestamping, hash algorithms with collision attacks + * *are* secure! We've still proven that both messages existed prior to some + * point in time - the fact that they both have the same hash digest doesn't + * change that. + * Heck, even md5 is still secure enough for timestamping... but that's + * pushing our luck... + * @extends CryptOp + */ +class OpSHA1 extends OpCrypto { + + public static byte _TAG = 0x02; + + @Override + public String _TAG_NAME() { + return "sha1"; + } + + @Override + public String _HASHLIB_NAME() { + return "SHA-1"; + } + + @Override + public int _DIGEST_LENGTH(){ return 20;} + + OpSHA1() { + super(); + this.arg = new byte[]{}; + } + OpSHA1(byte[] arg_) { + super(new byte[]{}); + this.arg = arg_; + } + + @Override + public byte[] call(byte[] msg) { + return super.call(msg); + } + + public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag) { + return OpCrypto.deserializeFromTag(ctx,tag); + } + +} \ No newline at end of file diff --git a/src/OpSHA256.java b/src/OpSHA256.java new file mode 100644 index 0000000..3e671c4 --- /dev/null +++ b/src/OpSHA256.java @@ -0,0 +1,42 @@ +/** + * Cryptographic SHA256 operation + * Cryptographic operation tag numbers taken from RFC4880, although it's not + * guaranteed that they'll continue to match that RFC in the future. + * @extends CryptOp + */ +class OpSHA256 extends OpCrypto { + + public static byte _TAG = 0x08; + + @Override + public String _TAG_NAME() { + return "sha256"; + } + + @Override + public String _HASHLIB_NAME() { + return "SHA-256"; + } + + @Override + public int _DIGEST_LENGTH(){ return 32;} + + OpSHA256() { + super(); + this.arg = new byte[]{}; + } + OpSHA256(byte[] arg_) { + super(new byte[]{}); + this.arg = arg_; + } + + @Override + public byte[] call(byte[] msg) { + return super.call(msg); + } + + public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag) { + return OpCrypto.deserializeFromTag(ctx,tag); + } + +} \ No newline at end of file diff --git a/src/OpUnary.java b/src/OpUnary.java new file mode 100644 index 0000000..377b1fb --- /dev/null +++ b/src/OpUnary.java @@ -0,0 +1,49 @@ + +/** + * Operations that act on a single message. + * @extends Op + */ +class OpUnary extends Op { + + byte[] arg; + + @Override + public String _TAG_NAME() { + return ""; + } + + OpUnary() { + super(); + this.arg = new byte[]{}; + } + OpUnary(byte[] arg_) { + super(); + this.arg = arg_; + } + + public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag) { + if (tag == OpAppend._TAG){ + return new OpAppend(); + }else if (tag == OpPrepend._TAG){ + return new OpPrepend(); + }else if (tag == OpSHA1._TAG){ + return new OpSHA1(); + }else if (tag == OpSHA256._TAG){ + return new OpSHA256(); + }else if (tag == OpRIPEMD160._TAG){ + return new OpRIPEMD160(); + }else { + System.err.print("Unknown operation tag: " + tag); + return null; + } + } + @Override + public void serialize(StreamSerializationContext ctx) { + super.serialize(ctx); + ctx.writeVarbytes(this.arg); + } + @Override + public String toString() { + return this._TAG_NAME() + ' ' + Utils.bytesToHex(this.arg); + } +} \ No newline at end of file diff --git a/src/OpenTimestamps.java b/src/OpenTimestamps.java new file mode 100644 index 0000000..0336e5e --- /dev/null +++ b/src/OpenTimestamps.java @@ -0,0 +1,219 @@ +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * OpenTimestamps module. + * @module OpenTimestamps + * @author EternityWall + * @license LPGL3 + */ + +public class OpenTimestamps { + + + /** + * Show information on a timestamp. + * @exports OpenTimestamps/info + * @param {byte[]} ots - The ots array buffer. + */ + public static String info(byte[] ots) { + if (ots == null) { + return "No ots file"; + } + + StreamDeserializationContext ctx = new StreamDeserializationContext(ots); + DetachedTimestampFile detachedTimestampFile=DetachedTimestampFile.deserialize(ctx); + + String fileHash = Utils.bytesToHex(detachedTimestampFile.timestamp.msg); + String hashOp = ((OpCrypto) detachedTimestampFile.fileHashOp)._HASHLIB_NAME(); + + String firstLine = "File " + hashOp + " hash: " + fileHash + '\n'; + return firstLine + "Timestamp:\n" + detachedTimestampFile.timestamp.strTree(0); + } + + /** + * Create timestamp with the aid of a remote calendar. May be specified multiple times. + * @exports OpenTimestamps/stamp + * @param {byte[]} plain - The plain array buffer to stamp. + * @param {Boolean} isHash - 1 = Hash , 0 = Data File + */ + public static byte[] stamp(byte[]plain, Boolean isHash) throws IOException { + DetachedTimestampFile fileTimestamp; + if (isHash != null && isHash == true) { + // Read Hash + fileTimestamp = DetachedTimestampFile.fromHash(new OpSHA256(), plain); + } else { + // Read from file stream + StreamDeserializationContext ctx = new StreamDeserializationContext(plain); + fileTimestamp = DetachedTimestampFile.fromBytes(new OpSHA256(), ctx); + } + + /* Add nonce: + * Remember that the files - and their timestamps - might get separated + * later, so if we didn't use a nonce for every file, the timestamp + * would leak information on the digests of adjacent files. + * */ + Timestamp merkleRoot; + byte[] bytesRandom16 = new byte[16]; + try { + bytesRandom16 = Utils.randBytes(16); + } catch (IOException e) { + e.printStackTrace(); + throw new IOException(); + } + + // nonce_appended_stamp = file_timestamp.timestamp.ops.add(OpAppend(os.urandom(16))) + Op opAppend = new OpAppend(bytesRandom16); + Timestamp nonceAppendedStamp = fileTimestamp.timestamp.ops.get(opAppend); + if (nonceAppendedStamp == null) { + nonceAppendedStamp = new Timestamp(opAppend.call(fileTimestamp.timestamp.msg)); + fileTimestamp.timestamp.ops.put(opAppend, nonceAppendedStamp); + } + + // merkle_root = nonce_appended_stamp.ops.add(OpSHA256()) + Op opSHA256 = new OpSHA256(); + merkleRoot = nonceAppendedStamp.ops.get(opSHA256); + if (merkleRoot == null) { + merkleRoot = new Timestamp(opSHA256.call(nonceAppendedStamp.msg)); + nonceAppendedStamp.ops.put(opSHA256, merkleRoot); + } + + Timestamp merkleTip = merkleRoot; + List calendarUrls = new ArrayList(); + calendarUrls.add("https://alice.btc.calendar.opentimestamps.org"); + // calendarUrls.append('https://b.pool.opentimestamps.org'); + calendarUrls.add("https://ots.eternitywall.it"); + + Timestamp resultTimestamp = OpenTimestamps.createTimestamp(merkleTip, calendarUrls); + + if (resultTimestamp == null) { + throw new IOException(); + } + // Timestamp serialization + StreamSerializationContext css = new StreamSerializationContext(); + fileTimestamp.serialize(css); + return css.getOutput(); + } + + /** + * Create a timestamp + * @param {timestamp} timestamp - The timestamp. + * @param {List} calendarUrls - List of calendar's to use. + */ + public static Timestamp createTimestamp(Timestamp timestamp, ListcalendarUrls) { + List calendars = new ArrayList(); + /*for (final String calendarUrl : calendarUrls) { + Calendar calendar = new Calendar(calendarUrl); + calendars.add(calendar.submit(timestamp.msg)); + }*/ + Calendar calendar = new Calendar(calendarUrls.get(0)); + Timestamp resultTimestamp = calendar.submit(timestamp.msg); + timestamp.merge(resultTimestamp); + return timestamp; + } + + + /** + * Verify a timestamp. + * @exports OpenTimestamps/verify + * @param {byte[]} ots - The ots array buffer containing the proof to verify. + * @param {byte[]} plain - The plain array buffer to verify. + * @param {Boolean} isHash - 1 = Hash , 0 = Data File + */ + public static String verify(byte[]ots, byte[]plain, Boolean isHash) { + // Read OTS + DetachedTimestampFile detachedTimestamp = null; + try { + StreamDeserializationContext ctx = new StreamDeserializationContext(ots); + detachedTimestamp = DetachedTimestampFile.deserialize(ctx); + } catch (Exception e) { + + } + + byte[] actualFileDigest = new byte[0]; + if (isHash == null || !isHash) { + // Read from file stream + try { + StreamDeserializationContext ctxHashfd = new StreamDeserializationContext(plain); + actualFileDigest = ((OpCrypto)(detachedTimestamp.fileHashOp)).hashFd(ctxHashfd); + } catch (Exception e) { + + } + } else { + // Read Hash + try { + actualFileDigest = plain.clone(); + } catch (Exception e) { + + } + } + + byte[] detachedFileDigest = detachedTimestamp.fileDigest(); + if (!Arrays.equals(actualFileDigest, detachedFileDigest)) { + System.err.print("Expected digest " + Utils.bytesToHex(detachedTimestamp.fileDigest())); + System.err.print("File does not match original!"); + + } + + // console.log(Timestamp.strTreeExtended(detachedTimestamp.timestamp, 0)); + return OpenTimestamps.verifyTimestamp(detachedTimestamp.timestamp); + } + + /** Verify a timestamp. + * @param {Timestamp} timestamp - The timestamp. + * @return {int} unix timestamp if verified, undefined otherwise. + */ + public static String verifyTimestamp(Timestamp timestamp) { + Boolean found = false; + + for (Map.Entry item : timestamp.allAttestations().entrySet()) { + byte[] msg = item.getKey(); + TimeAttestation attestation = item.getValue(); + + if (!found) { // Verify only the first BitcoinBlockHeaderAttestation + if (attestation instanceof PendingAttestation) { + // console.log('PendingAttestation: pass '); + } else if (attestation instanceof BitcoinBlockHeaderAttestation) { + found = true; + // console.log('Request to insight '); + Insight insight = new Insight("https://insight.bitpay.com/api"); + + String height = String.valueOf(((BitcoinBlockHeaderAttestation) attestation).height&0xff); + InsightResponse blockHash = insight.blockhash(height); + InsightResponse blockInfo = insight.block(blockHash.getBlockHash()); + + byte[] merkle = Utils.hexToBytes(blockInfo.getMerkleroot()); + byte[] message = Utils.arrayReverse(msg); + + // console.log('merkleroot: ' + Utils.bytesToHex(merkle)); + // console.log('msg: ' + Utils.bytesToHex(message)); + // console.log('Time: ' + (new Date(blockInfo.time * 1000))); + + // One Bitcoin attestation is enought + if (Arrays.equals(merkle, message)) { + return blockInfo.getTime(); + } else { + return ""; + } + } + } + } + if (!found) { + return ""; + } + return ""; + } + + /** Upgrade a timestamp. + * @param {byte[]} ots - The ots array buffer containing the proof to verify. + * @return {Promise} resolve(changed) : changed = True if the timestamp has changed, False otherwise. + */ + public static void upgrade(byte[] ots) { + + } +} + + diff --git a/src/PendingAttestation.java b/src/PendingAttestation.java new file mode 100644 index 0000000..b6b7fdb --- /dev/null +++ b/src/PendingAttestation.java @@ -0,0 +1,71 @@ + +/** + * Pending attestations. + * Commitment has been recorded in a remote calendar for future attestation, + * and we have a URI to find a more complete timestamp in the future. + * Nothing other than the URI is recorded, nor is there provision made to add + * extra metadata (other than the URI) in future upgrades. The rational here + * is that remote calendars promise to keep commitments indefinitely, so from + * the moment they are created it should be possible to find the commitment in + * the calendar. Thus if you're not satisfied with the local verifiability of + * a timestamp, the correct thing to do is just ask the remote calendar if + * additional attestations are available and/or when they'll be available. + * While we could additional metadata like what types of attestations the + * remote calendar expects to be able to provide in the future, that metadata + * can easily change in the future too. Given that we don't expect timestamps + * to normally have more than a small number of remote calendar attestations, + * it'd be better to have verifiers get the most recent status of such + * information (possibly with appropriate negative response caching). + * @extends TimeAttestation + */ +class PendingAttestation extends TimeAttestation { + + public static byte[] _TAG = {(byte)0x83, (byte)0xdf, (byte)0xe3, (byte)0x0d, (byte)0x2e, (byte)0xf9, (byte)0x0c, (byte)0x8e}; + + @Override + public byte[] _TAG() { + return PendingAttestation._TAG; + } + + public static int _MAX_URI_LENGTH= 1000; + + public static String _ALLOWED_URI_CHARS= "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._/:"; + + byte[] uri; + + PendingAttestation(byte[] uri_) { + super(); + this.uri = uri_; + } + + public static boolean checkUri(byte[] uri) { + if (uri.length > PendingAttestation._MAX_URI_LENGTH) { + System.err.print("URI exceeds maximum length"); + return false; + } + for (int i = 0; i < uri.length; i++) { + Character c = String.format("%c",uri[i]).charAt(0); + if (PendingAttestation._ALLOWED_URI_CHARS.indexOf(c) < 0) { + System.err.print("URI contains invalid character "); + return false; + } + } + return true; + } + + public static PendingAttestation deserialize(StreamDeserializationContext ctxPayload) { + byte[] utf8Uri = ctxPayload.readVarbytes(PendingAttestation._MAX_URI_LENGTH); + if (PendingAttestation.checkUri(utf8Uri) == false) { + System.err.print("Invalid URI: "); + return null; + } + return new PendingAttestation(utf8Uri); + } + @Override + public void serializePayload(StreamSerializationContext ctx) { + ctx.writeVarbytes(this.uri); + } + public String toString() { + return "PendingAttestation(\'" + this.uri + "\')"; + } +} diff --git a/src/StreamDeserializationContext.java b/src/StreamDeserializationContext.java new file mode 100644 index 0000000..0700ede --- /dev/null +++ b/src/StreamDeserializationContext.java @@ -0,0 +1,93 @@ +import java.lang.reflect.Array; +import java.util.Arrays; + +/** + * Created by luca on 25/02/2017. + */ +public class StreamDeserializationContext { + + byte[] buffer; + int counter=0; + + public StreamDeserializationContext(byte [] stream){ + this.buffer=stream; + this.counter=0; + } + + public byte[] getOutput() { + return this.buffer; + } + + public int getCounter() { + return this.counter; + } + + public byte[] read(int l) { + if (this.counter == this.buffer.length) { + return null; + } + if (l > this.buffer.length) { + l = this.buffer.length; + } + + // const uint8Array = new Uint8Array(this.buffer,this.counter,l); + byte[] uint8Array = Arrays.copyOfRange(this.buffer, this.counter, this.counter+l); + this.counter += l; + return uint8Array; + } + public boolean readBool() { + byte b = this.read(1)[0]; + if (b == 0xff) { + return true; + } else if (b == 0x00) { + return false; + } + return false; + } + public int readVaruint() { + int value = 0; + byte shift = 0; + byte b; + do { + b = this.read(1)[0]; + value |= (b & 0b01111111) << shift; + shift += 7; + } while ( (b & 0b10000000)==0b10000000); + return value; + } + public byte[] readBytes(int expectedLength) { + if (expectedLength == 0) { + return this.readVarbytes(1024,0); + } + return this.read(expectedLength); + } + public byte[] readVarbytes(int maxLen ) { + return readVarbytes(maxLen, 0 ); + } + public byte[] readVarbytes(int maxLen, int minLen ) { + int l = this.readVaruint(); + if ((l&0xff) > maxLen) { + System.err.println("varbytes max length exceeded;"); + return null; + } else if ((l&0xff) < minLen) { + System.err.println("varbytes min length not met;"); + return null; + } + return this.read((l&0xff)); + } + public boolean assertMagic(byte[] expectedMagic) { + byte[] actualMagic = this.read(expectedMagic.length); + + return Arrays.equals(expectedMagic, actualMagic); + } + public boolean assertEof() { + byte[] excess = this.read(1); + return excess != null; + } + + public String toString() { + return this.buffer.toString(); + } + + +} \ No newline at end of file diff --git a/src/StreamSerializationContext.java b/src/StreamSerializationContext.java new file mode 100644 index 0000000..6e2d3c7 --- /dev/null +++ b/src/StreamSerializationContext.java @@ -0,0 +1,77 @@ +import java.lang.reflect.Array; +import java.util.Arrays; + +/** + * Created by luca on 25/02/2017. + */ +public class StreamSerializationContext { + + byte[] buffer=new byte[1024*4]; + int length=0; + + public StreamSerializationContext(){ + this.buffer=new byte[1024*4]; + this.length=0; + } + + public byte[] getOutput() { + return Arrays.copyOfRange(this.buffer, 0, this.length); + } + + + public void writeBool(boolean value) { + if (value == true) { + this.writeByte((byte)0xff); + } else { + this.writeByte((byte)0x00); + } + } + + public void writeVaruint(int value) { + if (value == 0) { + this.writeByte((byte)0x00); + } else { + while (value != 0) { + byte b = (byte)(value & 0b01111111); + if (value > 0b01111111) { + b |= 0b10000000; + } + this.writeByte(b); + if (value <= 0b01111111) { + break; + } + value >>= 7; + } + } + } + + public void writeByte(byte value) { + if (this.length >= this.buffer.length) { + int newLenght = this.length * 2; + byte[] swapBuffer = new byte[newLenght]; + swapBuffer = Arrays.copyOf(this.buffer,this.length); + this.buffer = swapBuffer; + this.length = newLenght; + } + + this.buffer[this.length] = value; + this.length++; + } + + + + public void writeBytes(byte[] value) { + for (int i=0;i attestations; + HashMap ops; + + /** + * Create a Timestamp object. + * @param {string} msg - The server url. + */ + Timestamp(byte[] msg) { + this.msg = msg; + this.attestations = new ArrayList(); + this.ops = new HashMap(); + } + + /** + * Deserialize a Timestamp. + * Because the serialization format doesn't include the message that the + * timestamp operates on, you have to provide it so that the correct + * operation results can be calculated. + * The message you provide is assumed to be correct; if it causes a op to + * raise MsgValueError when the results are being calculated (done + * immediately, not lazily) DeserializationError is raised instead. + * @param {StreamDeserializationContext} ctx - The stream deserialization context. + * @param {initialMsg} initialMsg - The initial message. + * @return {Timestamp} The generated Timestamp. + */ + public static Timestamp deserialize(StreamDeserializationContext ctx, byte[] initialMsg) { + // console.log('deserialize: ', Utils.bytesToHex(initialMsg)); + Timestamp self = new Timestamp(initialMsg); + + byte tag = ctx.readBytes(1)[0]; + while (tag == 0xff) { + byte current = ctx.readBytes(1)[0]; + doTagOrAttestation(self, ctx, current, initialMsg); + tag = ctx.readBytes(1)[0]; + } + doTagOrAttestation(self, ctx, tag, initialMsg); + + return self; + } + + private static void doTagOrAttestation (Timestamp self, StreamDeserializationContext ctx, byte tag, byte[] initialMsg) { + // console.log('doTagOrAttestation: ', tag); + if (tag == 0x00) { + TimeAttestation attestation = TimeAttestation.deserialize(ctx); + self.attestations.add(attestation); + // console.log('attestation ', attestation); + } else { + Op op = Op.deserializeFromTag(ctx, tag); + + byte[] result = op.call(initialMsg); + // console.log('result: ', Utils.bytesToHex(result)); + + Timestamp stamp = Timestamp.deserialize(ctx, result); + self.ops.put(op, stamp); + } + } + + /** + * Create a Serialize object. + * @param {StreamSerializationContext} ctx - The stream serialization context. + */ + public void serialize(StreamSerializationContext ctx) { + // console.log('SERIALIZE'); + // console.log(ctx.toString()); + + // sort + List sortedAttestations = this.attestations; + if (sortedAttestations.size() > 1) { + for (int i = 0; i < sortedAttestations.size(); i++) { + ctx.writeBytes(new byte[]{(byte)0xff, (byte)0x00}); + sortedAttestations.get(i).serialize(ctx); + } + } + if (this.ops.size() == 0) { + ctx.writeByte((byte)0x00); + if (sortedAttestations.size() > 0) { + sortedAttestations.get(sortedAttestations.size() - 1).serialize(ctx); + } + } else if (this.ops.size() > 0) { + if (sortedAttestations.size() > 0) { + ctx.writeBytes(new byte[]{(byte)0xff, (byte)0x00}); + sortedAttestations.get(sortedAttestations.size() - 1).serialize(ctx); + } + + // all op/stamp + int counter = 0; + + for(Map.Entry entry : this.ops.entrySet()) { + Timestamp stamp = entry.getValue(); + Op op = entry.getKey(); + + if (counter < this.ops.size() - 1) { + ctx.writeBytes(new byte[]{(byte) 0xff}); + counter++; + } + op.serialize(ctx); + stamp.serialize(ctx); + + } + + } + } + + /** + * Add all operations and attestations from another timestamp to this one. + * @param {Timestamp} other - Initial other Timestamp to merge. + */ + void merge(Timestamp other) { + if (!(other instanceof Timestamp)) { + System.err.print("Can only merge Timestamps together"); + return; + } + if (!Arrays.equals(this.msg, other.msg)) { + System.err.print("Can\'t merge timestamps for different messages together"); + return; + } + + for (final TimeAttestation attestation : other.attestations) { + this.attestations.add(attestation); + } + + for(Map.Entry entry : other.ops.entrySet()) { + Timestamp otherOpStamp = entry.getValue(); + Op otherOp = entry.getKey(); + + Timestamp ourOpStamp = this.ops.get(otherOp); + if (ourOpStamp == null) { + ourOpStamp = new Timestamp(otherOp.call(this.msg)); + this.ops.put(otherOp, ourOpStamp); + } + ourOpStamp.merge(otherOpStamp); + } + } + + /** + * Iterate over all attestations recursively + * @return {HashMap} Returns iterable of (msg, attestation) + */ + /*allAttestations() { + const map = new Map(); + this.attestations.forEach(attestation => { + map.set(this.msg, attestation); + }); + this.ops.forEach(opStamp => { + const subMap = opStamp.allAttestations(); + subMap.forEach((b, a) => { + map.set(a, b); + }); + }); + return map; + }*/ + + /** + * Print as memory hierarchical object. + * @param {int} indent - Initial hierarchical indention. + * @return {string} The output string. + */ + public String toString(int indent) { + String output = ""; + output += Timestamp.indention(indent) + "msg: " + Utils.bytesToHex(this.msg) + "\n"; + output += Timestamp.indention(indent) + this.attestations.size() + " attestations: \n"; + int i = 0; + for (final TimeAttestation attestation : this.attestations) { + output += Timestamp.indention(indent) + "[" + i + "] " + attestation.toString() + "\n"; + i++; + } + + i = 0; + output += Timestamp.indention(indent) + this.ops.size() + " ops: \n"; + + for(Map.Entry entry : this.ops.entrySet()) { + Timestamp stamp = entry.getValue(); + Op op = entry.getKey(); + + output += Timestamp.indention(indent) + "[" + i + "] op: " + op.toString() + "\n"; + output += Timestamp.indention(indent) + "[" + i + "] timestamp: \n"; + output += stamp.toString(indent + 1); + i++; + } + + output += '\n'; + return output; + } + + /** + * Indention function for printing tree. + * @param {int} pos - Initial hierarchical indention. + * @return {string} The output space string. + */ + public static String indention(int pos) { + String output = ""; + for (int i = 0; i < pos; i++) { + output += " "; + } + return output; + } + + /** + * Print as tree hierarchical object. + * @param {int} indent - Initial hierarchical indention. + * @return {string} The output string. + */ + public String strTree(int indent) { + String output = ""; + if (this.attestations.size() > 0) { + for (final TimeAttestation attestation : this.attestations) { + output += Timestamp.indention(indent) ; + output += "verify " + attestation.toString() + '\n'; + + } + } + + if (this.ops.size() > 1) { + + for(Map.Entry entry : this.ops.entrySet()) { + Timestamp timestamp = entry.getValue(); + Op op = entry.getKey(); + output += Timestamp.indention(indent); + output += " -> "; + output += op.toString() + '\n'; + output += timestamp.strTree(indent + 1); + } + } else if (this.ops.size() > 0) { + // output += Timestamp.indention(indent); + for(Map.Entry entry : this.ops.entrySet()) { + Timestamp timestamp = entry.getValue(); + Op op = entry.getKey(); + output += Timestamp.indention(indent); + output += op.toString() + '\n'; + // output += ' ( ' + Utils.bytesToHex(this.msg) + ' ) '; + // output += '\n'; + output += timestamp.strTree(indent); + } + } + return output; + } + + /** + * Print as tree extended hierarchical object. + * @param {int} indent - Initial hierarchical indention. + * @return {string} The output string. + */ + public static String strTreeExtended(Timestamp timestamp, int indent) { + String output = ""; + + if (timestamp.attestations.size() > 0) { + for (final TimeAttestation attestation : timestamp.attestations) { + output += Timestamp.indention(indent); + output += "verify " + attestation.toString(); + output += " (" + Utils.bytesToHex(timestamp.msg) + ") "; + // output += " ["+Utils.bytesToHex(timestamp.msg)+"] "; + output += '\n'; + } + } + + if (timestamp.ops.size() > 1) { + + for(Map.Entry entry : timestamp.ops.entrySet()) { + Timestamp ts = entry.getValue(); + Op op = entry.getKey(); + output += Timestamp.indention(indent); + output += " -> "; + output += op.toString(); + output += " (" + Utils.bytesToHex(timestamp.msg) + ") "; + output += '\n'; + output += Timestamp.strTreeExtended(ts, indent + 1); + } + } else if (timestamp.ops.size() > 0) { + output += Timestamp.indention(indent); + for(Map.Entry entry : timestamp.ops.entrySet()) { + Timestamp ts = entry.getValue(); + Op op = entry.getKey(); + output += Timestamp.indention(indent); + output += op.toString(); + + output += " ( " + Utils.bytesToHex(timestamp.msg) + " ) "; + output += '\n'; + output += Timestamp.strTreeExtended(ts, indent); + } + } + return output; + } + + /** Set of al Attestations. + * @return {Array} Array of all sub timestamps with attestations. + */ + public List directlyVerified() { + if (this.attestations.size() > 0) { + List list = new ArrayList(); + list.add(this); + return list; + } + List list = new ArrayList(); + + for(Map.Entry entry : this.ops.entrySet()) { + Timestamp ts = entry.getValue(); + Op op = entry.getKey(); + + List result = ts.directlyVerified(); + list.addAll(result); + } + return list; + } + + /** Set of al Attestations. + * @return {Set} Set of all timestamp attestations. + */ + public Set getAttestations() { + Set set = new HashSet(); + for(Map.Entry item : this.allAttestations().entrySet()) { + byte[] msg = item.getKey(); + TimeAttestation attestation = item.getValue(); + set.add(attestation); + } + return set; + } + + /** Determine if timestamp is complete and can be verified. + * @return {boolean} True if the timestamp is complete, False otherwise. + */ + public Boolean isTimestampComplete() { + for(Map.Entry item : this.allAttestations().entrySet()) { + byte[] msg = item.getKey(); + TimeAttestation attestation = item.getValue(); + if (attestation instanceof BitcoinBlockHeaderAttestation) { + return true; + } + } + return false; + } + + /** + * Iterate over all attestations recursively + * @return {HashMap} Returns iterable of (msg, attestation) + */ + public HashMap allAttestations() { + HashMap map = new HashMap(); + for (TimeAttestation attestation : this.attestations ){ + map.put(this.msg, attestation); + } + for(Map.Entry entry : this.ops.entrySet()) { + Timestamp ts = entry.getValue(); + Op op = entry.getKey(); + + HashMap subMap = ts.allAttestations(); + for(Map.Entry item : subMap.entrySet()) { + byte[] msg = item.getKey(); + TimeAttestation attestation = item.getValue(); + map.put(msg,attestation); + } + } + return map; + } + +} \ No newline at end of file diff --git a/src/UnknownAttestation.java b/src/UnknownAttestation.java new file mode 100644 index 0000000..696ebf6 --- /dev/null +++ b/src/UnknownAttestation.java @@ -0,0 +1,36 @@ + +/** + * Placeholder for attestations that don't support + * @extends TimeAttestation + */ +class UnknownAttestation extends TimeAttestation { + + byte[] payload; + + public byte[] _TAG = new byte[]{}; + + @Override + public byte[] _TAG() { + return _TAG; + } + + UnknownAttestation(byte[] tag, byte[] payload) { + super(); + this._TAG = tag; + this.payload = payload; + } + + @Override + public void serializePayload(StreamSerializationContext ctx) { + ctx.writeBytes(this.payload); + } + + public static UnknownAttestation deserialize(StreamDeserializationContext ctxPayload, byte[] tag) { + byte[] payload = ctxPayload.readVarbytes(_MAX_PAYLOAD_SIZE); + return new UnknownAttestation(tag, payload); + } + + public String toString() { + return "UnknownAttestation " + this._TAG() + ' ' + this.payload; + } +} diff --git a/src/Utils.java b/src/Utils.java new file mode 100644 index 0000000..93eb008 --- /dev/null +++ b/src/Utils.java @@ -0,0 +1,58 @@ +import com.sun.tools.internal.ws.wsdl.document.jaxws.Exception; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Collections; + +/** + * Created by luca on 26/02/2017. + */ +public class Utils { + + + public static byte[] ArraysConcat(byte[] array1, byte[] array2){ + byte[] array1and2 = new byte[array1.length + array2.length]; + System.arraycopy(array1, 0, array1and2, 0, array1.length); + System.arraycopy(array2, 0, array1and2, array1.length, array2.length); + return array1and2; + } + + public static String bytesToHex(byte[]bytes){ + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02X ", b)); + } + return sb.toString(); + } + + public static byte[] randBytes(int length) throws IOException { + //Java 6 & 7: + //SecureRandom random = new SecureRandom(); + //byte[] bytes = new byte[20]; + //random.nextBytes(bytes); + + //Java 8 (even more secure): + byte[] bytes = new byte[length]; + try { + SecureRandom.getInstanceStrong().nextBytes(bytes); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + throw new IOException(); + } + return bytes; + } + + public static byte[] arrayReverse(byte[] array){ + byte[] tmp = array.clone(); + Collections.reverse(Arrays.asList(tmp)); + return tmp; + } + + public static byte[] hexToBytes(String hexString){ + byte[] yourBytes = new BigInteger(hexString, 16).toByteArray(); + return yourBytes; + } +} diff --git a/src/otscli.java b/src/otscli.java new file mode 100644 index 0000000..55dec2b --- /dev/null +++ b/src/otscli.java @@ -0,0 +1,51 @@ +/** + * Created by luca on 25/02/2017. + */ +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.Path; + +public class otscli { + + public static void main(String[] args) { + + + Path pathPlain = Paths.get("./examples/hello-world.txt"); + Path pathOts = Paths.get("./examples/hello-world.txt.ots"); + + /* INFO + Path path = Paths.get("./examples/hello-world.txt.ots"); + try { + byte[] data = Files.readAllBytes(path); + String res = OpenTimestamps.info(data); + System.out.print(res); + } catch (IOException e) { + e.printStackTrace(); + }*/ + + /* STAMP + Path path = Paths.get("./examples/hello-world.txt"); + try { + byte[] data = Files.readAllBytes(path); + byte[] ots = OpenTimestamps.stamp(data,true); + System.out.print(Utils.bytesToHex(ots)); + } catch (IOException e) { + e.printStackTrace(); + }*/ + + /* VERIFY */ + try { + byte[] bytesPlain = Files.readAllBytes(pathPlain); + byte[] bytesOts = Files.readAllBytes(pathOts); + String result = OpenTimestamps.verify(bytesOts,bytesPlain,false); + System.out.print(result); + } catch (IOException e) { + e.printStackTrace(); + } + + + + } + +}