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();
+ }
+
+
+
+ }
+
+}