Adds support for OpenTimestamps in Quartz

This commit is contained in:
Vitor Pamplona
2024-02-25 11:49:51 -05:00
parent ad03d355b0
commit c4d6b1a5d1
48 changed files with 5727 additions and 0 deletions
@@ -111,6 +111,7 @@ class EventFactory {
MetadataEvent.KIND -> MetadataEvent(id, pubKey, createdAt, tags, content, sig)
MuteListEvent.KIND -> MuteListEvent(id, pubKey, createdAt, tags, content, sig)
NNSEvent.KIND -> NNSEvent(id, pubKey, createdAt, tags, content, sig)
OtsEvent.KIND -> OtsEvent(id, pubKey, createdAt, tags, content, sig)
PeopleListEvent.KIND -> PeopleListEvent(id, pubKey, createdAt, tags, content, sig)
PinListEvent.KIND -> PinListEvent(id, pubKey, createdAt, tags, content, sig)
PollNoteEvent.KIND -> PollNoteEvent(id, pubKey, createdAt, tags, content, sig)
@@ -0,0 +1,105 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.events
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.ots.DetachedTimestampFile
import com.vitorpamplona.quartz.ots.Hash
import com.vitorpamplona.quartz.ots.OpenTimestamps
import com.vitorpamplona.quartz.ots.VerifyResult
import com.vitorpamplona.quartz.ots.op.OpSHA256
import com.vitorpamplona.quartz.signers.NostrSigner
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.coroutines.CancellationException
import java.util.Base64
@Immutable
class OtsEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
fun digestEvent() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1)
fun digest() = digestEvent()?.hexToByteArray()
fun otsByteArray(): ByteArray {
return Base64.getDecoder().decode(content)
}
fun verify(): Long? {
try {
val digest = digest() ?: return null
val detachedOts = DetachedTimestampFile.deserialize(otsByteArray())
val result = OpenTimestamps.verify(detachedOts, digest)
if (result == null || result.isEmpty()) {
return null
} else {
return result.get(VerifyResult.Chains.BITCOIN)?.timestamp
}
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("OpenTimeStamps", "Failed to verify", e)
return null
}
}
fun info(): String {
val detachedOts = DetachedTimestampFile.deserialize(otsByteArray())
return OpenTimestamps.info(detachedOts)
}
companion object {
const val KIND = 1040
const val ALT = "Opentimestamps Attestation"
fun stamp(eventId: HexKey): ByteArray {
val hash = Hash(eventId.hexToByteArray(), OpSHA256._TAG)
val file = DetachedTimestampFile.from(hash)
val timestamp = OpenTimestamps.stamp(file)
val detachedToSerialize = DetachedTimestampFile(hash.getOp(), timestamp)
return detachedToSerialize.serialize()
}
fun create(
eventId: HexKey,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (OtsEvent) -> Unit,
) {
val otsFile = stamp(eventId)
val tags =
arrayOf(
arrayOf("e", eventId),
arrayOf("alt", ALT),
)
signer.sign(createdAt, KIND, tags, Base64.getEncoder().encodeToString(otsFile), onReady)
}
}
}
@@ -0,0 +1,73 @@
package com.vitorpamplona.quartz.ots;
public class BlockHeader {
private String merkleroot;
private String blockHash;
private String time;
public void setTime(String time) {
this.time = time;
}
public Long getTime() {
return Long.valueOf(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;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
BlockHeader that = (BlockHeader) o;
if (merkleroot != null ? !merkleroot.equals(that.merkleroot) : that.merkleroot != null) {
return false;
}
if (blockHash != null ? !blockHash.equals(that.blockHash) : that.blockHash != null) {
return false;
}
return time != null ? time.equals(that.time) : that.time == null;
}
@Override
public int hashCode() {
int result = merkleroot != null ? merkleroot.hashCode() : 0;
result = 31 * result + (blockHash != null ? blockHash.hashCode() : 0);
result = 31 * result + (time != null ? time.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "BlockHeader{" +
"merkleroot='" + merkleroot + '\'' +
", blockHash='" + blockHash + '\'' +
", time='" + time + '\'' +
'}';
}
}
@@ -0,0 +1,120 @@
package com.vitorpamplona.quartz.ots;
import com.vitorpamplona.quartz.ots.exceptions.CommitmentNotFoundException;
import com.vitorpamplona.quartz.ots.exceptions.DeserializationException;
import com.vitorpamplona.quartz.ots.exceptions.ExceededSizeException;
import com.vitorpamplona.quartz.ots.exceptions.UrlException;
import com.vitorpamplona.quartz.ots.http.Request;
import com.vitorpamplona.quartz.ots.http.Response;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
/**
* Class representing remote calendar server interface.
*/
public class Calendar {
private String url;
/**
* Create a RemoteCalendar.
*
* @param url The server url.
*/
public Calendar(String url) {
this.url = url;
}
/**
* Get calendar url.
*
* @return The calendar url.
*/
public String getUrl() {
return this.url;
}
/**
* Submitting a digest to remote calendar. Returns a com.eternitywall.ots.Timestamp committing to that digest.
*
* @param digest The digest hash to send.
* @return the Timestamp received from the calendar.
* @throws ExceededSizeException if response is too big.
* @throws UrlException if url is not reachable.
* @throws DeserializationException if the data is corrupt
*/
public Timestamp submit(byte[] digest)
throws ExceededSizeException, UrlException, DeserializationException {
try {
Map<String, String> headers = new HashMap<>();
headers.put("Accept", "application/vnd.opentimestamps.v1");
headers.put("User-Agent", "java-opentimestamps");
headers.put("Content-Type", "application/x-www-form-urlencoded");
URL obj = new URL(url + "/digest");
Request task = new Request(obj);
task.setData(digest);
task.setHeaders(headers);
Response response = task.call();
byte[] body = response.getBytes();
if (body.length > 10000) {
throw new ExceededSizeException("Calendar response exceeded size limit");
}
StreamDeserializationContext ctx = new StreamDeserializationContext(body);
return Timestamp.deserialize(ctx, digest);
} catch (ExceededSizeException | DeserializationException e)
{
throw e;
}
catch (Exception e) {
throw new UrlException(e.getMessage());
}
}
/**
* Get a timestamp for a given commitment.
*
* @param commitment The digest hash to send.
* @return the Timestamp from the calendar server (with blockchain information if already written).
* @throws ExceededSizeException if response is too big.
* @throws UrlException if url is not reachable.
* @throws CommitmentNotFoundException if commit is not found.
* @throws DeserializationException if the data is corrupt
*/
public Timestamp getTimestamp(byte[] commitment) throws DeserializationException, ExceededSizeException, CommitmentNotFoundException, UrlException {
try {
Map<String, String> headers = new HashMap<>();
headers.put("Accept", "application/vnd.opentimestamps.v1");
headers.put("User-Agent", "java-opentimestamps");
headers.put("Content-Type", "application/x-www-form-urlencoded");
URL obj = new URL(url + "/timestamp/" + Utils.bytesToHex(commitment).toLowerCase());
Request task = new Request(obj);
task.setHeaders(headers);
Response response = task.call();
byte[] body = response.getBytes();
if (body.length > 10000) {
throw new ExceededSizeException("Calendar response exceeded size limit");
}
if (!response.isOk()) {
throw new CommitmentNotFoundException("com.eternitywall.ots.Calendar response a status code != 200 which is: " + response.getStatus());
}
StreamDeserializationContext ctx = new StreamDeserializationContext(body);
return Timestamp.deserialize(ctx, commitment);
}
catch (DeserializationException | ExceededSizeException | CommitmentNotFoundException e)
{
throw e;
}
catch (Exception e) {
throw new UrlException(e.getMessage());
}
}
}
@@ -0,0 +1,59 @@
package com.vitorpamplona.quartz.ots;
import android.util.Log;
import com.vitorpamplona.quartz.ots.http.Request;
import com.vitorpamplona.quartz.ots.http.Response;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
/**
* For making async calls to a calendar server
*/
public class CalendarAsyncSubmit implements Callable<Optional<Timestamp>> {
private String url;
private byte[] digest;
private BlockingQueue<Optional<Timestamp>> queue;
public CalendarAsyncSubmit(String url, byte[] digest) {
this.url = url;
this.digest = digest;
}
public void setQueue(BlockingQueue<Optional<Timestamp>> queue) {
this.queue = queue;
}
@Override
public Optional<Timestamp> call() throws Exception {
Map<String, String> headers = new HashMap<>();
headers.put("Accept", "application/vnd.opentimestamps.v1");
headers.put("User-Agent", "java-opentimestamps");
headers.put("Content-Type", "application/x-www-form-urlencoded");
URL obj = new URL(url + "/digest");
Request task = new Request(obj);
task.setData(digest);
task.setHeaders(headers);
Response response = task.call();
if (response.isOk()) {
byte[] body = response.getBytes();
StreamDeserializationContext ctx = new StreamDeserializationContext(body);
Timestamp timestamp = Timestamp.deserialize(ctx, digest);
Optional<Timestamp> of = Optional.of(timestamp);
queue.add(of);
return of;
}
queue.add(Optional.empty());
return Optional.empty();
}
}
@@ -0,0 +1,227 @@
package com.vitorpamplona.quartz.ots;
import com.vitorpamplona.quartz.ots.exceptions.DeserializationException;
import com.vitorpamplona.quartz.ots.op.Op;
import com.vitorpamplona.quartz.ots.op.OpCrypto;
import com.vitorpamplona.quartz.ots.op.OpSHA256;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.security.NoSuchAlgorithmException;
/**
* Class representing Detached com.vitorpamplona.quartz.ots.Timestamp File.
* A file containing a timestamp for another file.
* Contains a timestamp, along with a header and the digest of the file.
*/
public 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.
*
* @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.
*
* @default 1
*/
static byte MAJOR_VERSION = 1;
Op fileHashOp;
Timestamp timestamp;
public DetachedTimestampFile(Op fileHashOp, Timestamp timestamp) {
this.fileHashOp = fileHashOp;
this.timestamp = timestamp;
}
/**
* The digest of the file that was timestamped.
*
* @return The message inside the timestamp.
*/
public byte[] fileDigest() {
return this.timestamp.msg;
}
/**
* Retrieve the internal timestamp.
*
* @return the timestamp.
*/
public Timestamp getTimestamp() {
return this.timestamp;
}
/**
* Serialize a com.vitorpamplona.quartz.ots.Timestamp File.
*
* @param ctx The stream serialization context.
*/
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);
}
/**
* Serialize a com.vitorpamplona.quartz.ots.Timestamp File.
*
* @return The byte array of serialized data.
*/
public byte[] serialize() {
StreamSerializationContext ctx = new StreamSerializationContext();
this.serialize(ctx);
return ctx.getOutput();
}
/**
* Deserialize a com.vitorpamplona.quartz.ots.Timestamp File.
*
* @param ctx The stream deserialization context.
* @return The generated com.vitorpamplona.quartz.ots.DetachedTimestampFile object.
*/
public static DetachedTimestampFile deserialize(StreamDeserializationContext ctx) throws DeserializationException {
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);
}
/**
* Deserialize a com.vitorpamplona.quartz.ots.Timestamp File.
*
* @param ots The byte array of deserialization DetachedFileTimestamped.
* @return The generated com.vitorpamplona.quartz.ots.DetachedTimestampFile object.
*/
public static DetachedTimestampFile deserialize(byte[] ots) throws DeserializationException {
StreamDeserializationContext ctx = new StreamDeserializationContext(ots);
return DetachedTimestampFile.deserialize(ctx);
}
/**
* Read the Detached com.vitorpamplona.quartz.ots.Timestamp File from bytes.
*
* @param fileHashOp The file hash operation.
* @param ctx The stream deserialization context.
* @return The generated com.vitorpamplona.quartz.ots.DetachedTimestampFile object.
* @throws NoSuchAlgorithmException desc
*/
public static DetachedTimestampFile from(OpCrypto fileHashOp, StreamDeserializationContext ctx) throws NoSuchAlgorithmException {
byte[] fdHash = fileHashOp.hashFd(ctx);
return new DetachedTimestampFile(fileHashOp, new Timestamp(fdHash));
}
/**
* Read the Detached com.vitorpamplona.quartz.ots.Timestamp File from bytes.
*
* @param fileHashOp The file hash operation.
* @param bytes The byte array of data to hash
* @return The generated com.vitorpamplona.quartz.ots.DetachedTimestampFile object.
* @throws NoSuchAlgorithmException desc
*/
public static DetachedTimestampFile from(OpCrypto fileHashOp, byte[] bytes) throws Exception {
byte[] fdHash = fileHashOp.hashFd(bytes);
return new DetachedTimestampFile(fileHashOp, new Timestamp(fdHash));
}
/**
* Read the Detached com.vitorpamplona.quartz.ots.Timestamp File from hash.
*
* @param inputStream The InputStream of the file to hash
* @return The generated com.vitorpamplona.quartz.ots.DetachedTimestampFile object.
* @throws Exception if the input stream is null
*/
public static DetachedTimestampFile from(InputStream inputStream) throws Exception {
if (inputStream == null) {
throw new Exception(); // TODO: Add exception string later on
}
try {
DetachedTimestampFile fileTimestamp = DetachedTimestampFile.from(new OpSHA256(), inputStream); // Read from file reader stream
return fileTimestamp;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
throw new Exception();
}
}
/**
* Read the Detached com.vitorpamplona.quartz.ots.Timestamp File from InputStream.
*
* @param fileHashOp The file hash operation.
* @param inputStream The input stream file.
* @return The generated com.vitorpamplona.quartz.ots.DetachedTimestampFile object.
* @throws IOException desc
* @throws NoSuchAlgorithmException desc
*/
public static DetachedTimestampFile from(OpCrypto fileHashOp, InputStream inputStream) throws IOException, NoSuchAlgorithmException {
byte[] fdHash = fileHashOp.hashFd(inputStream);
return new DetachedTimestampFile(fileHashOp, new Timestamp(fdHash));
}
/**
* Read the Detached com.vitorpamplona.quartz.ots.Timestamp File from hash.
*
* @param hash The hash of the file.
* @return The generated com.vitorpamplona.quartz.ots.DetachedTimestampFile object.
*/
public static DetachedTimestampFile from(Hash hash) {
return new DetachedTimestampFile(hash.getOp(), new Timestamp(hash.getValue()));
}
/**
* Read the Detached com.vitorpamplona.quartz.ots.Timestamp File from File.
*
* @param fileHashOp The file hash operation.
* @param file The hash file.
* @return The generated com.vitorpamplona.quartz.ots.DetachedTimestampFile object.
* @throws IOException desc
* @throws NoSuchAlgorithmException desc
*/
public static DetachedTimestampFile from(OpCrypto fileHashOp, File file) throws IOException, NoSuchAlgorithmException {
byte[] fdHash = fileHashOp.hashFd(file);
return new DetachedTimestampFile(fileHashOp, new Timestamp(fdHash));
}
/**
* Print the object.
*
* @return The output.
*/
@Override
public String toString() {
String output = "com.vitorpamplona.quartz.ots.DetachedTimestampFile\n";
output += "fileHashOp: " + this.fileHashOp.toString() + '\n';
output += "timestamp: " + this.timestamp.toString() + '\n';
return output;
}
}
@@ -0,0 +1,66 @@
package com.vitorpamplona.quartz.ots;
import com.vitorpamplona.quartz.ots.http.Request;
import com.vitorpamplona.quartz.ots.http.Response;
import org.json.JSONObject;
import java.net.URL;
import java.util.concurrent.*;
import java.util.logging.Logger;
public class Esplora {
private static final Logger log = Utils.getLogger(Esplora.class.getName());
private static final String esploraUrl = "https://blockstream.info/api";
/**
* Retrieve the block information from the block hash.
*
* @param hash Hash of the block.
* @return the blockheader of the hash
* @throws Exception desc
*/
public static BlockHeader block(final String hash) throws Exception {
final URL url = new URL(esploraUrl + "/block/" + hash);
final Request task = new Request(url);
final ExecutorService executor = Executors.newSingleThreadExecutor();
final Future<Response> future = executor.submit(task);
final Response take = future.get();
executor.shutdown();
if (!take.isOk())
throw new Exception();
final JSONObject jsonObject = take.getJson();
final String merkleroot = jsonObject.getString("merkle_root");
final String time = String.valueOf(jsonObject.getInt("timestamp"));
final BlockHeader blockHeader = new BlockHeader();
blockHeader.setMerkleroot(merkleroot);
blockHeader.setTime(time);
blockHeader.setBlockHash(hash);
log.info(take.getFromUrl() + " " + blockHeader);
return blockHeader;
//log.warning("Cannot parse merkleroot from body: " + jsonObject + ": " + e.getMessage());
}
/**
* Retrieve the block hash from the block height.
*
* @param height Height of the block.
* @return the hash of the block at height height
* @throws Exception desc
*/
public static String blockHash(final Integer height) throws Exception {
final URL url = new URL(esploraUrl + "/block-height/" + height);
final Request task = new Request(url);
final ExecutorService executor = Executors.newSingleThreadExecutor();
final Future<Response> future = executor.submit(task);
final Response take = future.get();
executor.shutdown();
if (!take.isOk())
throw new Exception();
final String blockHash = take.getString();
log.info(take.getFromUrl() + " " + blockHash);
return blockHash;
}
}
@@ -0,0 +1,198 @@
package com.vitorpamplona.quartz.ots;
import com.vitorpamplona.quartz.ots.op.OpCrypto;
import com.vitorpamplona.quartz.ots.op.OpKECCAK256;
import com.vitorpamplona.quartz.ots.op.OpRIPEMD160;
import com.vitorpamplona.quartz.ots.op.OpSHA1;
import com.vitorpamplona.quartz.ots.op.OpSHA256;
import com.vitorpamplona.quartz.encoders.Hex;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.security.NoSuchAlgorithmException;
public class Hash {
private byte[] value;
private byte algorithm;
/**
* Create a Hash object.
*
* @param value - The byte array of the hash
* @param algorithm - The hashlib tag of crypto operation
*/
public Hash(byte[] value, byte algorithm) {
this.value = value;
this.algorithm = algorithm;
}
/**
* Create a Hash object.
*
* @param value - The byte array of the hash
* @param label - The hashlib name of crypto operation
*/
public Hash(byte[] value, String label) {
this.value = value;
this.algorithm = getOp(label)._TAG();
}
/**
* Get Value.
*
* @return value - The hash in byte array.
*/
public byte[] getValue() {
return value;
}
/**
* Set Value tag.
*
* @param value - The hash in byte array.
*/
public void setValue(byte[] value) {
this.value = value;
}
/**
* Get Algorithm tag.
*
* @return algorithm - The algorithm tag of crypto operation.
*/
public byte getAlgorithm() {
return algorithm;
}
/**
* Set Algorithm tag.
*
* @param algorithm - The algorithm tag of crypto operation.
*/
public void setAlgorithm(byte algorithm) {
this.algorithm = algorithm;
}
/**
* Get the current Crypto operation.
*
* @return The generated com.vitorpamplona.quartz.ots.OpCrypto object.
*/
public OpCrypto getOp() {
if (this.algorithm == OpSHA1._TAG) {
return new OpSHA1();
} else if (this.algorithm == OpSHA256._TAG) {
return new OpSHA256();
} else if (this.algorithm == OpRIPEMD160._TAG) {
return new OpRIPEMD160();
} else if (this.algorithm == OpKECCAK256._TAG) {
return new OpKECCAK256();
}
return new OpSHA256();
}
/**
* Get Crypto operation from hashlib tag.
*
* @param algorithm The hashlib tag.
* @return The generated com.vitorpamplona.quartz.ots.OpCrypto object.
*/
public static OpCrypto getOp(byte algorithm) {
if (algorithm == OpSHA1._TAG) {
return new OpSHA1();
} else if (algorithm == OpSHA256._TAG) {
return new OpSHA256();
} else if (algorithm == OpRIPEMD160._TAG) {
return new OpRIPEMD160();
} else if (algorithm == OpKECCAK256._TAG) {
return new OpKECCAK256();
}
return new OpSHA256();
}
/**
* Get Crypto operation from hashlib name.
*
* @param label The hashlib name.
* @return The generated com.vitorpamplona.quartz.ots.OpCrypto object.
*/
public static OpCrypto getOp(String label) {
if (label.toLowerCase().equals(new OpSHA1()._TAG_NAME())) {
return new OpSHA1();
} else if (label.toLowerCase().equals(new OpSHA256()._TAG_NAME())) {
return new OpSHA256();
} else if (label.toLowerCase().equals(new OpRIPEMD160()._TAG_NAME())) {
return new OpRIPEMD160();
} else if (label.toLowerCase().equals(new OpKECCAK256()._TAG_NAME())) {
return new OpKECCAK256();
}
return new OpSHA256();
}
/**
* Build hash from data.
*
* @param bytes The byte array of data to hash.
* @param algorithm The hash file.
* @return The generated com.vitorpamplona.quartz.ots.Hash object.
* @throws IOException desc
* @throws NoSuchAlgorithmException desc
*/
public static Hash from(byte[] bytes, byte algorithm) throws IOException, NoSuchAlgorithmException {
OpCrypto opCrypto = getOp(algorithm);
byte[] value = opCrypto.hashFd(bytes);
return new Hash(value, algorithm);
}
/**
* Build hash from File.
*
* @param file The File of data to hash.
* @param algorithm The hash file.
* @return The generated com.vitorpamplona.quartz.ots.Hash object.
* @throws IOException desc
* @throws NoSuchAlgorithmException desc
*/
public static Hash from(File file, byte algorithm) throws IOException, NoSuchAlgorithmException {
OpCrypto opCrypto = getOp(algorithm);
byte[] value = opCrypto.hashFd(file);
return new Hash(value, algorithm);
}
/**
* Build hash from InputStream.
*
* @param inputStream The InputStream of data to hash.
* @param algorithm The hash file.
* @return The generated com.vitorpamplona.quartz.ots.Hash object.
* @throws IOException desc
* @throws NoSuchAlgorithmException desc
*/
public static Hash from(InputStream inputStream, byte algorithm) throws IOException, NoSuchAlgorithmException {
OpCrypto opCrypto = getOp(algorithm);
byte[] value = opCrypto.hashFd(inputStream);
return new Hash(value, algorithm);
}
/**
* Print the object.
*
* @return The output.
*/
@Override
public String toString() {
String output = "com.vitorpamplona.quartz.ots.Hash\n";
output += "algorithm: " + this.getOp()._HASHLIB_NAME() + '\n';
output += "value: " + Hex.encode(this.value) + '\n';
return output;
}
}
@@ -0,0 +1,95 @@
package com.vitorpamplona.quartz.ots;
import com.vitorpamplona.quartz.ots.op.OpAppend;
import com.vitorpamplona.quartz.ots.op.OpPrepend;
import com.vitorpamplona.quartz.ots.op.OpSHA256;
import java.util.ArrayList;
import java.util.List;
/**
* Utility functions for merkle trees
*/
public class Merkle {
/**
* Concatenate left and right, then perform a unary operation on them left and right can be either timestamps or bytes.
* Appropriate intermediary append/prepend operations will be created as needed for left and right.
*
* @param left the left timestamp parameter
* @param right the right timestamp parameter
* @return the concatenation of left and right
*/
public static Timestamp catThenUnaryOp(Timestamp left, Timestamp right) {
// rightPrependStamp = right.ops.add(OpPrepend(left.msg))
Timestamp rightPrependStamp = right.add(new OpPrepend(left.msg));
// Left and right should produce the same thing, so we can set the timestamp of the left to the right.
// left.ops[OpAppend(right.msg)] = right_prepend_stamp
// leftAppendStamp = left.ops.add(OpAppend(right.msg))
//Timestamp leftPrependStamp = left.add(new OpAppend(right.msg));
left.ops.put(new OpAppend(right.msg), rightPrependStamp);
// return rightPrependStamp.ops.add(unaryOpCls())
Timestamp res = rightPrependStamp.add(new OpSHA256());
return res;
}
public static Timestamp catSha256(Timestamp left, Timestamp right) {
return Merkle.catThenUnaryOp(left, right);
}
public static Timestamp catSha256d(Timestamp left, Timestamp right) {
Timestamp sha256Timestamp = Merkle.catSha256(left, right);
// res = sha256Timestamp.ops.add(OpSHA256());
Timestamp res = sha256Timestamp.add(new OpSHA256());
return res;
}
/**
* Merkelize a set of timestamps.
* A merkle tree of all the timestamps is built in-place using binop() to
* timestamp each pair of timestamps. The exact algorithm used is structurally
* identical to a merkle-mountain-range, although leaf sums aren't committed.
* As this function is under the consensus-critical core, it's guaranteed that
* the algorithm will not be changed in the future.
*
* @param timestamps a list of timestamps
* @return the timestamp for the tip of the tree.
*/
public static Timestamp makeMerkleTree(List<Timestamp> timestamps) {
List<Timestamp> stamps = timestamps;
Timestamp prevStamp = null;
boolean exit = false;
while (!exit) {
if (stamps.size() > 0) {
prevStamp = stamps.get(0);
}
List<Timestamp> subStamps = stamps.subList(1, stamps.size());
List<Timestamp> nextStamps = new ArrayList<>();
for (Timestamp stamp : subStamps) {
if (prevStamp == null) {
prevStamp = stamp;
} else {
nextStamps.add(Merkle.catSha256(prevStamp, stamp));
prevStamp = null;
}
}
if (nextStamps.size() == 0) {
exit = true;
} else {
if (prevStamp != null) {
nextStamps.add(prevStamp);
}
stamps = nextStamps;
}
}
return prevStamp;
}
}
@@ -0,0 +1,475 @@
package com.vitorpamplona.quartz.ots;
import android.util.Log;
import com.vitorpamplona.quartz.ots.attestation.BitcoinBlockHeaderAttestation;
import com.vitorpamplona.quartz.ots.attestation.EthereumBlockHeaderAttestation;
import com.vitorpamplona.quartz.ots.attestation.LitecoinBlockHeaderAttestation;
import com.vitorpamplona.quartz.ots.attestation.PendingAttestation;
import com.vitorpamplona.quartz.ots.attestation.TimeAttestation;
import com.vitorpamplona.quartz.encoders.Hex;
import com.vitorpamplona.quartz.ots.exceptions.VerificationException;
import com.vitorpamplona.quartz.ots.op.OpAppend;
import com.vitorpamplona.quartz.ots.op.OpCrypto;
import com.vitorpamplona.quartz.ots.op.OpSHA256;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* The main class for timestamp operations.
*/
public class OpenTimestamps {
/**
* Show information on a detached timestamp.
*
* @param detachedTimestampFile The DetachedTimestampFile ots.
* @return the string representation of the timestamp.
*/
public static String info(DetachedTimestampFile detachedTimestampFile) {
return info(detachedTimestampFile, false);
}
/**
* Show information on a detached timestamp with verbose option.
*
* @param detachedTimestampFile The DetachedTimestampFile ots.
* @param verbose Show verbose output.
* @return the string representation of the timestamp.
*/
public static String info(DetachedTimestampFile detachedTimestampFile, boolean verbose) {
if (detachedTimestampFile == null) {
return "No ots file";
}
String fileHash = Utils.bytesToHex(detachedTimestampFile.timestamp.msg).toLowerCase();
String hashOp = ((OpCrypto) detachedTimestampFile.fileHashOp)._TAG_NAME();
String firstLine = "File " + hashOp + " hash: " + fileHash + '\n';
return firstLine + "Timestamp:\n" + detachedTimestampFile.timestamp.strTree(0, verbose);
}
/**
* Show information on a timestamp.
*
* @param timestamp The timestamp buffered.
* @return the string representation of the timestamp.
*/
public static String info(Timestamp timestamp) {
if (timestamp == null) {
return "No timestamp";
}
String fileHash = Utils.bytesToHex(timestamp.msg).toLowerCase();
String firstLine = "Hash: " + fileHash + '\n';
return firstLine + "Timestamp:\n" + timestamp.strTree(0);
}
/**
* Create timestamp with the aid of a remote calendar. May be specified multiple times.
*
* @param fileTimestamp The Detached Timestamp File.
* @return The plain array buffer of stamped.
* @throws IOException if fileTimestamp is not valid, or the stamp procedure fails.
*/
public static Timestamp stamp(DetachedTimestampFile fileTimestamp) throws IOException {
return OpenTimestamps.stamp(fileTimestamp, null, 0);
}
/**
* Create timestamp with the aid of a remote calendar. May be specified multiple times.
*
* @param fileTimestamp The timestamp to stamp.
* @param calendarsUrl The list of calendar urls.
* @param m The number of calendar to use.
* @return The plain array buffer of stamped.
* @throws IOException if fileTimestamp is not valid, or the stamp procedure fails.
*/
public static Timestamp stamp(DetachedTimestampFile fileTimestamp, List<String> calendarsUrl, Integer m) throws IOException {
List<DetachedTimestampFile> fileTimestamps = new ArrayList<DetachedTimestampFile>();
fileTimestamps.add(fileTimestamp);
return OpenTimestamps.stamp(fileTimestamps, calendarsUrl, m);
}
/**
* Create timestamp with the aid of a remote calendar. May be specified multiple times.
*
* @param fileTimestamps The list of timestamp to stamp.
* @param calendarsUrl The list of calendar urls.
* @param m The number of calendar to use.
* @return The plain array buffer of stamped.
* @throws IOException if fileTimestamp is not valid, or the stamp procedure fails.
*/
public static Timestamp stamp(List<DetachedTimestampFile> fileTimestamps, List<String> calendarsUrl, Integer m) throws IOException {
// Parse parameters
if (fileTimestamps == null || fileTimestamps.size() == 0) {
throw new IOException();
}
if (calendarsUrl == null || calendarsUrl.size() == 0) {
calendarsUrl = new ArrayList<String>();
calendarsUrl.add("https://alice.btc.calendar.opentimestamps.org");
calendarsUrl.add("https://bob.btc.calendar.opentimestamps.org");
calendarsUrl.add("https://finney.calendar.eternitywall.com");
}
if (m == null || m <= 0) {
if (calendarsUrl.size() == 0) {
m = 2;
} else if (calendarsUrl.size() == 1) {
m = 1;
} else {
m = calendarsUrl.size();
}
}
if (m < 0 || m > calendarsUrl.size()) {
Log.e("OpenTimestamp", "m cannot be greater than available calendar neither less or equal 0");
throw new IOException();
}
// Build markle tree
Timestamp merkleTip = OpenTimestamps.makeMerkleTree(fileTimestamps);
if (merkleTip == null) {
throw new IOException();
}
// Stamping
Timestamp resultTimestamp = OpenTimestamps.create(merkleTip, calendarsUrl, m);
if (resultTimestamp == null) {
throw new IOException();
}
// Result of timestamp serialization
if (fileTimestamps.size() == 1) {
return fileTimestamps.get(0).timestamp;
} else {
return merkleTip;
}
}
/**
* Create a timestamp
*
* @param timestamp The timestamp.
* @param calendarUrls List of calendar's to use.
* @param m Number of calendars to use.
* @return The created timestamp.
*/
private static Timestamp create(Timestamp timestamp, List<String> calendarUrls, Integer m) {
int capacity = calendarUrls.size();
ExecutorService executor = Executors.newFixedThreadPool(4);
ArrayBlockingQueue<Optional<Timestamp>> queue = new ArrayBlockingQueue<>(capacity);
// Submit to all public calendars
for (final String calendarUrl : calendarUrls) {
Log.i("OpenTimestamps", "Submitting to remote calendar " + calendarUrl);
try {
CalendarAsyncSubmit task = new CalendarAsyncSubmit(calendarUrl, timestamp.msg);
task.setQueue(queue);
executor.submit(task);
} catch (Exception e) {
e.printStackTrace();
}
}
int count = 0;
for (count = 0; count < capacity && count < m; count++) {
try {
Optional<Timestamp> optionalStamp = queue.take();
if (optionalStamp.isPresent()) {
try {
Timestamp time = optionalStamp.get();
timestamp.merge(time);
Log.i("Open", ""+ timestamp.attestations.size());
} catch (Exception e) {
e.printStackTrace();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
if (count < m) {
Log.e("OpenTimestamp", "Failed to create timestamp: requested " + String.valueOf(m) + " attestation" + ((m > 1) ? "s" : "") + " but received only " + String.valueOf(count));
}
//shut down the executor service now
executor.shutdown();
return timestamp;
}
/**
* Make Merkle Tree of detached timestamps.
*
* @param fileTimestamps The list of DetachedTimestampFile.
* @return merkle tip timestamp.
*/
public static Timestamp makeMerkleTree(List<DetachedTimestampFile> fileTimestamps) {
List<Timestamp> merkleRoots = new ArrayList<>();
for (DetachedTimestampFile fileTimestamp : fileTimestamps) {
byte[] bytesRandom16 = new byte[16];
try {
bytesRandom16 = Utils.randBytes(16);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
// nonce_appended_stamp = file_timestamp.timestamp.ops.add(com.vitorpamplona.quartz.ots.op.OpAppend(os.urandom(16)))
Timestamp nonceAppendedStamp = fileTimestamp.timestamp.add(new OpAppend(bytesRandom16));
// merkle_root = nonce_appended_stamp.ops.add(com.vitorpamplona.quartz.ots.op.OpSHA256())
Timestamp merkleRoot = nonceAppendedStamp.add(new OpSHA256());
merkleRoots.add(merkleRoot);
}
Timestamp merkleTip = Merkle.makeMerkleTree(merkleRoots);
return merkleTip;
}
/**
* Compare and verify a detached timestamp.
*
* @param ots The DetachedTimestampFile containing the proof to verify.
* @param diggest The hash of the stamped file, in bytes
* @return Hashmap of block heights and timestamps indexed by chain: timestamp in seconds from 1 January 1970.
* @throws Exception if the verification procedure fails.
*/
public static HashMap<VerifyResult.Chains, VerifyResult> verify(DetachedTimestampFile ots, byte[] diggest) throws Exception {
if (!Arrays.equals(ots.fileDigest(), diggest)) {
Log.e("OpenTimestamp", "Expected digest " + Hex.encode(ots.fileDigest()).toLowerCase());
Log.e("OpenTimestamp", "File does not match original!");
throw new Exception("File does not match original!");
}
return OpenTimestamps.verify(ots.timestamp);
}
/**
* Verify a timestamp.
*
* @param timestamp The timestamp.
* @return HashMap of block heights and timestamps indexed by chain: timestamp in seconds from 1 January 1970.
* @throws Exception if the verification procedure fails.
*/
public static HashMap<VerifyResult.Chains, VerifyResult> verify(Timestamp timestamp) throws Exception {
HashMap<VerifyResult.Chains, VerifyResult> verifyResults = new HashMap<>();
for (Map.Entry<byte[], TimeAttestation> item : timestamp.allAttestations().entrySet()) {
byte[] msg = item.getKey();
TimeAttestation attestation = item.getValue();
VerifyResult verifyResult = null;
VerifyResult.Chains chain = null;
try {
if (attestation instanceof BitcoinBlockHeaderAttestation) {
chain = VerifyResult.Chains.BITCOIN;
Long time = verify((BitcoinBlockHeaderAttestation) attestation, msg);
int height = ((BitcoinBlockHeaderAttestation) attestation).getHeight();
verifyResult = new VerifyResult(time, height);
} else if (attestation instanceof LitecoinBlockHeaderAttestation) {
chain = VerifyResult.Chains.LITECOIN;
Long time = verify((LitecoinBlockHeaderAttestation) attestation, msg);
int height = ((LitecoinBlockHeaderAttestation) attestation).getHeight();
verifyResult = new VerifyResult(time, height);
}
if (verifyResult != null && verifyResults.containsKey(chain)) {
if (verifyResult.height < verifyResults.get(chain).height) {
verifyResults.put(chain, verifyResult);
}
}
if (verifyResult != null && !verifyResults.containsKey(chain)) {
verifyResults.put(chain, verifyResult);
}
} catch (VerificationException e) {
throw e;
} catch (Exception e) {
String text = "";
if (chain == VerifyResult.Chains.BITCOIN) {
text = BitcoinBlockHeaderAttestation.chain;
} else if (chain == VerifyResult.Chains.LITECOIN) {
text = LitecoinBlockHeaderAttestation.chain;
} else if (chain == VerifyResult.Chains.ETHEREUM) {
text = EthereumBlockHeaderAttestation.chain;
} else {
throw e;
}
Log.e("OpenTimestamp", Utils.toUpperFirstLetter(text) + " verification failed: " + e.getMessage());
throw e;
}
}
return verifyResults;
}
/**
* Verify an Bitcoin Block Header Attestation.
* if the node is not reachable or it fails, uses Lite-client verification.
*
* @param attestation The BitcoinBlockHeaderAttestation attestation.
* @param msg The digest to verify.
* @return The unix timestamp in seconds from 1 January 1970.
* @throws VerificationException if it doesn't check the merkle root of the block.
* @throws Exception if the verification procedure fails.
*/
public static Long verify(BitcoinBlockHeaderAttestation attestation, byte[] msg) throws VerificationException, Exception {
Integer height = attestation.getHeight();
BlockHeader blockInfo;
try {
String blockHash = Esplora.blockHash(height);
blockInfo = Esplora.block(blockHash);
Log.i("OpenTimestamps", "Lite-client verification, assuming block " + blockHash + " is valid");
} catch (Exception e2) {
e2.printStackTrace();
throw e2;
}
return attestation.verifyAgainstBlockheader(Utils.arrayReverse(msg), blockInfo);
}
/**
* Verify an Litecoin Block Header Attestation. Litecoin verification uses only lite-client verification.
*
* @param attestation The LitecoinBlockHeaderAttestation attestation.
* @param msg The digest to verify.
* @return The unix timestamp in seconds from 1 January 1970.
* @throws VerificationException if it doesn't check the merkle root of the block.
* @throws Exception if the verification procedure fails.
*/
public static Long verify(LitecoinBlockHeaderAttestation attestation, byte[] msg) throws VerificationException, Exception {
Integer height = attestation.getHeight();
BlockHeader blockInfo;
try {
String blockHash = Esplora.blockHash(height);
blockInfo = Esplora.block(blockHash);
Log.i("OpenTimestamps", "Lite-client verification, assuming block " + blockHash + " is valid");
} catch (Exception e2) {
e2.printStackTrace();
throw e2;
}
return attestation.verifyAgainstBlockheader(Utils.arrayReverse(msg), blockInfo);
}
/**
* Upgrade a timestamp.
*
* @param detachedTimestamp The DetachedTimestampFile containing the proof to verify.
* @return a boolean representing if the timestamp has changed.
* @throws Exception if the upgrading procedure fails.
*/
public static boolean upgrade(DetachedTimestampFile detachedTimestamp) throws Exception {
// Upgrade timestamp
boolean changed = OpenTimestamps.upgrade(detachedTimestamp.timestamp);
return changed;
}
/**
* Attempt to upgrade an incomplete timestamp to make it verifiable.
* Note that this means if the timestamp that is already complete, False will be returned as nothing has changed.
*
* @param timestamp The timestamp to upgrade.
* @return a boolean representing if the timestamp has changed.
* @throws Exception if the upgrading procedure fails.
*/
public static boolean upgrade(Timestamp timestamp) throws Exception {
// Check remote calendars for upgrades.
// This time we only check PendingAttestations - we can't be as agressive.
boolean upgraded = false;
Set<TimeAttestation> existingAttestations = timestamp.getAttestations();
for (Timestamp subStamp : timestamp.directlyVerified()) {
for (TimeAttestation attestation : subStamp.attestations) {
if (attestation instanceof PendingAttestation && !subStamp.isTimestampComplete()) {
String calendarUrl = new String(((PendingAttestation) attestation).getUri(), StandardCharsets.UTF_8);
// var calendarUrl = calendarUrls[0];
byte[] commitment = subStamp.msg;
try {
Calendar calendar = new Calendar(calendarUrl);
Timestamp upgradedStamp = OpenTimestamps.upgrade(subStamp, calendar, commitment, existingAttestations);
try {
subStamp.merge(upgradedStamp);
} catch (Exception e) {
e.printStackTrace();
}
upgraded = true;
} catch (Exception e) {
Log.i("OpenTimestamps", e.getMessage());
}
}
}
}
return upgraded;
}
private static Timestamp upgrade(Timestamp subStamp, Calendar calendar, byte[] commitment, Set<TimeAttestation> existingAttestations) throws Exception {
Timestamp upgradedStamp;
try {
upgradedStamp = calendar.getTimestamp(commitment);
if (upgradedStamp == null) {
throw new Exception("Invalid stamp");
}
} catch (Exception e) {
Log.i("OpenTimestamps", "Calendar " + calendar.getUrl() + ": " + e.getMessage());
throw e;
}
Set<TimeAttestation> attsFromRemote = upgradedStamp.getAttestations();
if (attsFromRemote.size() > 0) {
Log.i("OpenTimestamps", "Got 1 attestation(s) from " + calendar.getUrl());
}
// Set difference from remote attestations & existing attestations
Set<TimeAttestation> newAttestations = attsFromRemote;
newAttestations.removeAll(existingAttestations);
// changed & found_new_attestations
// foundNewAttestations = true;
// Log.i("OpenTimestamps", attsFromRemote.size + ' attestation(s) from ' + calendar.url);
// Set union of existingAttestations & newAttestations
existingAttestations.addAll(newAttestations);
return upgradedStamp;
// subStamp.merge(upgradedStamp);
// args.cache.merge(upgraded_stamp)
// sub_stamp.merge(upgraded_stamp)
}
}
@@ -0,0 +1,17 @@
This code came from https://github.com/opentimestamps/java-opentimestamps
And includes modifications to avoid dependencies that do not work on Android
Original License
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.
@@ -0,0 +1,107 @@
package com.vitorpamplona.quartz.ots;
import com.vitorpamplona.quartz.ots.exceptions.DeserializationException;
import java.util.Arrays;
import java.util.logging.Logger;
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.counter > this.buffer.length) {
l = this.buffer.length-this.counter;
}
// 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) != 0b00000000);
return value;
}
public byte[] readBytes(int expectedLength) throws DeserializationException {
if (expectedLength == 0) {
return this.readVarbytes(1024, 0);
}
return this.read(expectedLength);
}
public byte[] readVarbytes(int maxLen) throws DeserializationException {
return readVarbytes(maxLen, 0);
}
public byte[] readVarbytes(int maxLen, int minLen) throws DeserializationException {
int l = this.readVaruint();
if (l > maxLen) {
throw new DeserializationException("varbytes max length exceeded;");
} else if (l < minLen) {
throw new DeserializationException("varbytes min length not met;");
}
return this.read(l);
}
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 Arrays.toString(this.buffer);
}
}
@@ -0,0 +1,82 @@
package com.vitorpamplona.quartz.ots;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;
public class StreamSerializationContext {
List<Byte> buffer = new ArrayList<>();
public StreamSerializationContext() {
this.buffer = new ArrayList<>();
}
public byte[] getOutput() {
byte[] bytes = new byte[this.buffer.size()];
for (int i = 0; i < this.buffer.size(); i++) {
bytes[i] = this.buffer.get(i);
}
return bytes;
}
public int getLength() {
return this.buffer.size();
}
public void writeBool(boolean value) {
if (value == true) {
this.writeByte((byte) 0xff);
} else {
this.writeByte((byte) 0x00);
}
}
public void writeVaruint(int value) {
if ((value) == 0b00000000) {
this.writeByte((byte) 0x00);
} else {
while (value != 0) {
byte b = (byte) ((value & 0xff) & 0b01111111);
if ((value) > 0b01111111) {
b |= 0b10000000;
}
this.writeByte(b);
if ((value) <= 0b01111111) {
break;
}
value = value >> 7;
}
}
}
public void writeByte(byte value) {
this.buffer.add(new Byte(value));
}
public void writeByte(Byte value) {
this.buffer.add(value);
}
public void writeBytes(byte[] value) {
for (int i = 0; i < value.length; i++) {
this.writeByte(value[i]);
}
}
public void writeVarbytes(byte[] value) {
this.writeVaruint(value.length);
this.writeBytes(value);
}
public String toString() {
return Arrays.toString(this.getOutput());
}
}
@@ -0,0 +1,617 @@
package com.vitorpamplona.quartz.ots;
import com.vitorpamplona.quartz.ots.attestation.BitcoinBlockHeaderAttestation;
import com.vitorpamplona.quartz.ots.attestation.TimeAttestation;
import com.vitorpamplona.quartz.encoders.Hex;
import com.vitorpamplona.quartz.ots.exceptions.DeserializationException;
import com.vitorpamplona.quartz.ots.op.Op;
import com.vitorpamplona.quartz.ots.op.OpBinary;
import com.vitorpamplona.quartz.ots.op.OpSHA256;
import java.util.*;
import java.util.Map.Entry;
/**
* Proof that one or more attestations commit to a message.
* The proof is in the form of a tree, with each node being a message, and the
* edges being operations acting on those messages. The leafs of the tree are
* attestations that attest to the time that messages in the tree existed prior.
*/
public class Timestamp {
public byte[] msg;
public List<TimeAttestation> attestations;
public HashMap<Op, Timestamp> ops;
/**
* Create a com.vitorpamplona.quartz.ots.Timestamp object.
*
* @param msg - Desc
*/
public Timestamp(byte[] msg) {
this.msg = msg;
this.attestations = new ArrayList<>();
this.ops = new HashMap<>();
}
/**
* Deserialize a Timestamp.
*
* @param ots - The serialized byte array.
* @param initialMsg - The initial message.
* @return The deserialized Timestamp.
*/
public static Timestamp deserialize(byte[] ots, byte[] initialMsg) throws DeserializationException {
StreamDeserializationContext ctx = new StreamDeserializationContext(ots);
return Timestamp.deserialize(ctx, initialMsg);
}
/**
* 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 ctx - The stream deserialization context.
* @param initialMsg - The initial message.
* @return The deserialized Timestamp.
*/
public static Timestamp deserialize(StreamDeserializationContext ctx, byte[] initialMsg)
throws DeserializationException {
Timestamp self = new Timestamp(initialMsg);
byte tag = ctx.readBytes(1)[0];
while ((tag & 0xff) == 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)
throws DeserializationException {
if ((tag & 0xff) == 0x00) {
TimeAttestation attestation = TimeAttestation.deserialize(ctx);
self.attestations.add(attestation);
} else {
Op op = Op.deserializeFromTag(ctx, tag);
byte[] result = op.call(initialMsg);
Timestamp stamp = Timestamp.deserialize(ctx, result);
self.ops.put(op, stamp);
}
}
/**
* Create a Serialize object.
*
* @return The byte array of the serialized timestamp
*/
public byte[] serialize() {
StreamSerializationContext ctx = new StreamSerializationContext();
serialize(ctx);
return ctx.getOutput();
}
/**
* Create a Serialize object.
*
* @param ctx - The stream serialization context.
*/
public void serialize(StreamSerializationContext ctx) {
List<TimeAttestation> sortedAttestations = this.attestations; // TODO: Hm, this is just a reference copy...
Collections.sort(sortedAttestations);
if (sortedAttestations.size() > 1) {
for (int i = 0; i < sortedAttestations.size() - 1; i++) {
ctx.writeBytes(new byte[]{(byte) 0xff, (byte) 0x00});
sortedAttestations.get(i).serialize(ctx);
}
}
if (this.ops.isEmpty()) {
ctx.writeByte((byte) 0x00);
if (!sortedAttestations.isEmpty()) {
sortedAttestations.get(sortedAttestations.size() - 1).serialize(ctx);
}
} else if (!this.ops.isEmpty()) {
if (!sortedAttestations.isEmpty()) {
ctx.writeBytes(new byte[]{(byte) 0xff, (byte) 0x00});
sortedAttestations.get(sortedAttestations.size() - 1).serialize(ctx);
}
int counter = 0;
List<Map.Entry<Op, Timestamp>> list = sortToList(this.ops.entrySet());
for (Map.Entry<Op, Timestamp> entry : list) {
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 other - Initial other com.vitorpamplona.quartz.ots.Timestamp to merge.
* @throws Exception different timestamps messages
*/
public void merge(Timestamp other) throws Exception {
if (!Arrays.equals(this.msg, other.msg)) {
//Log.e("OpenTimestamp", "Can\'t merge timestamps for different messages together");
throw new Exception("Can\'t merge timestamps for different messages together");
}
for (final TimeAttestation attestation : other.attestations) {
this.attestations.add(attestation);
}
for (Map.Entry<Op, Timestamp> 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);
}
}
/**
* Shrink Timestamp.
* Remove useless pending attestions if exist a full bitcoin attestation.
*
* @return TimeAttestation - the minimal attestation.
* @throws Exception no attestion founds.
*/
public TimeAttestation shrink() throws Exception {
// Get all attestations
HashMap<byte[], TimeAttestation> allAttestations = this.allAttestations();
if (allAttestations.size() == 0) {
throw new Exception();
} else if (allAttestations.size() == 1) {
return allAttestations.values().iterator().next();
} else if (this.ops.size() == 0) {
throw new Exception(); // TODO: Need a descriptive exception string here
}
// Fore >1 attestations :
// Search first BitcoinBlockHeaderAttestation
TimeAttestation minAttestation = null;
for (Map.Entry<Op, Timestamp> entry : this.ops.entrySet()) {
Timestamp timestamp = entry.getValue();
//Op op = entry.getKey();
for (TimeAttestation attestation : timestamp.getAttestations()) {
if (attestation instanceof BitcoinBlockHeaderAttestation) {
if (minAttestation == null) {
minAttestation = attestation;
} else {
if (minAttestation instanceof BitcoinBlockHeaderAttestation
&& attestation instanceof BitcoinBlockHeaderAttestation
&& ((BitcoinBlockHeaderAttestation) minAttestation).getHeight()
> ((BitcoinBlockHeaderAttestation) attestation).getHeight()) {
minAttestation = attestation;
}
}
}
}
}
// Only pending attestations : return the first
if (minAttestation == null) {
return allAttestations.values().iterator().next();
}
// Remove attestation if not min attestation
boolean shrinked = false;
for (Iterator<Entry<Op, Timestamp>> it = this.ops.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<Op, Timestamp> entry = it.next();
Timestamp timestamp = entry.getValue();
Op op = entry.getKey();
Set<TimeAttestation> attestations = timestamp.getAttestations();
if (attestations.size() > 0 && attestations.contains(minAttestation) && shrinked == false) {
timestamp.shrink();
shrinked = true;
} else {
it.remove();
}
}
return minAttestation;
}
/**
* Return the digest of the timestamp.
*
* @return The byte[] digest string.
*/
public byte[] getDigest() {
return this.msg;
}
/**
* Return as memory hierarchical object.
*
* @param indent - Initial hierarchical indention.
* @return The output string.
*/
public String toString(int indent) {
StringBuilder builder = new StringBuilder();
builder.append(Timestamp.indention(indent) + "msg: " + Hex.encode(this.msg) + "\n");
builder.append(Timestamp.indention(indent) + this.attestations.size() + " attestations: \n");
int i = 0;
for (final TimeAttestation attestation : this.attestations) {
builder.append(Timestamp.indention(indent) + "[" + i + "] " + attestation.toString() + "\n");
i++;
}
i = 0;
builder.append(Timestamp.indention(indent) + this.ops.size() + " ops: \n");
for (Map.Entry<Op, Timestamp> entry : this.ops.entrySet()) {
Timestamp stamp = entry.getValue();
Op op = entry.getKey();
builder.append(Timestamp.indention(indent) + "[" + i + "] op: " + op.toString() + "\n");
builder.append(Timestamp.indention(indent) + "[" + i + "] timestamp: \n");
builder.append(stamp.toString(indent + 1));
i++;
}
builder.append('\n');
return builder.toString();
}
/**
* Indention function for printing tree.
*
* @param pos - Initial hierarchical indention.
* @return The output space string.
*/
public static String indention(int pos) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < pos; i++) {
builder.append(" ");
}
return builder.toString();
}
public String strTree(int indent) {
return strTree(indent, false);
}
private String strResult(boolean verbosity, byte[] parameter, byte[] result) {
final String ANSI_HEADER = "\u001B[95m";
final String ANSI_OKBLUE = "\u001B[94m";
final String ANSI_OKGREEN = "\u001B[92m";
final String ANSI_WARNING = "\u001B[93m";
final String ANSI_FAIL = "\u001B[91m";
final String ANSI_ENDC = "\u001B[0m";
final String ANSI_BOLD = "\u001B[1m";
final String ANSI_UNDERLINE = "\u001B[4m";
String rr = "";
if (verbosity == true && result != null) {
rr += " == ";
String resultHex = Utils.bytesToHex(result);
if (parameter == null) {
rr += resultHex;
} else {
String parameterHex = Utils.bytesToHex(parameter);
try {
int index = resultHex.indexOf(parameterHex);
String parameterHexHighlight = ANSI_BOLD + parameterHex + ANSI_ENDC;
if (index == 0) {
rr += parameterHexHighlight + resultHex.substring(index + parameterHex.length(), resultHex.length());
} else {
rr += resultHex.substring(0, index) + parameterHexHighlight;
}
} catch (Exception err) {
rr += resultHex;
}
}
}
return rr;
}
/**
* Return as tree hierarchical object.
*
* @param indent - Initial hierarchical indention.
* @param verbosity - Verbose option.
* @return The output string.
*/
public String strTree(int indent, boolean verbosity) {
StringBuilder builder = new StringBuilder();
if (!this.attestations.isEmpty()) {
for (final TimeAttestation attestation : this.attestations) {
builder.append(Timestamp.indention(indent));
builder.append("verify " + attestation.toString() + strResult(verbosity, this.msg, null) + "\n");
if (attestation instanceof BitcoinBlockHeaderAttestation) {
String tx = Utils.bytesToHex(Utils.arrayReverse(this.msg));
builder.append(Timestamp.indention(indent) + "# Bitcoin block merkle root " + tx.toLowerCase() + "\n");
}
}
}
if (this.ops.size() > 1) {
TreeMap<Op, Timestamp> ordered = new TreeMap<>(this.ops);
for (Map.Entry<Op, Timestamp> entry : ordered.entrySet()) {
Timestamp timestamp = entry.getValue();
Op op = entry.getKey();
byte[] curRes = op.call(this.msg);
byte[] curPar = null;
if (op instanceof OpBinary) {
curPar = ((OpBinary) op).arg;
}
builder.append(Timestamp.indention(indent) + " -> " + op.toString().toLowerCase() + strResult(verbosity, curPar, curRes).toLowerCase() + "\n");
builder.append(timestamp.strTree(indent + 1, verbosity));
}
} else if (this.ops.size() > 0) {
// output += com.eternitywall.ots.Timestamp.indention(indent);
for (Map.Entry<Op, Timestamp> entry : this.ops.entrySet()) {
Timestamp timestamp = entry.getValue();
Op op = entry.getKey();
byte[] curRes = op.call(this.msg);
byte[] curPar = null;
if (op instanceof OpBinary) {
curPar = ((OpBinary) op).arg;
}
builder.append(Timestamp.indention(indent) + op.toString().toLowerCase() + strResult(verbosity, curPar, curRes).toLowerCase() + "\n");
builder.append(timestamp.strTree(indent, verbosity));
}
}
return builder.toString();
}
/**
* Returns a list of all sub timestamps with attestations.
*
* @return List of all sub timestamps with attestations.
*/
public List<Timestamp> directlyVerified() {
if (!this.attestations.isEmpty()) {
List<Timestamp> list = new ArrayList<>();
list.add(this);
return list;
}
List<Timestamp> list = new ArrayList<>();
for (Map.Entry<Op, Timestamp> entry : this.ops.entrySet()) {
Timestamp ts = entry.getValue();
//Op op = entry.getKey();
List<Timestamp> result = ts.directlyVerified();
list.addAll(result);
}
return list;
}
/**
* Returns a set of all Attestations.
*
* @return Set of all timestamp attestations.
*/
public Set<TimeAttestation> getAttestations() {
Set set = new HashSet<TimeAttestation>();
for (Map.Entry<byte[], TimeAttestation> 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 True if the timestamp is complete, False otherwise.
*/
public Boolean isTimestampComplete() {
for (Map.Entry<byte[], TimeAttestation> 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 Returns iterable of (msg, attestation)
*/
public HashMap<byte[], TimeAttestation> allAttestations() {
HashMap<byte[], TimeAttestation> map = new HashMap<>();
for (TimeAttestation attestation : this.attestations) {
map.put(this.msg, attestation);
}
for (Map.Entry<Op, Timestamp> entry : this.ops.entrySet()) {
Timestamp ts = entry.getValue();
//Op op = entry.getKey();
HashMap<byte[], TimeAttestation> subMap = ts.allAttestations();
for (Map.Entry<byte[], TimeAttestation> item : subMap.entrySet()) {
byte[] msg = item.getKey();
TimeAttestation attestation = item.getValue();
map.put(msg, attestation);
}
}
return map;
}
/**
* Iterate over all tips recursively
*
* @return Returns iterable of (msg, attestation)
*/
public Set<byte[]> allTips() {
Set<byte[]> set = new HashSet<>();
if (this.ops.size() == 0) {
set.add(this.msg);
}
for (Map.Entry<Op, Timestamp> entry : this.ops.entrySet()) {
Timestamp ts = entry.getValue();
//Op op = entry.getKey();
Set<byte[]> subSet = ts.allTips();
for (byte[] msg : subSet) {
set.add(msg);
}
}
return set;
}
/**
* Compare timestamps.
*
* @param timestamp the timestamp to compare with
* @return Returns true if timestamps are equals
*/
public boolean equals(Timestamp timestamp) {
if (Arrays.equals(this.getDigest(), timestamp.getDigest()) == false) {
return false;
}
// Check attestations
if (this.attestations.size() != timestamp.attestations.size()) {
return false;
}
for (int i = 0; i < this.attestations.size(); i++) {
TimeAttestation ta1 = this.attestations.get(i);
TimeAttestation ta2 = timestamp.attestations.get(i);
if (!(ta1.equals(ta2))) {
return false;
}
}
// Check operations
if (this.ops.size() != timestamp.ops.size()) {
return false;
}
// Order list of operations
List<Map.Entry<Op, Timestamp>> list1 = sortToList(this.ops.entrySet());
List<Map.Entry<Op, Timestamp>> list2 = sortToList(timestamp.ops.entrySet());
for (int i = 0; i < list1.size(); i++) {
Op op1 = list1.get(i).getKey();
Op op2 = list2.get(i).getKey();
if (!op1.equals(op2)) {
return false;
}
Timestamp t1 = list1.get(i).getValue();
Timestamp t2 = list2.get(i).getValue();
if (!t1.equals(t2)) {
return false;
}
}
return true;
}
/**
* Add Op to current timestamp and return the sub stamp
*
* @param op - The operation to insert
* @return Returns the sub timestamp
*/
public Timestamp add(Op op) {
// nonce_appended_stamp = timestamp.ops.add(com.vitorpamplona.quartz.ots.op.OpAppend(os.urandom(16)))
//Op opAppend = new OpAppend(bytes);
if (this.ops.containsKey(op)) {
return this.ops.get(op);
}
Timestamp stamp = new Timestamp(op.call(this.msg));
this.ops.put(op, stamp);
return stamp;
}
/**
* Retrieve a sorted list of all map entries.
*
* @param setEntries - The entries set of ops hashmap
* @return Returns the sorted list of map entries
*/
public List<Map.Entry<Op, Timestamp>> sortToList(Set<Entry<Op, Timestamp>> setEntries) {
List<Map.Entry<Op, Timestamp>> entries = new ArrayList<>(setEntries);
Collections.sort(entries, new Comparator<Map.Entry<Op, Timestamp>>() {
@Override
public int compare(Entry<Op, Timestamp> a, Entry<Op, Timestamp> b) {
return a.getKey().compareTo(b.getKey());
}
});
return entries;
}
}
@@ -0,0 +1,204 @@
package com.vitorpamplona.quartz.ots;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.logging.ConsoleHandler;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
/**
* Utility functions for (mostly) manipulating byte arrays.
*/
public class Utils {
/**
* Fills a byte array with the given byte value.
*
* @param array the array to fill
* @param value the value to fill the array with
*/
public static void arrayFill(byte[] array, byte value) {
for (int i = 0; i < array.length; i++) {
array[i] = value;
}
}
/**
* Returns the first value that is not null. If all objects are null, then it returns null.
*
* @param items the array of Ts
* @param <T> This describes my type parameter
* @return the first value that is not null. If all objects are null, then it returns null.
* @deprecated Not used by Java OpenTimestamps itself, and doesn't offer much useful functionality.
*/
@Deprecated
public static <T> T coalesce(T... items) {
for (T i : items) {
if (i != null) {
return i;
}
}
return null;
}
/**
* Returns a copy of the byte array argument, or null if the byte array argument is null.
*
* @param data the array of bytes to copy
* @return the copied byte array
*/
public static byte[] arraysCopy(byte[] data) {
if (data == null) {
return null;
}
byte[] copy = new byte[data.length];
System.arraycopy(data, 0, copy, 0, data.length);
return copy;
}
/**
* Returns a byte array which is the result of concatenating the two passed in byte arrays.
* None of the passed in arrays may be null.
*
* @param array1 the first array of bytes
* @param array2 the second array of bytes
* @return a copy of array1 and array2 concatenated together
*/
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;
}
/**
* Returns a given length array of random bytes.
*
* @param length the requested length of the byte array
* @return a given length array of random bytes
* @throws NoSuchAlgorithmException for Java 8 implementations
*/
public static byte[] randBytes(int length) throws NoSuchAlgorithmException {
//Java 6 & 7:
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[length];
random.nextBytes(bytes);
// Java 8 (even more secure):
// SecureRandom.getInstanceStrong().nextBytes(bytes);
return bytes;
}
/**
* Returns a reversed copy of the passed in byte array.
*
* @param array the byte array to reverse
* @return a copy of the byte array, reversed
*/
public static byte[] arrayReverse(byte[] array) {
byte[] reversedArray = new byte[array.length];
for (int i = array.length - 1, j = 0; i >= 0; i--, j++) {
reversedArray[j] = array[i];
}
return reversedArray;
}
/**
* Compares two byte arrays.
*
* @param left the left byte array to compare with
* @param right the right byte array to compare with
* @return 0 if the arrays are identical, negative if left &lt; right, positive if left &gt; right
*/
public static int compare(byte[] left, byte[] right) {
for (int i = 0, j = 0; i < left.length && j < right.length; i++, j++) {
int a = (left[i] & 0xff);
int b = (right[j] & 0xff);
if (a != b) {
return a - b;
}
}
return left.length - right.length;
}
/**
* Returns a HEX representation of the passed in byte array.
*
* @param bytes the byte array to convert into a string of HEX
* @return a string representation of the byte array
*/
public static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02X", b));
}
return sb.toString();
}
/**
* Converts a HEX representation to its byte array representation. The passed in string must not be null.
*
* @param s a string in HEX format to be converted to a corresponding byte array
* @return the byte array representation of the string in HEX format
* @throws IllegalArgumentException if the passed in HEX string can't be converted to a byte array
*/
public static byte[] hexToBytes(String s) throws IllegalArgumentException {
int len = s.length();
if (len % 2 == 1) {
throw new IllegalArgumentException();
}
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
if ((Character.digit(s.charAt(i), 16) == -1) || (Character.digit(s.charAt(i + 1), 16) == -1)) {
throw new IllegalArgumentException();
}
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i + 1), 16));
}
return data;
}
/**
* Returns a string with the first letter uppercase.
*
* @param string the string to get its first character converted to uppercase
* @return the string, with its first character converted to uppercase
*/
public static String toUpperFirstLetter(String string) {
return string.substring(0, 1).toUpperCase() + string.substring(1).toLowerCase();
}
// TODO: This is not the way to do logging. Fix later, possibly with slf4j annotation. Need to read up on the subject.
public static Logger getLogger(String name) {
Logger log = Logger.getLogger(name);
ConsoleHandler handler = new ConsoleHandler();
handler.setFormatter(new SimpleFormatter() {
@Override
public synchronized String format(LogRecord lr) {
return lr.getMessage() + "\r\n";
}
});
log.setUseParentHandlers(false);
log.addHandler(handler);
return log;
}
}
@@ -0,0 +1,56 @@
package com.vitorpamplona.quartz.ots;
import java.text.DateFormatSymbols;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
* Class that lets us compare, sort, store and print timestamps.
*/
public class VerifyResult implements Comparable<VerifyResult> {
public static enum Chains {
BITCOIN, LITECOIN, ETHEREUM
}
public Long timestamp;
public int height;
public VerifyResult(Long timestamp, int height) {
this.timestamp = timestamp;
this.height = height;
}
/**
* Returns, if existing, a string representation describing the existence of a block attest
*/
public String toString() {
if (height == 0 || timestamp == null) {
return "";
}
String pattern = "YYYY-MM-dd z";
Locale locale = new Locale("en", "UK");
DateFormatSymbols dateFormatSymbols = new DateFormatSymbols(locale);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern, dateFormatSymbols);
String string = simpleDateFormat.format(new Date(timestamp * 1000));
return "block " + String.valueOf(height) + " attests data existed as of " + string;
}
@Override
public int compareTo(VerifyResult vr) {
return this.height - vr.height;
}
@Override
public boolean equals(Object obj) {
VerifyResult vr = (VerifyResult) obj;
return this.timestamp == vr.timestamp && this.height == vr.height;
}
@Override
public int hashCode() {
return ((int) (long) (this.timestamp)) ^ this.height;
}
}
@@ -0,0 +1,118 @@
package com.vitorpamplona.quartz.ots.attestation;
import com.vitorpamplona.quartz.ots.BlockHeader;
import com.vitorpamplona.quartz.ots.StreamDeserializationContext;
import com.vitorpamplona.quartz.ots.StreamSerializationContext;
import com.vitorpamplona.quartz.ots.Utils;
import com.vitorpamplona.quartz.ots.exceptions.VerificationException;
import java.util.Arrays;
import java.util.logging.Logger;
/**
* 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.
*
* @see TimeAttestation
*/
public class BitcoinBlockHeaderAttestation extends TimeAttestation {
public static byte[] _TAG = {(byte) 0x05, (byte) 0x88, (byte) 0x96, (byte) 0x0d, (byte) 0x73, (byte) 0xd7, (byte) 0x19, (byte) 0x01};
public static String chain = "bitcoin";
@Override
public byte[] _TAG() {
return BitcoinBlockHeaderAttestation._TAG;
}
private int height = 0;
public int getHeight() {
return height;
}
public 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 + ")";
}
@Override
public int compareTo(TimeAttestation o) {
BitcoinBlockHeaderAttestation ob = (BitcoinBlockHeaderAttestation) o;
return this.height - ob.height;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof BitcoinBlockHeaderAttestation)) {
return false;
}
if (!Arrays.equals(this._TAG(), ((BitcoinBlockHeaderAttestation) obj)._TAG())) {
return false;
}
if (this.height != ((BitcoinBlockHeaderAttestation) obj).height) {
return false;
}
return true;
}
@Override
public int hashCode() {
return Arrays.hashCode(this._TAG()) ^ this.height;
}
/**
* Verify attestation against a Bitcoin block header.
* @param digest the digest
* @param block the Bitcoin block header
* @return the block time on success; raises VerificationError on failure.
* @throws VerificationException verification exception
*/
public Long verifyAgainstBlockheader(byte[] digest, BlockHeader block) throws VerificationException {
if (digest.length != 32) {
throw new VerificationException("Expected digest with length 32 bytes; got " + digest.length + " bytes");
} else if (!Arrays.equals(digest, Utils.hexToBytes(block.getMerkleroot()))) {
throw new VerificationException("Digest does not match merkleroot");
}
return block.getTime();
}
}
@@ -0,0 +1,79 @@
package com.vitorpamplona.quartz.ots.attestation;
import com.vitorpamplona.quartz.ots.StreamDeserializationContext;
import com.vitorpamplona.quartz.ots.StreamSerializationContext;
import com.vitorpamplona.quartz.ots.Utils;
import java.util.Arrays;
import java.util.logging.Logger;
/**
* Ethereum Block Header Attestation.
*
* @see TimeAttestation
*/
public class EthereumBlockHeaderAttestation extends TimeAttestation {
public static byte[] _TAG = {(byte) 0x30, (byte) 0xfe, (byte) 0x80, (byte) 0x87, (byte) 0xb5, (byte) 0xc7, (byte) 0xea, (byte) 0xd7};
public static String chain = "ethereum";
@Override
public byte[] _TAG() {
return EthereumBlockHeaderAttestation._TAG;
}
private int height = 0;
public int getHeight() {
return height;
}
EthereumBlockHeaderAttestation(int height_) {
super();
this.height = height_;
}
public static EthereumBlockHeaderAttestation deserialize(StreamDeserializationContext ctxPayload) {
int height = ctxPayload.readVaruint();
return new EthereumBlockHeaderAttestation(height);
}
@Override
public void serializePayload(StreamSerializationContext ctx) {
ctx.writeVaruint(this.height);
}
public String toString() {
return "EthereumBlockHeaderAttestation(" + this.height + ")";
}
@Override
public int compareTo(TimeAttestation o) {
EthereumBlockHeaderAttestation ob = (EthereumBlockHeaderAttestation) o;
return this.height - ob.height;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof EthereumBlockHeaderAttestation)) {
return false;
}
if (!Arrays.equals(this._TAG(), ((EthereumBlockHeaderAttestation) obj)._TAG())) {
return false;
}
if (this.height != ((EthereumBlockHeaderAttestation) obj).height) {
return false;
}
return true;
}
@Override
public int hashCode() {
return Arrays.hashCode(this._TAG()) ^ this.height;
}
}
@@ -0,0 +1,99 @@
package com.vitorpamplona.quartz.ots.attestation;
import com.vitorpamplona.quartz.ots.BlockHeader;
import com.vitorpamplona.quartz.ots.StreamDeserializationContext;
import com.vitorpamplona.quartz.ots.StreamSerializationContext;
import com.vitorpamplona.quartz.ots.Utils;
import com.vitorpamplona.quartz.ots.exceptions.VerificationException;
import java.util.Arrays;
import java.util.logging.Logger;
/**
* Litecoin Block Header Attestation.
*
* @see TimeAttestation
*/
public class LitecoinBlockHeaderAttestation extends TimeAttestation {
public static byte[] _TAG = {(byte) 0x06, (byte) 0x86, (byte) 0x9a, (byte) 0x0d, (byte) 0x73, (byte) 0xd7, (byte) 0x1b, (byte) 0x45};
public static String chain = "litecoin";
@Override
public byte[] _TAG() {
return LitecoinBlockHeaderAttestation._TAG;
}
private int height = 0;
public int getHeight() {
return height;
}
public LitecoinBlockHeaderAttestation(int height_) {
super();
this.height = height_;
}
public static LitecoinBlockHeaderAttestation deserialize(StreamDeserializationContext ctxPayload) {
int height = ctxPayload.readVaruint();
return new LitecoinBlockHeaderAttestation(height);
}
@Override
public void serializePayload(StreamSerializationContext ctx) {
ctx.writeVaruint(this.height);
}
public String toString() {
return "LitecoinBlockHeaderAttestation(" + this.height + ")";
}
@Override
public int compareTo(TimeAttestation o) {
LitecoinBlockHeaderAttestation ob = (LitecoinBlockHeaderAttestation) o;
return this.height - ob.height;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof LitecoinBlockHeaderAttestation)) {
return false;
}
if (!Arrays.equals(this._TAG(), ((LitecoinBlockHeaderAttestation) obj)._TAG())) {
return false;
}
if (this.height != ((LitecoinBlockHeaderAttestation) obj).height) {
return false;
}
return true;
}
@Override
public int hashCode() {
return Arrays.hashCode(this._TAG()) ^ this.height;
}
/**
* Verify attestation against a Litecoin block header.
* @param digest the digest
* @param block the Litecoin block header
* @return the block time on success; raises VerificationError on failure.
* @throws VerificationException verification exception
*/
public Long verifyAgainstBlockheader(byte[] digest, BlockHeader block) throws VerificationException {
if (digest.length != 32) {
throw new VerificationException("Expected digest with length 32 bytes; got " + digest.length + " bytes");
} else if (!Arrays.equals(digest, Utils.hexToBytes(block.getMerkleroot()))) {
throw new VerificationException("Digest does not match merkleroot");
}
return block.getTime();
}
}
@@ -0,0 +1,134 @@
package com.vitorpamplona.quartz.ots.attestation;
import android.util.Log;
import com.vitorpamplona.quartz.ots.StreamDeserializationContext;
import com.vitorpamplona.quartz.ots.StreamSerializationContext;
import com.vitorpamplona.quartz.ots.Utils;
import com.vitorpamplona.quartz.ots.exceptions.DeserializationException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.logging.Logger;
/**
* 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).
*
* @see TimeAttestation
*/
public 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-._/:";
private byte[] uri;
public byte[] getUri() {
return uri;
}
public 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) {
Log.e("OpenTimestamp","URI contains invalid character ");
return false;
}
}
return true;
}
public static PendingAttestation deserialize(StreamDeserializationContext ctxPayload)
throws DeserializationException {
byte[] utf8Uri;
try {
utf8Uri = ctxPayload.readVarbytes(PendingAttestation._MAX_URI_LENGTH);
} catch (DeserializationException e) {
Log.e("OpenTimestamp","URI too long and thus invalid: ");
throw new DeserializationException("Invalid URI: ");
}
if (PendingAttestation.checkUri(utf8Uri) == false) {
Log.e("OpenTimestamp","Invalid URI: ");
throw new DeserializationException("Invalid URI: ");
}
return new PendingAttestation(utf8Uri);
}
@Override
public void serializePayload(StreamSerializationContext ctx) {
ctx.writeVarbytes(this.uri);
}
public String toString() {
return "PendingAttestation(\'" + new String(this.uri, StandardCharsets.UTF_8) + "\')";
}
@Override
public int compareTo(TimeAttestation o) {
PendingAttestation opa = (PendingAttestation) o;
return Utils.compare(this.uri, opa.uri);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof PendingAttestation)) {
return false;
}
if (!Arrays.equals(this._TAG(), ((PendingAttestation) obj)._TAG())) {
return false;
}
if (!Arrays.equals(this.uri, ((PendingAttestation) obj).uri)) {
return false;
}
return true;
}
@Override
public int hashCode() {
return Arrays.hashCode(this._TAG()) ^ Arrays.hashCode(this.uri);
}
}
@@ -0,0 +1,83 @@
package com.vitorpamplona.quartz.ots.attestation;
import com.vitorpamplona.quartz.ots.StreamDeserializationContext;
import com.vitorpamplona.quartz.ots.StreamSerializationContext;
import com.vitorpamplona.quartz.ots.Utils;
import com.vitorpamplona.quartz.ots.exceptions.DeserializationException;
import java.util.Arrays;
import java.util.logging.Logger;
/**
* Class representing {@link com.vitorpamplona.quartz.ots.Timestamp} signature verification
*/
public abstract class TimeAttestation implements Comparable<TimeAttestation> {
public static int _TAG_SIZE = 8;
public static int _MAX_PAYLOAD_SIZE = 8192;
public byte[] _TAG;
public byte[] _TAG() {
return new byte[]{};
}
/**
* Deserialize a general Time Attestation to the specific subclass Attestation.
*
* @param ctx The stream deserialization context.
* @return The specific subclass Attestation.
*/
public static TimeAttestation deserialize(StreamDeserializationContext ctx) throws DeserializationException {
// console.log('attestation deserialize');
byte[] tag = ctx.readBytes(_TAG_SIZE);
// console.log('tag: ', com.vitorpamplona.quartz.ots.Utils.bytesToHex(tag));
byte[] serializedAttestation = ctx.readVarbytes(_MAX_PAYLOAD_SIZE);
// console.log('serializedAttestation: ', com.vitorpamplona.quartz.ots.Utils.bytesToHex(serializedAttestation));
StreamDeserializationContext ctxPayload = new StreamDeserializationContext(serializedAttestation);
/* eslint no-use-before-define: ["error", { "classes": false }] */
if (Arrays.equals(tag, PendingAttestation._TAG) == true) {
return PendingAttestation.deserialize(ctxPayload);
} else if (Arrays.equals(tag, BitcoinBlockHeaderAttestation._TAG) == true) {
return BitcoinBlockHeaderAttestation.deserialize(ctxPayload);
} else if (Arrays.equals(tag, LitecoinBlockHeaderAttestation._TAG) == true) {
return LitecoinBlockHeaderAttestation.deserialize(ctxPayload);
} else if (Arrays.equals(tag, EthereumBlockHeaderAttestation._TAG) == true) {
return EthereumBlockHeaderAttestation.deserialize(ctxPayload);
}
return new UnknownAttestation(tag, serializedAttestation);
}
/**
* Serialize a a general Time Attestation to the specific subclass Attestation.
*
* @param ctx The output stream serialization context.
*/
public void serialize(StreamSerializationContext ctx) {
ctx.writeBytes(this._TAG());
StreamSerializationContext ctxPayload = new StreamSerializationContext();
serializePayload(ctxPayload);
ctx.writeVarbytes(ctxPayload.getOutput());
}
public void serializePayload(StreamSerializationContext ctxPayload) {
// TODO: Is this intentional?
}
@Override
public int compareTo(TimeAttestation o) {
int deltaTag = Utils.compare(this._TAG(), o._TAG());
if (deltaTag == 0) {
return this.compareTo(o);
} else {
return deltaTag;
}
}
}
@@ -0,0 +1,76 @@
package com.vitorpamplona.quartz.ots.attestation;
import com.vitorpamplona.quartz.ots.StreamDeserializationContext;
import com.vitorpamplona.quartz.ots.StreamSerializationContext;
import com.vitorpamplona.quartz.ots.Utils;
import com.vitorpamplona.quartz.ots.exceptions.DeserializationException;
import java.util.Arrays;
import java.util.logging.Logger;
/**
* Placeholder for attestations that don't support
*
* @see TimeAttestation
*/
public class UnknownAttestation extends TimeAttestation {
byte[] payload;
public static 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) throws DeserializationException {
byte[] payload = ctxPayload.readVarbytes(_MAX_PAYLOAD_SIZE);
return new UnknownAttestation(tag, payload);
}
public String toString() {
return "UnknownAttestation " + Utils.bytesToHex(this._TAG()) + ' ' + Utils.bytesToHex(this.payload);
}
@Override
public int compareTo(TimeAttestation o) {
UnknownAttestation ota = (UnknownAttestation) o;
return Utils.compare(this.payload, ota.payload);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof UnknownAttestation)) {
return false;
}
if (!Arrays.equals(this._TAG(), ((UnknownAttestation) obj)._TAG())) {
return false;
}
if (!Arrays.equals(this.payload, ((UnknownAttestation) obj).payload)) {
return false;
}
return true;
}
@Override
public int hashCode() {
return Arrays.hashCode(this._TAG()) ^ Arrays.hashCode(this.payload);
}
}
@@ -0,0 +1,52 @@
package com.vitorpamplona.quartz.ots.crypto;
/**
* Message digest interface
*/
public interface Digest {
/**
* Return the algorithm name
*
* @return the algorithm name
*/
public String getAlgorithmName();
/**
* Return the size, in bytes, of the digest produced by this message digest.
*
* @return the size, in bytes, of the digest produced by this message digest.
*/
public int getDigestSize();
/**
* Update the message digest with a single byte.
*
* @param in the input byte to be entered.
*/
public void update(byte in);
/**
* Update the message digest with a block of bytes.
*
* @param in the byte array containing the data.
* @param inOff the offset into the byte array where the data starts.
* @param len the length of the data.
*/
public void update(byte[] in, int inOff, int len);
/**
* Close the digest, producing the final digest value. The doFinal
* call also resets the digest.
*
* @param out the array the digest is to be copied into.
* @param outOff the offset into the out array the digest is to start at.
* @return something
* @see #reset()
*/
public int doFinal(byte[] out, int outOff);
/**
* Reset the digest back to it's initial state.
*/
public void reset();
}
@@ -0,0 +1,11 @@
package com.vitorpamplona.quartz.ots.crypto;
public interface ExtendedDigest extends Digest {
/**
* Return the size in bytes of the internal buffer the digest applies it's compression
* function to.
*
* @return byte length of the digests internal buffer.
*/
public int getByteLength();
}
@@ -0,0 +1,138 @@
package com.vitorpamplona.quartz.ots.crypto;
/**
* Base implementation of MD4 family style digest as outlined in
* "Handbook of Applied Cryptography", pages 344 - 347.
*/
public abstract class GeneralDigest implements ExtendedDigest, Memoable {
private static final int BYTE_LENGTH = 64;
private final byte[] xBuf = new byte[4];
private int xBufOff;
private long byteCount;
/**
* Standard constructor
*/
protected GeneralDigest() {
xBufOff = 0;
}
/**
* Copy constructor. We are using copy constructors in place
* of the Object.clone() interface as this interface is not
* supported by J2ME.
*
* @param t the GeneralDigest
*/
protected GeneralDigest(GeneralDigest t) {
copyIn(t);
}
protected GeneralDigest(byte[] encodedState) {
System.arraycopy(encodedState, 0, xBuf, 0, xBuf.length);
xBufOff = Pack.bigEndianToInt(encodedState, 4);
byteCount = Pack.bigEndianToLong(encodedState, 8);
}
protected void copyIn(GeneralDigest t) {
System.arraycopy(t.xBuf, 0, xBuf, 0, t.xBuf.length);
xBufOff = t.xBufOff;
byteCount = t.byteCount;
}
public void update(byte in) {
xBuf[xBufOff++] = in;
if (xBufOff == xBuf.length) {
processWord(xBuf, 0);
xBufOff = 0;
}
byteCount++;
}
public void update(byte[] in, int inOff, int len) {
len = Math.max(0, len);
//
// fill the current word
//
int i = 0;
if (xBufOff != 0) {
while (i < len) {
xBuf[xBufOff++] = in[inOff + i++];
if (xBufOff == 4) {
processWord(xBuf, 0);
xBufOff = 0;
break;
}
}
}
//
// process whole words.
//
int limit = ((len - i) & ~3) + i;
for (; i < limit; i += 4) {
processWord(in, inOff + i);
}
//
// load in the remainder.
//
while (i < len) {
xBuf[xBufOff++] = in[inOff + i++];
}
byteCount += len;
}
public void finish() {
long bitLength = (byteCount << 3);
//
// add the pad bytes.
//
update((byte) 128);
while (xBufOff != 0) {
update((byte) 0);
}
processLength(bitLength);
processBlock();
}
public void reset() {
byteCount = 0;
xBufOff = 0;
for (int i = 0; i < xBuf.length; i++) {
xBuf[i] = 0;
}
}
protected void populateState(byte[] state) {
System.arraycopy(xBuf, 0, state, 0, xBufOff);
Pack.intToBigEndian(xBufOff, state, 4);
Pack.longToBigEndian(byteCount, state, 8);
}
public int getByteLength() {
return BYTE_LENGTH;
}
protected abstract void processWord(byte[] in, int inOff);
protected abstract void processLength(long bitLength);
protected abstract void processBlock();
}
@@ -0,0 +1,473 @@
package com.vitorpamplona.quartz.ots.crypto;
import com.vitorpamplona.quartz.ots.Utils;
/**
* Implementation of Keccak based on following KeccakNISTInterface.c from http://keccak.noekeon.org/
* <p>
* Following the naming conventions used in the C source code to enable easy review of the implementation.
*/
public class KeccakDigest implements ExtendedDigest {
private static long[] KeccakRoundConstants = keccakInitializeRoundConstants();
private static int[] KeccakRhoOffsets = keccakInitializeRhoOffsets();
private static long[] keccakInitializeRoundConstants() {
long[] keccakRoundConstants = new long[24];
byte[] LFSRstate = new byte[1];
LFSRstate[0] = 0x01;
int i, j, bitPosition;
for (i = 0; i < 24; i++) {
keccakRoundConstants[i] = 0;
for (j = 0; j < 7; j++) {
bitPosition = (1 << j) - 1;
if (LFSR86540(LFSRstate)) {
keccakRoundConstants[i] ^= 1L << bitPosition;
}
}
}
return keccakRoundConstants;
}
private static boolean LFSR86540(byte[] LFSR) {
boolean result = (((LFSR[0]) & 0x01) != 0);
if (((LFSR[0]) & 0x80) != 0) {
LFSR[0] = (byte) (((LFSR[0]) << 1) ^ 0x71);
} else {
LFSR[0] <<= 1;
}
return result;
}
private static int[] keccakInitializeRhoOffsets() {
int[] keccakRhoOffsets = new int[25];
int x, y, t, newX, newY;
keccakRhoOffsets[(((0) % 5) + 5 * ((0) % 5))] = 0;
x = 1;
y = 0;
for (t = 0; t < 24; t++) {
keccakRhoOffsets[(((x) % 5) + 5 * ((y) % 5))] = ((t + 1) * (t + 2) / 2) % 64;
newX = (0 * x + 1 * y) % 5;
newY = (2 * x + 3 * y) % 5;
x = newX;
y = newY;
}
return keccakRhoOffsets;
}
protected byte[] state = new byte[(1600 / 8)];
protected byte[] dataQueue = new byte[(1536 / 8)];
protected int rate;
protected int bitsInQueue;
protected int fixedOutputLength;
protected boolean squeezing;
protected int bitsAvailableForSqueezing;
protected byte[] chunk;
protected byte[] oneByte;
private void clearDataQueueSection(int off, int len) {
for (int i = off; i != off + len; i++) {
dataQueue[i] = 0;
}
}
public KeccakDigest() {
this(288);
}
public KeccakDigest(int bitLength) {
init(bitLength);
}
public KeccakDigest(KeccakDigest source) {
System.arraycopy(source.state, 0, this.state, 0, source.state.length);
System.arraycopy(source.dataQueue, 0, this.dataQueue, 0, source.dataQueue.length);
this.rate = source.rate;
this.bitsInQueue = source.bitsInQueue;
this.fixedOutputLength = source.fixedOutputLength;
this.squeezing = source.squeezing;
this.bitsAvailableForSqueezing = source.bitsAvailableForSqueezing;
this.chunk = Utils.arraysCopy(source.chunk);
this.oneByte = Utils.arraysCopy(source.oneByte);
}
public String getAlgorithmName() {
return "Keccak-" + fixedOutputLength;
}
public int getDigestSize() {
return fixedOutputLength / 8;
}
public void update(byte in) {
oneByte[0] = in;
absorb(oneByte, 0, 8L);
}
public void update(byte[] in, int inOff, int len) {
absorb(in, inOff, len * 8L);
}
public int doFinal(byte[] out, int outOff) {
squeeze(out, outOff, fixedOutputLength);
reset();
return getDigestSize();
}
/*
* TODO Possible API change to support partial-byte suffixes.
*/
protected int doFinal(byte[] out, int outOff, byte partialByte, int partialBits) {
if (partialBits > 0) {
oneByte[0] = partialByte;
absorb(oneByte, 0, partialBits);
}
squeeze(out, outOff, fixedOutputLength);
reset();
return getDigestSize();
}
public void reset() {
init(fixedOutputLength);
}
/**
* Return the size of block that the compression function is applied to in bytes.
*
* @return internal byte length of a block.
*/
public int getByteLength() {
return rate / 8;
}
private void init(int bitLength) {
switch (bitLength) {
case 288:
initSponge(1024, 576);
break;
case 128:
initSponge(1344, 256);
break;
case 224:
initSponge(1152, 448);
break;
case 256:
initSponge(1088, 512);
break;
case 384:
initSponge(832, 768);
break;
case 512:
initSponge(576, 1024);
break;
default:
throw new IllegalArgumentException("bitLength must be one of 128, 224, 256, 288, 384, or 512.");
}
}
private void initSponge(int rate, int capacity) {
if (rate + capacity != 1600) {
throw new IllegalStateException("rate + capacity != 1600");
}
if ((rate <= 0) || (rate >= 1600) || ((rate % 64) != 0)) {
throw new IllegalStateException("invalid rate value");
}
this.rate = rate;
// this is never read, need to check to see why we want to save it
// this.capacity = capacity;
Utils.arrayFill(this.state, (byte) 0);
Utils.arrayFill(this.dataQueue, (byte) 0);
this.bitsInQueue = 0;
this.squeezing = false;
this.bitsAvailableForSqueezing = 0;
this.fixedOutputLength = capacity / 2;
this.chunk = new byte[rate / 8];
this.oneByte = new byte[1];
}
private void absorbQueue() {
KeccakAbsorb(state, dataQueue, rate / 8);
bitsInQueue = 0;
}
protected void absorb(byte[] data, int off, long databitlen) {
long i, j, wholeBlocks;
if ((bitsInQueue % 8) != 0) {
throw new IllegalStateException("attempt to absorb with odd length queue");
}
if (squeezing) {
throw new IllegalStateException("attempt to absorb while squeezing");
}
i = 0;
while (i < databitlen) {
if ((bitsInQueue == 0) && (databitlen >= rate) && (i <= (databitlen - rate))) {
wholeBlocks = (databitlen - i) / rate;
for (j = 0; j < wholeBlocks; j++) {
System.arraycopy(data, (int) (off + (i / 8) + (j * chunk.length)), chunk, 0, chunk.length);
// displayIntermediateValues.displayBytes(1, "Block to be absorbed", curData, rate / 8);
KeccakAbsorb(state, chunk, chunk.length);
}
i += wholeBlocks * rate;
} else {
int partialBlock = (int) (databitlen - i);
if (partialBlock + bitsInQueue > rate) {
partialBlock = rate - bitsInQueue;
}
int partialByte = partialBlock % 8;
partialBlock -= partialByte;
System.arraycopy(data, off + (int) (i / 8), dataQueue, bitsInQueue / 8, partialBlock / 8);
bitsInQueue += partialBlock;
i += partialBlock;
if (bitsInQueue == rate) {
absorbQueue();
}
if (partialByte > 0) {
int mask = (1 << partialByte) - 1;
dataQueue[bitsInQueue / 8] = (byte) (data[off + ((int) (i / 8))] & mask);
bitsInQueue += partialByte;
i += partialByte;
}
}
}
}
private void padAndSwitchToSqueezingPhase() {
if (bitsInQueue + 1 == rate) {
dataQueue[bitsInQueue / 8] |= 1 << (bitsInQueue % 8);
absorbQueue();
clearDataQueueSection(0, rate / 8);
} else {
clearDataQueueSection((bitsInQueue + 7) / 8, rate / 8 - (bitsInQueue + 7) / 8);
dataQueue[bitsInQueue / 8] |= 1 << (bitsInQueue % 8);
}
dataQueue[(rate - 1) / 8] |= 1 << ((rate - 1) % 8);
absorbQueue();
// displayIntermediateValues.displayText(1, "--- Switching to squeezing phase ---");
if (rate == 1024) {
KeccakExtract1024bits(state, dataQueue);
bitsAvailableForSqueezing = 1024;
} else {
KeccakExtract(state, dataQueue, rate / 64);
bitsAvailableForSqueezing = rate;
}
// displayIntermediateValues.displayBytes(1, "Block available for squeezing", dataQueue, bitsAvailableForSqueezing / 8);
squeezing = true;
}
protected void squeeze(byte[] output, int offset, long outputLength) {
long i;
int partialBlock;
if (!squeezing) {
padAndSwitchToSqueezingPhase();
}
if ((outputLength % 8) != 0) {
throw new IllegalStateException("outputLength not a multiple of 8");
}
i = 0;
while (i < outputLength) {
if (bitsAvailableForSqueezing == 0) {
keccakPermutation(state);
if (rate == 1024) {
KeccakExtract1024bits(state, dataQueue);
bitsAvailableForSqueezing = 1024;
} else {
KeccakExtract(state, dataQueue, rate / 64);
bitsAvailableForSqueezing = rate;
}
// displayIntermediateValues.displayBytes(1, "Block available for squeezing", dataQueue, bitsAvailableForSqueezing / 8);
}
partialBlock = bitsAvailableForSqueezing;
if ((long) partialBlock > outputLength - i) {
partialBlock = (int) (outputLength - i);
}
System.arraycopy(dataQueue, (rate - bitsAvailableForSqueezing) / 8, output, offset + (int) (i / 8), partialBlock / 8);
bitsAvailableForSqueezing -= partialBlock;
i += partialBlock;
}
}
private void fromBytesToWords(long[] stateAsWords, byte[] state) {
for (int i = 0; i < (1600 / 64); i++) {
stateAsWords[i] = 0;
int index = i * (64 / 8);
for (int j = 0; j < (64 / 8); j++) {
stateAsWords[i] |= ((long) state[index + j] & 0xff) << ((8 * j));
}
}
}
private void fromWordsToBytes(byte[] state, long[] stateAsWords) {
for (int i = 0; i < (1600 / 64); i++) {
int index = i * (64 / 8);
for (int j = 0; j < (64 / 8); j++) {
state[index + j] = (byte) ((stateAsWords[i] >>> ((8 * j))) & 0xFF);
}
}
}
private void keccakPermutation(byte[] state) {
long[] longState = new long[state.length / 8];
fromBytesToWords(longState, state);
// displayIntermediateValues.displayStateAsBytes(1, "Input of permutation", longState);
keccakPermutationOnWords(longState);
// displayIntermediateValues.displayStateAsBytes(1, "State after permutation", longState);
fromWordsToBytes(state, longState);
}
private void keccakPermutationAfterXor(byte[] state, byte[] data, int dataLengthInBytes) {
int i;
for (i = 0; i < dataLengthInBytes; i++) {
state[i] ^= data[i];
}
keccakPermutation(state);
}
private void keccakPermutationOnWords(long[] state) {
int i;
// displayIntermediateValues.displayStateAs64bitWords(3, "Same, with lanes as 64-bit words", state);
for (i = 0; i < 24; i++) {
// displayIntermediateValues.displayRoundNumber(3, i);
theta(state);
// displayIntermediateValues.displayStateAs64bitWords(3, "After theta", state);
rho(state);
// displayIntermediateValues.displayStateAs64bitWords(3, "After rho", state);
pi(state);
// displayIntermediateValues.displayStateAs64bitWords(3, "After pi", state);
chi(state);
// displayIntermediateValues.displayStateAs64bitWords(3, "After chi", state);
iota(state, i);
// displayIntermediateValues.displayStateAs64bitWords(3, "After iota", state);
}
}
long[] C = new long[5];
private void theta(long[] A) {
for (int x = 0; x < 5; x++) {
C[x] = 0;
for (int y = 0; y < 5; y++) {
C[x] ^= A[x + 5 * y];
}
}
for (int x = 0; x < 5; x++) {
long dX = ((((C[(x + 1) % 5]) << 1) ^ ((C[(x + 1) % 5]) >>> (64 - 1)))) ^ C[(x + 4) % 5];
for (int y = 0; y < 5; y++) {
A[x + 5 * y] ^= dX;
}
}
}
private void rho(long[] A) {
for (int x = 0; x < 5; x++) {
for (int y = 0; y < 5; y++) {
int index = x + 5 * y;
A[index] = ((KeccakRhoOffsets[index] != 0) ? (((A[index]) << KeccakRhoOffsets[index]) ^ ((A[index]) >>> (64 - KeccakRhoOffsets[index]))) : A[index]);
}
}
}
long[] tempA = new long[25];
private void pi(long[] A) {
System.arraycopy(A, 0, tempA, 0, tempA.length);
for (int x = 0; x < 5; x++) {
for (int y = 0; y < 5; y++) {
A[y + 5 * ((2 * x + 3 * y) % 5)] = tempA[x + 5 * y];
}
}
}
long[] chiC = new long[5];
private void chi(long[] A) {
for (int y = 0; y < 5; y++) {
for (int x = 0; x < 5; x++) {
chiC[x] = A[x + 5 * y] ^ ((~A[(((x + 1) % 5) + 5 * y)]) & A[(((x + 2) % 5) + 5 * y)]);
}
for (int x = 0; x < 5; x++) {
A[x + 5 * y] = chiC[x];
}
}
}
private void iota(long[] A, int indexRound) {
A[(((0) % 5) + 5 * ((0) % 5))] ^= KeccakRoundConstants[indexRound];
}
private void KeccakAbsorb(byte[] byteState, byte[] data, int dataInBytes) {
keccakPermutationAfterXor(byteState, data, dataInBytes);
}
private void KeccakExtract1024bits(byte[] byteState, byte[] data) {
System.arraycopy(byteState, 0, data, 0, 128);
}
private void KeccakExtract(byte[] byteState, byte[] data, int laneCount) {
System.arraycopy(byteState, 0, data, 0, laneCount * 8);
}
}
@@ -0,0 +1,28 @@
package com.vitorpamplona.quartz.ots.crypto;
/**
* Interface for Memoable objects. Memoable objects allow the taking of a snapshot of their internal state
* via the {@link #copy copy()} method and then resetting the object back to that state later using the
* {@link #reset reset()} method.
*/
public interface Memoable {
/**
* Produce a copy of this object with its configuration and in its current state.
* <p>
* The returned object may be used simply to store the state, or may be used as a similar object
* starting from the copied state.
*
* @return Memoable object
*/
Memoable copy();
/**
* Restore a copied object state into this object.
* <p>
* Implementations of this method <em>should</em> try to avoid or minimise memory allocation to perform the reset.
*
* @param other an object originally {@link #copy() copied} from an object of the same type as this instance.
* @throws ClassCastException if the provided object is not of the correct type.
*/
void reset(Memoable other);
}
@@ -0,0 +1,242 @@
package com.vitorpamplona.quartz.ots.crypto;
/**
* Utility methods for converting byte arrays into ints and longs, and back again.
*/
public abstract class Pack
{
public static short bigEndianToShort(byte[] bs, int off)
{
int n = (bs[ off] & 0xff) << 8;
n |= (bs[++off] & 0xff);
return (short)n;
}
public static int bigEndianToInt(byte[] bs, int off)
{
int n = bs[ off] << 24;
n |= (bs[++off] & 0xff) << 16;
n |= (bs[++off] & 0xff) << 8;
n |= (bs[++off] & 0xff);
return n;
}
public static void bigEndianToInt(byte[] bs, int off, int[] ns)
{
for (int i = 0; i < ns.length; ++i)
{
ns[i] = bigEndianToInt(bs, off);
off += 4;
}
}
public static byte[] intToBigEndian(int n)
{
byte[] bs = new byte[4];
intToBigEndian(n, bs, 0);
return bs;
}
public static void intToBigEndian(int n, byte[] bs, int off)
{
bs[ off] = (byte)(n >>> 24);
bs[++off] = (byte)(n >>> 16);
bs[++off] = (byte)(n >>> 8);
bs[++off] = (byte)(n );
}
public static byte[] intToBigEndian(int[] ns)
{
byte[] bs = new byte[4 * ns.length];
intToBigEndian(ns, bs, 0);
return bs;
}
public static void intToBigEndian(int[] ns, byte[] bs, int off)
{
for (int i = 0; i < ns.length; ++i)
{
intToBigEndian(ns[i], bs, off);
off += 4;
}
}
public static long bigEndianToLong(byte[] bs, int off)
{
int hi = bigEndianToInt(bs, off);
int lo = bigEndianToInt(bs, off + 4);
return ((long)(hi & 0xffffffffL) << 32) | (long)(lo & 0xffffffffL);
}
public static void bigEndianToLong(byte[] bs, int off, long[] ns)
{
for (int i = 0; i < ns.length; ++i)
{
ns[i] = bigEndianToLong(bs, off);
off += 8;
}
}
public static byte[] longToBigEndian(long n)
{
byte[] bs = new byte[8];
longToBigEndian(n, bs, 0);
return bs;
}
public static void longToBigEndian(long n, byte[] bs, int off)
{
intToBigEndian((int)(n >>> 32), bs, off);
intToBigEndian((int)(n & 0xffffffffL), bs, off + 4);
}
public static byte[] longToBigEndian(long[] ns)
{
byte[] bs = new byte[8 * ns.length];
longToBigEndian(ns, bs, 0);
return bs;
}
public static void longToBigEndian(long[] ns, byte[] bs, int off)
{
for (int i = 0; i < ns.length; ++i)
{
longToBigEndian(ns[i], bs, off);
off += 8;
}
}
public static short littleEndianToShort(byte[] bs, int off)
{
int n = bs[ off] & 0xff;
n |= (bs[++off] & 0xff) << 8;
return (short)n;
}
public static int littleEndianToInt(byte[] bs, int off)
{
int n = bs[ off] & 0xff;
n |= (bs[++off] & 0xff) << 8;
n |= (bs[++off] & 0xff) << 16;
n |= bs[++off] << 24;
return n;
}
public static void littleEndianToInt(byte[] bs, int off, int[] ns)
{
for (int i = 0; i < ns.length; ++i)
{
ns[i] = littleEndianToInt(bs, off);
off += 4;
}
}
public static void littleEndianToInt(byte[] bs, int bOff, int[] ns, int nOff, int count)
{
for (int i = 0; i < count; ++i)
{
ns[nOff + i] = littleEndianToInt(bs, bOff);
bOff += 4;
}
}
public static int[] littleEndianToInt(byte[] bs, int off, int count)
{
int[] ns = new int[count];
for (int i = 0; i < ns.length; ++i)
{
ns[i] = littleEndianToInt(bs, off);
off += 4;
}
return ns;
}
public static byte[] shortToLittleEndian(short n)
{
byte[] bs = new byte[2];
shortToLittleEndian(n, bs, 0);
return bs;
}
public static void shortToLittleEndian(short n, byte[] bs, int off)
{
bs[ off] = (byte)(n );
bs[++off] = (byte)(n >>> 8);
}
public static byte[] intToLittleEndian(int n)
{
byte[] bs = new byte[4];
intToLittleEndian(n, bs, 0);
return bs;
}
public static void intToLittleEndian(int n, byte[] bs, int off)
{
bs[ off] = (byte)(n );
bs[++off] = (byte)(n >>> 8);
bs[++off] = (byte)(n >>> 16);
bs[++off] = (byte)(n >>> 24);
}
public static byte[] intToLittleEndian(int[] ns)
{
byte[] bs = new byte[4 * ns.length];
intToLittleEndian(ns, bs, 0);
return bs;
}
public static void intToLittleEndian(int[] ns, byte[] bs, int off)
{
for (int i = 0; i < ns.length; ++i)
{
intToLittleEndian(ns[i], bs, off);
off += 4;
}
}
public static long littleEndianToLong(byte[] bs, int off)
{
int lo = littleEndianToInt(bs, off);
int hi = littleEndianToInt(bs, off + 4);
return ((long)(hi & 0xffffffffL) << 32) | (long)(lo & 0xffffffffL);
}
public static void littleEndianToLong(byte[] bs, int off, long[] ns)
{
for (int i = 0; i < ns.length; ++i)
{
ns[i] = littleEndianToLong(bs, off);
off += 8;
}
}
public static byte[] longToLittleEndian(long n)
{
byte[] bs = new byte[8];
longToLittleEndian(n, bs, 0);
return bs;
}
public static void longToLittleEndian(long n, byte[] bs, int off)
{
intToLittleEndian((int)(n & 0xffffffffL), bs, off);
intToLittleEndian((int)(n >>> 32), bs, off + 4);
}
public static byte[] longToLittleEndian(long[] ns)
{
byte[] bs = new byte[8 * ns.length];
longToLittleEndian(ns, bs, 0);
return bs;
}
public static void longToLittleEndian(long[] ns, byte[] bs, int off)
{
for (int i = 0; i < ns.length; ++i)
{
longToLittleEndian(ns[i], bs, off);
off += 8;
}
}
}
@@ -0,0 +1,442 @@
package com.vitorpamplona.quartz.ots.crypto;
/**
* Implementation of RIPEMD
*
* @see <a href="http://www.esat.kuleuven.ac.be/~bosselae/ripemd160.html">ripemd160</a>
*/
public class RIPEMD160Digest
extends GeneralDigest
{
private static final int DIGEST_LENGTH = 20;
private int H0, H1, H2, H3, H4; // IV's
private int[] X = new int[16];
private int xOff;
/**
* Standard constructor
*/
public RIPEMD160Digest()
{
reset();
}
/**
* Copy constructor. This will copy the state of the provided
* message digest.
* @param t RIPEMD160Digest
*/
public RIPEMD160Digest(RIPEMD160Digest t)
{
super(t);
copyIn(t);
}
private void copyIn(RIPEMD160Digest t)
{
super.copyIn(t);
H0 = t.H0;
H1 = t.H1;
H2 = t.H2;
H3 = t.H3;
H4 = t.H4;
System.arraycopy(t.X, 0, X, 0, t.X.length);
xOff = t.xOff;
}
public String getAlgorithmName()
{
return "RIPEMD160";
}
public int getDigestSize()
{
return DIGEST_LENGTH;
}
protected void processWord(
byte[] in,
int inOff)
{
X[xOff++] = (in[inOff] & 0xff) | ((in[inOff + 1] & 0xff) << 8)
| ((in[inOff + 2] & 0xff) << 16) | ((in[inOff + 3] & 0xff) << 24);
if (xOff == 16)
{
processBlock();
}
}
protected void processLength(
long bitLength)
{
if (xOff > 14)
{
processBlock();
}
X[14] = (int)(bitLength & 0xffffffff);
X[15] = (int)(bitLength >>> 32);
}
private void unpackWord(
int word,
byte[] out,
int outOff)
{
out[outOff] = (byte)word;
out[outOff + 1] = (byte)(word >>> 8);
out[outOff + 2] = (byte)(word >>> 16);
out[outOff + 3] = (byte)(word >>> 24);
}
public int doFinal(
byte[] out,
int outOff)
{
finish();
unpackWord(H0, out, outOff);
unpackWord(H1, out, outOff + 4);
unpackWord(H2, out, outOff + 8);
unpackWord(H3, out, outOff + 12);
unpackWord(H4, out, outOff + 16);
reset();
return DIGEST_LENGTH;
}
/**
* reset the chaining variables to the IV values.
*/
public void reset()
{
super.reset();
H0 = 0x67452301;
H1 = 0xefcdab89;
H2 = 0x98badcfe;
H3 = 0x10325476;
H4 = 0xc3d2e1f0;
xOff = 0;
for (int i = 0; i != X.length; i++)
{
X[i] = 0;
}
}
/*
* rotate int x left n bits.
*/
private int RL(
int x,
int n)
{
return (x << n) | (x >>> (32 - n));
}
/*
* f1,f2,f3,f4,f5 are the basic RIPEMD160 functions.
*/
/*
* rounds 0-15
*/
private int f1(
int x,
int y,
int z)
{
return x ^ y ^ z;
}
/*
* rounds 16-31
*/
private int f2(
int x,
int y,
int z)
{
return (x & y) | (~x & z);
}
/*
* rounds 32-47
*/
private int f3(
int x,
int y,
int z)
{
return (x | ~y) ^ z;
}
/*
* rounds 48-63
*/
private int f4(
int x,
int y,
int z)
{
return (x & z) | (y & ~z);
}
/*
* rounds 64-79
*/
private int f5(
int x,
int y,
int z)
{
return x ^ (y | ~z);
}
protected void processBlock()
{
int a, aa;
int b, bb;
int c, cc;
int d, dd;
int e, ee;
a = aa = H0;
b = bb = H1;
c = cc = H2;
d = dd = H3;
e = ee = H4;
//
// Rounds 1 - 16
//
// left
a = RL(a + f1(b,c,d) + X[ 0], 11) + e; c = RL(c, 10);
e = RL(e + f1(a,b,c) + X[ 1], 14) + d; b = RL(b, 10);
d = RL(d + f1(e,a,b) + X[ 2], 15) + c; a = RL(a, 10);
c = RL(c + f1(d,e,a) + X[ 3], 12) + b; e = RL(e, 10);
b = RL(b + f1(c,d,e) + X[ 4], 5) + a; d = RL(d, 10);
a = RL(a + f1(b,c,d) + X[ 5], 8) + e; c = RL(c, 10);
e = RL(e + f1(a,b,c) + X[ 6], 7) + d; b = RL(b, 10);
d = RL(d + f1(e,a,b) + X[ 7], 9) + c; a = RL(a, 10);
c = RL(c + f1(d,e,a) + X[ 8], 11) + b; e = RL(e, 10);
b = RL(b + f1(c,d,e) + X[ 9], 13) + a; d = RL(d, 10);
a = RL(a + f1(b,c,d) + X[10], 14) + e; c = RL(c, 10);
e = RL(e + f1(a,b,c) + X[11], 15) + d; b = RL(b, 10);
d = RL(d + f1(e,a,b) + X[12], 6) + c; a = RL(a, 10);
c = RL(c + f1(d,e,a) + X[13], 7) + b; e = RL(e, 10);
b = RL(b + f1(c,d,e) + X[14], 9) + a; d = RL(d, 10);
a = RL(a + f1(b,c,d) + X[15], 8) + e; c = RL(c, 10);
// right
aa = RL(aa + f5(bb,cc,dd) + X[ 5] + 0x50a28be6, 8) + ee; cc = RL(cc, 10);
ee = RL(ee + f5(aa,bb,cc) + X[14] + 0x50a28be6, 9) + dd; bb = RL(bb, 10);
dd = RL(dd + f5(ee,aa,bb) + X[ 7] + 0x50a28be6, 9) + cc; aa = RL(aa, 10);
cc = RL(cc + f5(dd,ee,aa) + X[ 0] + 0x50a28be6, 11) + bb; ee = RL(ee, 10);
bb = RL(bb + f5(cc,dd,ee) + X[ 9] + 0x50a28be6, 13) + aa; dd = RL(dd, 10);
aa = RL(aa + f5(bb,cc,dd) + X[ 2] + 0x50a28be6, 15) + ee; cc = RL(cc, 10);
ee = RL(ee + f5(aa,bb,cc) + X[11] + 0x50a28be6, 15) + dd; bb = RL(bb, 10);
dd = RL(dd + f5(ee,aa,bb) + X[ 4] + 0x50a28be6, 5) + cc; aa = RL(aa, 10);
cc = RL(cc + f5(dd,ee,aa) + X[13] + 0x50a28be6, 7) + bb; ee = RL(ee, 10);
bb = RL(bb + f5(cc,dd,ee) + X[ 6] + 0x50a28be6, 7) + aa; dd = RL(dd, 10);
aa = RL(aa + f5(bb,cc,dd) + X[15] + 0x50a28be6, 8) + ee; cc = RL(cc, 10);
ee = RL(ee + f5(aa,bb,cc) + X[ 8] + 0x50a28be6, 11) + dd; bb = RL(bb, 10);
dd = RL(dd + f5(ee,aa,bb) + X[ 1] + 0x50a28be6, 14) + cc; aa = RL(aa, 10);
cc = RL(cc + f5(dd,ee,aa) + X[10] + 0x50a28be6, 14) + bb; ee = RL(ee, 10);
bb = RL(bb + f5(cc,dd,ee) + X[ 3] + 0x50a28be6, 12) + aa; dd = RL(dd, 10);
aa = RL(aa + f5(bb,cc,dd) + X[12] + 0x50a28be6, 6) + ee; cc = RL(cc, 10);
//
// Rounds 16-31
//
// left
e = RL(e + f2(a,b,c) + X[ 7] + 0x5a827999, 7) + d; b = RL(b, 10);
d = RL(d + f2(e,a,b) + X[ 4] + 0x5a827999, 6) + c; a = RL(a, 10);
c = RL(c + f2(d,e,a) + X[13] + 0x5a827999, 8) + b; e = RL(e, 10);
b = RL(b + f2(c,d,e) + X[ 1] + 0x5a827999, 13) + a; d = RL(d, 10);
a = RL(a + f2(b,c,d) + X[10] + 0x5a827999, 11) + e; c = RL(c, 10);
e = RL(e + f2(a,b,c) + X[ 6] + 0x5a827999, 9) + d; b = RL(b, 10);
d = RL(d + f2(e,a,b) + X[15] + 0x5a827999, 7) + c; a = RL(a, 10);
c = RL(c + f2(d,e,a) + X[ 3] + 0x5a827999, 15) + b; e = RL(e, 10);
b = RL(b + f2(c,d,e) + X[12] + 0x5a827999, 7) + a; d = RL(d, 10);
a = RL(a + f2(b,c,d) + X[ 0] + 0x5a827999, 12) + e; c = RL(c, 10);
e = RL(e + f2(a,b,c) + X[ 9] + 0x5a827999, 15) + d; b = RL(b, 10);
d = RL(d + f2(e,a,b) + X[ 5] + 0x5a827999, 9) + c; a = RL(a, 10);
c = RL(c + f2(d,e,a) + X[ 2] + 0x5a827999, 11) + b; e = RL(e, 10);
b = RL(b + f2(c,d,e) + X[14] + 0x5a827999, 7) + a; d = RL(d, 10);
a = RL(a + f2(b,c,d) + X[11] + 0x5a827999, 13) + e; c = RL(c, 10);
e = RL(e + f2(a,b,c) + X[ 8] + 0x5a827999, 12) + d; b = RL(b, 10);
// right
ee = RL(ee + f4(aa,bb,cc) + X[ 6] + 0x5c4dd124, 9) + dd; bb = RL(bb, 10);
dd = RL(dd + f4(ee,aa,bb) + X[11] + 0x5c4dd124, 13) + cc; aa = RL(aa, 10);
cc = RL(cc + f4(dd,ee,aa) + X[ 3] + 0x5c4dd124, 15) + bb; ee = RL(ee, 10);
bb = RL(bb + f4(cc,dd,ee) + X[ 7] + 0x5c4dd124, 7) + aa; dd = RL(dd, 10);
aa = RL(aa + f4(bb,cc,dd) + X[ 0] + 0x5c4dd124, 12) + ee; cc = RL(cc, 10);
ee = RL(ee + f4(aa,bb,cc) + X[13] + 0x5c4dd124, 8) + dd; bb = RL(bb, 10);
dd = RL(dd + f4(ee,aa,bb) + X[ 5] + 0x5c4dd124, 9) + cc; aa = RL(aa, 10);
cc = RL(cc + f4(dd,ee,aa) + X[10] + 0x5c4dd124, 11) + bb; ee = RL(ee, 10);
bb = RL(bb + f4(cc,dd,ee) + X[14] + 0x5c4dd124, 7) + aa; dd = RL(dd, 10);
aa = RL(aa + f4(bb,cc,dd) + X[15] + 0x5c4dd124, 7) + ee; cc = RL(cc, 10);
ee = RL(ee + f4(aa,bb,cc) + X[ 8] + 0x5c4dd124, 12) + dd; bb = RL(bb, 10);
dd = RL(dd + f4(ee,aa,bb) + X[12] + 0x5c4dd124, 7) + cc; aa = RL(aa, 10);
cc = RL(cc + f4(dd,ee,aa) + X[ 4] + 0x5c4dd124, 6) + bb; ee = RL(ee, 10);
bb = RL(bb + f4(cc,dd,ee) + X[ 9] + 0x5c4dd124, 15) + aa; dd = RL(dd, 10);
aa = RL(aa + f4(bb,cc,dd) + X[ 1] + 0x5c4dd124, 13) + ee; cc = RL(cc, 10);
ee = RL(ee + f4(aa,bb,cc) + X[ 2] + 0x5c4dd124, 11) + dd; bb = RL(bb, 10);
//
// Rounds 32-47
//
// left
d = RL(d + f3(e,a,b) + X[ 3] + 0x6ed9eba1, 11) + c; a = RL(a, 10);
c = RL(c + f3(d,e,a) + X[10] + 0x6ed9eba1, 13) + b; e = RL(e, 10);
b = RL(b + f3(c,d,e) + X[14] + 0x6ed9eba1, 6) + a; d = RL(d, 10);
a = RL(a + f3(b,c,d) + X[ 4] + 0x6ed9eba1, 7) + e; c = RL(c, 10);
e = RL(e + f3(a,b,c) + X[ 9] + 0x6ed9eba1, 14) + d; b = RL(b, 10);
d = RL(d + f3(e,a,b) + X[15] + 0x6ed9eba1, 9) + c; a = RL(a, 10);
c = RL(c + f3(d,e,a) + X[ 8] + 0x6ed9eba1, 13) + b; e = RL(e, 10);
b = RL(b + f3(c,d,e) + X[ 1] + 0x6ed9eba1, 15) + a; d = RL(d, 10);
a = RL(a + f3(b,c,d) + X[ 2] + 0x6ed9eba1, 14) + e; c = RL(c, 10);
e = RL(e + f3(a,b,c) + X[ 7] + 0x6ed9eba1, 8) + d; b = RL(b, 10);
d = RL(d + f3(e,a,b) + X[ 0] + 0x6ed9eba1, 13) + c; a = RL(a, 10);
c = RL(c + f3(d,e,a) + X[ 6] + 0x6ed9eba1, 6) + b; e = RL(e, 10);
b = RL(b + f3(c,d,e) + X[13] + 0x6ed9eba1, 5) + a; d = RL(d, 10);
a = RL(a + f3(b,c,d) + X[11] + 0x6ed9eba1, 12) + e; c = RL(c, 10);
e = RL(e + f3(a,b,c) + X[ 5] + 0x6ed9eba1, 7) + d; b = RL(b, 10);
d = RL(d + f3(e,a,b) + X[12] + 0x6ed9eba1, 5) + c; a = RL(a, 10);
// right
dd = RL(dd + f3(ee,aa,bb) + X[15] + 0x6d703ef3, 9) + cc; aa = RL(aa, 10);
cc = RL(cc + f3(dd,ee,aa) + X[ 5] + 0x6d703ef3, 7) + bb; ee = RL(ee, 10);
bb = RL(bb + f3(cc,dd,ee) + X[ 1] + 0x6d703ef3, 15) + aa; dd = RL(dd, 10);
aa = RL(aa + f3(bb,cc,dd) + X[ 3] + 0x6d703ef3, 11) + ee; cc = RL(cc, 10);
ee = RL(ee + f3(aa,bb,cc) + X[ 7] + 0x6d703ef3, 8) + dd; bb = RL(bb, 10);
dd = RL(dd + f3(ee,aa,bb) + X[14] + 0x6d703ef3, 6) + cc; aa = RL(aa, 10);
cc = RL(cc + f3(dd,ee,aa) + X[ 6] + 0x6d703ef3, 6) + bb; ee = RL(ee, 10);
bb = RL(bb + f3(cc,dd,ee) + X[ 9] + 0x6d703ef3, 14) + aa; dd = RL(dd, 10);
aa = RL(aa + f3(bb,cc,dd) + X[11] + 0x6d703ef3, 12) + ee; cc = RL(cc, 10);
ee = RL(ee + f3(aa,bb,cc) + X[ 8] + 0x6d703ef3, 13) + dd; bb = RL(bb, 10);
dd = RL(dd + f3(ee,aa,bb) + X[12] + 0x6d703ef3, 5) + cc; aa = RL(aa, 10);
cc = RL(cc + f3(dd,ee,aa) + X[ 2] + 0x6d703ef3, 14) + bb; ee = RL(ee, 10);
bb = RL(bb + f3(cc,dd,ee) + X[10] + 0x6d703ef3, 13) + aa; dd = RL(dd, 10);
aa = RL(aa + f3(bb,cc,dd) + X[ 0] + 0x6d703ef3, 13) + ee; cc = RL(cc, 10);
ee = RL(ee + f3(aa,bb,cc) + X[ 4] + 0x6d703ef3, 7) + dd; bb = RL(bb, 10);
dd = RL(dd + f3(ee,aa,bb) + X[13] + 0x6d703ef3, 5) + cc; aa = RL(aa, 10);
//
// Rounds 48-63
//
// left
c = RL(c + f4(d,e,a) + X[ 1] + 0x8f1bbcdc, 11) + b; e = RL(e, 10);
b = RL(b + f4(c,d,e) + X[ 9] + 0x8f1bbcdc, 12) + a; d = RL(d, 10);
a = RL(a + f4(b,c,d) + X[11] + 0x8f1bbcdc, 14) + e; c = RL(c, 10);
e = RL(e + f4(a,b,c) + X[10] + 0x8f1bbcdc, 15) + d; b = RL(b, 10);
d = RL(d + f4(e,a,b) + X[ 0] + 0x8f1bbcdc, 14) + c; a = RL(a, 10);
c = RL(c + f4(d,e,a) + X[ 8] + 0x8f1bbcdc, 15) + b; e = RL(e, 10);
b = RL(b + f4(c,d,e) + X[12] + 0x8f1bbcdc, 9) + a; d = RL(d, 10);
a = RL(a + f4(b,c,d) + X[ 4] + 0x8f1bbcdc, 8) + e; c = RL(c, 10);
e = RL(e + f4(a,b,c) + X[13] + 0x8f1bbcdc, 9) + d; b = RL(b, 10);
d = RL(d + f4(e,a,b) + X[ 3] + 0x8f1bbcdc, 14) + c; a = RL(a, 10);
c = RL(c + f4(d,e,a) + X[ 7] + 0x8f1bbcdc, 5) + b; e = RL(e, 10);
b = RL(b + f4(c,d,e) + X[15] + 0x8f1bbcdc, 6) + a; d = RL(d, 10);
a = RL(a + f4(b,c,d) + X[14] + 0x8f1bbcdc, 8) + e; c = RL(c, 10);
e = RL(e + f4(a,b,c) + X[ 5] + 0x8f1bbcdc, 6) + d; b = RL(b, 10);
d = RL(d + f4(e,a,b) + X[ 6] + 0x8f1bbcdc, 5) + c; a = RL(a, 10);
c = RL(c + f4(d,e,a) + X[ 2] + 0x8f1bbcdc, 12) + b; e = RL(e, 10);
// right
cc = RL(cc + f2(dd,ee,aa) + X[ 8] + 0x7a6d76e9, 15) + bb; ee = RL(ee, 10);
bb = RL(bb + f2(cc,dd,ee) + X[ 6] + 0x7a6d76e9, 5) + aa; dd = RL(dd, 10);
aa = RL(aa + f2(bb,cc,dd) + X[ 4] + 0x7a6d76e9, 8) + ee; cc = RL(cc, 10);
ee = RL(ee + f2(aa,bb,cc) + X[ 1] + 0x7a6d76e9, 11) + dd; bb = RL(bb, 10);
dd = RL(dd + f2(ee,aa,bb) + X[ 3] + 0x7a6d76e9, 14) + cc; aa = RL(aa, 10);
cc = RL(cc + f2(dd,ee,aa) + X[11] + 0x7a6d76e9, 14) + bb; ee = RL(ee, 10);
bb = RL(bb + f2(cc,dd,ee) + X[15] + 0x7a6d76e9, 6) + aa; dd = RL(dd, 10);
aa = RL(aa + f2(bb,cc,dd) + X[ 0] + 0x7a6d76e9, 14) + ee; cc = RL(cc, 10);
ee = RL(ee + f2(aa,bb,cc) + X[ 5] + 0x7a6d76e9, 6) + dd; bb = RL(bb, 10);
dd = RL(dd + f2(ee,aa,bb) + X[12] + 0x7a6d76e9, 9) + cc; aa = RL(aa, 10);
cc = RL(cc + f2(dd,ee,aa) + X[ 2] + 0x7a6d76e9, 12) + bb; ee = RL(ee, 10);
bb = RL(bb + f2(cc,dd,ee) + X[13] + 0x7a6d76e9, 9) + aa; dd = RL(dd, 10);
aa = RL(aa + f2(bb,cc,dd) + X[ 9] + 0x7a6d76e9, 12) + ee; cc = RL(cc, 10);
ee = RL(ee + f2(aa,bb,cc) + X[ 7] + 0x7a6d76e9, 5) + dd; bb = RL(bb, 10);
dd = RL(dd + f2(ee,aa,bb) + X[10] + 0x7a6d76e9, 15) + cc; aa = RL(aa, 10);
cc = RL(cc + f2(dd,ee,aa) + X[14] + 0x7a6d76e9, 8) + bb; ee = RL(ee, 10);
//
// Rounds 64-79
//
// left
b = RL(b + f5(c,d,e) + X[ 4] + 0xa953fd4e, 9) + a; d = RL(d, 10);
a = RL(a + f5(b,c,d) + X[ 0] + 0xa953fd4e, 15) + e; c = RL(c, 10);
e = RL(e + f5(a,b,c) + X[ 5] + 0xa953fd4e, 5) + d; b = RL(b, 10);
d = RL(d + f5(e,a,b) + X[ 9] + 0xa953fd4e, 11) + c; a = RL(a, 10);
c = RL(c + f5(d,e,a) + X[ 7] + 0xa953fd4e, 6) + b; e = RL(e, 10);
b = RL(b + f5(c,d,e) + X[12] + 0xa953fd4e, 8) + a; d = RL(d, 10);
a = RL(a + f5(b,c,d) + X[ 2] + 0xa953fd4e, 13) + e; c = RL(c, 10);
e = RL(e + f5(a,b,c) + X[10] + 0xa953fd4e, 12) + d; b = RL(b, 10);
d = RL(d + f5(e,a,b) + X[14] + 0xa953fd4e, 5) + c; a = RL(a, 10);
c = RL(c + f5(d,e,a) + X[ 1] + 0xa953fd4e, 12) + b; e = RL(e, 10);
b = RL(b + f5(c,d,e) + X[ 3] + 0xa953fd4e, 13) + a; d = RL(d, 10);
a = RL(a + f5(b,c,d) + X[ 8] + 0xa953fd4e, 14) + e; c = RL(c, 10);
e = RL(e + f5(a,b,c) + X[11] + 0xa953fd4e, 11) + d; b = RL(b, 10);
d = RL(d + f5(e,a,b) + X[ 6] + 0xa953fd4e, 8) + c; a = RL(a, 10);
c = RL(c + f5(d,e,a) + X[15] + 0xa953fd4e, 5) + b; e = RL(e, 10);
b = RL(b + f5(c,d,e) + X[13] + 0xa953fd4e, 6) + a; d = RL(d, 10);
// right
bb = RL(bb + f1(cc,dd,ee) + X[12], 8) + aa; dd = RL(dd, 10);
aa = RL(aa + f1(bb,cc,dd) + X[15], 5) + ee; cc = RL(cc, 10);
ee = RL(ee + f1(aa,bb,cc) + X[10], 12) + dd; bb = RL(bb, 10);
dd = RL(dd + f1(ee,aa,bb) + X[ 4], 9) + cc; aa = RL(aa, 10);
cc = RL(cc + f1(dd,ee,aa) + X[ 1], 12) + bb; ee = RL(ee, 10);
bb = RL(bb + f1(cc,dd,ee) + X[ 5], 5) + aa; dd = RL(dd, 10);
aa = RL(aa + f1(bb,cc,dd) + X[ 8], 14) + ee; cc = RL(cc, 10);
ee = RL(ee + f1(aa,bb,cc) + X[ 7], 6) + dd; bb = RL(bb, 10);
dd = RL(dd + f1(ee,aa,bb) + X[ 6], 8) + cc; aa = RL(aa, 10);
cc = RL(cc + f1(dd,ee,aa) + X[ 2], 13) + bb; ee = RL(ee, 10);
bb = RL(bb + f1(cc,dd,ee) + X[13], 6) + aa; dd = RL(dd, 10);
aa = RL(aa + f1(bb,cc,dd) + X[14], 5) + ee; cc = RL(cc, 10);
ee = RL(ee + f1(aa,bb,cc) + X[ 0], 15) + dd; bb = RL(bb, 10);
dd = RL(dd + f1(ee,aa,bb) + X[ 3], 13) + cc; aa = RL(aa, 10);
cc = RL(cc + f1(dd,ee,aa) + X[ 9], 11) + bb; ee = RL(ee, 10);
bb = RL(bb + f1(cc,dd,ee) + X[11], 11) + aa; dd = RL(dd, 10);
dd += c + H1;
H1 = H2 + d + ee;
H2 = H3 + e + aa;
H3 = H4 + a + bb;
H4 = H0 + b + cc;
H0 = dd;
//
// reset the offset and clean out the word buffer.
//
xOff = 0;
for (int i = 0; i != X.length; i++)
{
X[i] = 0;
}
}
public Memoable copy()
{
return new RIPEMD160Digest(this);
}
public void reset(Memoable other)
{
RIPEMD160Digest d = (RIPEMD160Digest)other;
copyIn(d);
}
}
@@ -0,0 +1,7 @@
package com.vitorpamplona.quartz.ots.exceptions;
public class CommitmentNotFoundException extends Exception {
public CommitmentNotFoundException(String message) {
super(message);
}
}
@@ -0,0 +1,7 @@
package com.vitorpamplona.quartz.ots.exceptions;
public class DeserializationException extends Exception {
public DeserializationException(String message) {
super(message);
}
}
@@ -0,0 +1,7 @@
package com.vitorpamplona.quartz.ots.exceptions;
public class ExceededSizeException extends Exception {
public ExceededSizeException(String message) {
super(message);
}
}
@@ -0,0 +1,7 @@
package com.vitorpamplona.quartz.ots.exceptions;
public class UrlException extends Exception {
public UrlException(String message) {
super(message);
}
}
@@ -0,0 +1,7 @@
package com.vitorpamplona.quartz.ots.exceptions;
public class VerificationException extends Exception {
public VerificationException(String message) {
super(message);
}
}
@@ -0,0 +1,119 @@
package com.vitorpamplona.quartz.ots.http;
import android.util.Log;
import java.io.DataOutputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.zip.GZIPInputStream;
/**
* For making an HTTP request.
*/
public class Request implements Callable<Response> {
private URL url;
private byte[] data;
private Map<String, String> headers;
private BlockingQueue<Response> queue;
public Request(URL url) {
this.url = url;
}
public void setData(byte[] data) {
this.data = data;
}
public void setHeaders(Map<String, String> headers) {
this.headers = headers;
}
public void setQueue(BlockingQueue<Response> queue) {
this.queue = queue;
}
@Override
public Response call() throws Exception {
Response response = new Response();
try {
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setReadTimeout(10000);
httpURLConnection.setConnectTimeout(10000);
httpURLConnection.setRequestProperty("User-Agent", "OpenTimestamps Java");
httpURLConnection.setRequestProperty("Accept", "application/json");
httpURLConnection.setRequestProperty("Accept-Encoding", "gzip");
if (headers != null) {
for (Map.Entry<String, String> entry : headers.entrySet()) {
httpURLConnection.setRequestProperty(entry.getKey(), entry.getValue());
}
}
if (data != null) {
httpURLConnection.setDoOutput(true);
httpURLConnection.setRequestMethod("POST");
httpURLConnection.setRequestProperty("Content-Length", "" + Integer.toString(this.data.length));
DataOutputStream wr = new DataOutputStream(httpURLConnection.getOutputStream());
wr.write(this.data, 0, this.data.length);
wr.flush();
wr.close();
} else {
httpURLConnection.setRequestMethod("GET");
}
httpURLConnection.connect();
int responseCode = httpURLConnection.getResponseCode();
Log.i("OpenTimestamp", responseCode + " responseCode ");
response.setStatus(responseCode);
response.setFromUrl(url.toString());
InputStream is = httpURLConnection.getInputStream();
if ("gzip".equals(httpURLConnection.getContentEncoding())) {
is = new GZIPInputStream(is);
}
response.setStream(is);
} catch (Exception e) {
Log.w("OpenTimestamp", url.toString() + " exception " + e);
} finally {
if (queue != null) {
queue.offer(response);
}
}
return response;
}
public static String urlEncodeUTF8(String s) {
try {
return URLEncoder.encode(s, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new UnsupportedOperationException(e);
}
}
public static String urlEncodeUTF8(Map<?, ?> map) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<?, ?> entry : map.entrySet()) {
if (sb.length() > 0) {
sb.append("&");
}
sb.append(String.format("%s=%s",
urlEncodeUTF8(entry.getKey().toString()),
urlEncodeUTF8(entry.getValue().toString())
));
}
return sb.toString();
}
}
@@ -0,0 +1,78 @@
package com.vitorpamplona.quartz.ots.http;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
/**
* Holds the response from an HTTP request.
*/
public class Response {
private InputStream stream;
private String fromUrl;
private Integer status;
public Response() {
}
public Response(InputStream stream) {
this.stream = stream;
}
public void setStream(InputStream stream) {
this.stream = stream;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public boolean isOk() {
return getStatus() != null && 200 == getStatus();
}
public String getFromUrl() {
return fromUrl;
}
public void setFromUrl(String fromUrl) {
this.fromUrl = fromUrl;
}
public InputStream getStream() {
return this.stream;
}
public String getString() throws IOException {
return new String(getBytes(), StandardCharsets.UTF_8);
}
public byte[] getBytes() throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int nRead;
byte[] data = new byte[16384];
while ((nRead = this.stream.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
}
buffer.flush();
return buffer.toByteArray();
}
public JSONObject getJson() throws IOException, JSONException {
String jsonString = getString();
JSONObject json = new JSONObject(jsonString);
return json;
}
}
@@ -0,0 +1,139 @@
package com.vitorpamplona.quartz.ots.op;
import android.util.Log;
import com.vitorpamplona.quartz.ots.StreamDeserializationContext;
import com.vitorpamplona.quartz.ots.StreamSerializationContext;
import com.vitorpamplona.quartz.ots.exceptions.DeserializationException;
import java.util.logging.Logger;
/**
* Operations are the edges in the timestamp tree, with each operation taking a message and zero or more arguments to produce a result.
*/
public abstract class Op implements Comparable<Op> {
/**
* Maximum length of an com.vitorpamplona.quartz.ots.op.Op result
* <p>
* 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.
* <p>
* 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.
* <p>
* @see 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 com.vitorpamplona.quartz.ots.op.Op can be applied too.
* <p>
* 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.
* <p>
* com.vitorpamplona.quartz.ots.op.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 "";
}
public byte _TAG() {
return Op._TAG;
}
/**
* Deserialize operation from a buffer.
*
* @param ctx The stream deserialization context.
* @return The subclass Operation.
*/
public static Op deserialize(StreamDeserializationContext ctx) throws DeserializationException {
byte tag = ctx.readBytes(1)[0];
return Op.deserializeFromTag(ctx, tag);
}
/**
* Deserialize operation from a buffer.
*
* @param ctx The stream deserialization context.
* @param tag The tag of the operation.
* @return The subclass Operation.
*/
public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag)
throws DeserializationException {
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 if (tag == OpKECCAK256._TAG) {
return OpKECCAK256.deserializeFromTag(ctx, tag);
} else {
Log.e("OpenTimestamp", "Unknown operation tag: " + tag + " 0x" + String.format("%02x", tag));
return null; // TODO: Is this OK? Won't it blow up later? Better to throw?
}
}
/**
* Serialize operation.
*
* @param ctx The stream serialization context.
*/
public void serialize(StreamSerializationContext ctx) {
if (this._TAG() == 0x00) {
Log.e("OpenTimestamp", "No valid serialized Op");
// TODO: Is it OK to just log and carry on? Won't it blow up later? Better to throw?
}
ctx.writeByte(this._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 msg The message.
* @return the msg after the operation has been applied
*/
public byte[] call(byte[] msg) {
if (msg.length > _MAX_MSG_LENGTH) {
Log.e("OpenTimestamp", "Error : Message too long;");
return new byte[]{}; // TODO: Is this OK? Won't it blow up later? Better to throw?
}
byte[] r = this.call(msg);
if (r.length > _MAX_RESULT_LENGTH) {
Log.e("OpenTimestamp", "Error : Result too long;");
// TODO: Is it OK to just log and carry on? Won't it blow up later? Better to throw?
}
return r;
}
@Override
public int compareTo(Op o) {
return this._TAG() - o._TAG();
}
}
@@ -0,0 +1,63 @@
package com.vitorpamplona.quartz.ots.op;
import com.vitorpamplona.quartz.ots.StreamDeserializationContext;
import com.vitorpamplona.quartz.ots.Utils;
import com.vitorpamplona.quartz.ots.exceptions.DeserializationException;
import java.util.Arrays;
import java.util.logging.Logger;
/**
* Append a suffix to a message.
*
* @see OpBinary
*/
public class OpAppend extends OpBinary {
byte[] arg;
public static byte _TAG = (byte) 0xf0;
@Override
public byte _TAG() {
return OpAppend._TAG;
}
@Override
public String _TAG_NAME() {
return "append";
}
public OpAppend() {
super();
this.arg = new byte[]{};
}
public 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)
throws DeserializationException {
return OpBinary.deserializeFromTag(ctx, tag);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof OpAppend)) {
return false;
}
return Arrays.equals(this.arg, ((OpAppend) obj).arg);
}
@Override
public int hashCode() {
return _TAG ^ Arrays.hashCode(this.arg);
}
}
@@ -0,0 +1,75 @@
package com.vitorpamplona.quartz.ots.op;
import android.util.Log;
import com.vitorpamplona.quartz.encoders.Hex;
import com.vitorpamplona.quartz.ots.StreamDeserializationContext;
import com.vitorpamplona.quartz.ots.StreamSerializationContext;
import com.vitorpamplona.quartz.ots.Utils;
import com.vitorpamplona.quartz.ots.exceptions.DeserializationException;
import java.util.Arrays;
/**
* Operations that act on a message and a single argument.
*
* @see OpUnary
*/
public abstract class OpBinary extends Op implements Comparable<Op> {
public byte[] arg;
@Override
public String _TAG_NAME() {
return "";
}
public OpBinary() {
super();
this.arg = new byte[]{};
}
public OpBinary(byte[] arg_) {
super();
this.arg = arg_;
}
public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag)
throws DeserializationException {
byte[] arg = ctx.readVarbytes(_MAX_RESULT_LENGTH, 1);
if (tag == OpAppend._TAG) {
return new OpAppend(arg);
} else if (tag == OpPrepend._TAG) {
return new OpPrepend(arg);
} else {
Log.e("OpenTimestamp", "Unknown operation tag: " + tag + " 0x" + String.format("%02x", tag));
return null; // TODO: Is this OK? Won't it blow up later? Better to throw?
}
}
@Override
public void serialize(StreamSerializationContext ctx) {
super.serialize(ctx);
ctx.writeVarbytes(this.arg);
}
@Override
public String toString() {
return this._TAG_NAME() + ' ' + Hex.encode(this.arg).toLowerCase();
}
@Override
public int compareTo(Op o) {
if (o instanceof OpBinary && this._TAG() == o._TAG()) {
return Utils.compare(this.arg, ((OpBinary) o).arg);
}
return this._TAG() - o._TAG();
}
@Override
public int hashCode() {
return _TAG ^ Arrays.hashCode(this.arg);
}
}
@@ -0,0 +1,97 @@
package com.vitorpamplona.quartz.ots.op;
import android.util.Log;
import com.vitorpamplona.quartz.ots.StreamDeserializationContext;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* 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.
*
* @see OpUnary
*/
public class OpCrypto extends OpUnary {
public String _TAG_NAME = "";
public String _HASHLIB_NAME() {
return "";
}
public int _DIGEST_LENGTH() {
return 0;
}
OpCrypto() {
super();
}
public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag) {
return OpUnary.deserializeFromTag(ctx, tag);
}
@Override
public byte[] call(byte[] msg) {
// 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) {
Log.e("OpenTimestamp", "NoSuchAlgorithmException");
e.printStackTrace();
return new byte[]{}; // TODO: Is this OK? Won't it blow up later? Better to throw?
}
}
public byte[] hashFd(StreamDeserializationContext ctx) throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance(this._HASHLIB_NAME());
byte[] chunk = ctx.read(1048576);
while (chunk != null && chunk.length > 0) {
digest.update(chunk);
chunk = ctx.read(1048576);
}
byte[] hash = digest.digest();
return hash;
}
public byte[] hashFd(File file) throws IOException, NoSuchAlgorithmException {
return hashFd(new FileInputStream(file));
}
public byte[] hashFd(byte[] bytes) throws IOException, NoSuchAlgorithmException {
StreamDeserializationContext ctx = new StreamDeserializationContext(bytes);
return hashFd(ctx);
}
public byte[] hashFd(InputStream inputStream) throws IOException, NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance(this._HASHLIB_NAME());
byte[] chunk = new byte[1048576];
int count = inputStream.read(chunk, 0, 1048576);
while (count >= 0) {
digest.update(chunk, 0, count);
count = inputStream.read(chunk, 0, 1048576);
}
inputStream.close();
byte[] hash = digest.digest();
return hash;
}
}
@@ -0,0 +1,62 @@
package com.vitorpamplona.quartz.ots.op;
import com.vitorpamplona.quartz.ots.StreamDeserializationContext;
import com.vitorpamplona.quartz.ots.Utils;
import com.vitorpamplona.quartz.ots.crypto.KeccakDigest;
import java.util.logging.Logger;
/**
* Cryptographic Keccak256 operation.
* Cryptographic operation tag numbers taken from RFC4880, although it's not
* guaranteed that they'll continue to match that RFC in the future.
*
* @see OpCrypto
*/
public class OpKECCAK256 extends OpCrypto {
private KeccakDigest digest = new KeccakDigest(256);
public static byte _TAG = (byte) 103;
@Override
public byte _TAG() {
return OpKECCAK256._TAG;
}
@Override
public String _TAG_NAME() {
return "keccak256";
}
@Override
public String _HASHLIB_NAME() {
return "keccak256";
}
@Override
public int _DIGEST_LENGTH() {
return digest.getDigestSize();
}
public OpKECCAK256() {
super();
}
@Override
public byte[] call(byte[] msg) {
digest.update(msg, 0, msg.length);
byte[] hash = new byte[digest.getDigestSize()];
digest.doFinal(hash, 0);
return hash;
}
public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag) {
return OpCrypto.deserializeFromTag(ctx, tag);
}
@Override
public boolean equals(Object obj) {
return (obj instanceof OpKECCAK256);
}
}
@@ -0,0 +1,64 @@
package com.vitorpamplona.quartz.ots.op;
import com.vitorpamplona.quartz.ots.StreamDeserializationContext;
import com.vitorpamplona.quartz.ots.Utils;
import com.vitorpamplona.quartz.ots.exceptions.DeserializationException;
import java.util.Arrays;
import java.util.logging.Logger;
/**
* Prepend a prefix to a message.
*
* @see OpBinary
*/
public class OpPrepend extends OpBinary {
byte[] arg;
public static byte _TAG = (byte) 0xf1;
@Override
public byte _TAG() {
return OpPrepend._TAG;
}
@Override
public String _TAG_NAME() {
return "prepend";
}
public OpPrepend() {
super();
this.arg = new byte[]{};
}
public 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)
throws DeserializationException {
return OpBinary.deserializeFromTag(ctx, tag);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof OpPrepend)) {
return false;
}
return Arrays.equals(this.arg, ((OpPrepend) obj).arg);
}
@Override
public int hashCode() {
return _TAG ^ Arrays.hashCode(this.arg);
}
}
@@ -0,0 +1,67 @@
package com.vitorpamplona.quartz.ots.op;
import com.vitorpamplona.quartz.ots.StreamDeserializationContext;
import com.vitorpamplona.quartz.ots.Utils;
import com.vitorpamplona.quartz.ots.crypto.RIPEMD160Digest;
import java.util.logging.Logger;
/**
* 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.
*
* @see OpCrypto
*/
public class OpRIPEMD160 extends OpCrypto {
public static byte _TAG = 0x03;
@Override
public byte _TAG() {
return OpRIPEMD160._TAG;
}
@Override
public String _TAG_NAME() {
return "ripemd160";
}
@Override
public String _HASHLIB_NAME() {
return "ripemd160";
}
@Override
public int _DIGEST_LENGTH() {
return 20;
}
public OpRIPEMD160() {
super();
}
@Override
public byte[] call(byte[] msg) {
RIPEMD160Digest digest = new RIPEMD160Digest();
digest.update(msg, 0, msg.length);
byte[] hash = new byte[digest.getDigestSize()];
digest.doFinal(hash, 0);
return hash;
}
public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag) {
return OpCrypto.deserializeFromTag(ctx, tag);
}
@Override
public boolean equals(Object obj) {
return (obj instanceof OpRIPEMD160);
}
@Override
public int hashCode() {
return _TAG;
}
}
@@ -0,0 +1,68 @@
package com.vitorpamplona.quartz.ots.op;
import com.vitorpamplona.quartz.ots.StreamDeserializationContext;
import com.vitorpamplona.quartz.ots.Utils;
import java.util.logging.Logger;
/**
* 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...
*
* @see OpCrypto
*/
public class OpSHA1 extends OpCrypto {
public static byte _TAG = 0x02;
@Override
public byte _TAG() {
return OpSHA1._TAG;
}
@Override
public String _TAG_NAME() {
return "sha1";
}
@Override
public String _HASHLIB_NAME() {
return "SHA-1";
}
@Override
public int _DIGEST_LENGTH() {
return 20;
}
public OpSHA1() {
super();
}
@Override
public byte[] call(byte[] msg) {
return super.call(msg);
}
public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag) {
return OpCrypto.deserializeFromTag(ctx, tag);
}
@Override
public boolean equals(Object obj) {
return (obj instanceof OpSHA1);
}
@Override
public int hashCode() {
return _TAG;
}
}
@@ -0,0 +1,62 @@
package com.vitorpamplona.quartz.ots.op;
import com.vitorpamplona.quartz.ots.StreamDeserializationContext;
import com.vitorpamplona.quartz.ots.Utils;
import java.util.logging.Logger;
/**
* 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.
*
* @see OpCrypto
*/
public class OpSHA256 extends OpCrypto {
public static byte _TAG = 0x08;
@Override
public byte _TAG() {
return OpSHA256._TAG;
}
@Override
public String _TAG_NAME() {
return "sha256";
}
@Override
public String _HASHLIB_NAME() {
return "SHA-256";
}
@Override
public int _DIGEST_LENGTH() {
return 32;
}
public OpSHA256() {
super();
}
@Override
public byte[] call(byte[] msg) {
return super.call(msg);
}
public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag) {
return OpCrypto.deserializeFromTag(ctx, tag);
}
@Override
public boolean equals(Object obj) {
return (obj instanceof OpSHA256);
}
@Override
public int hashCode() {
return _TAG;
}
}
@@ -0,0 +1,46 @@
package com.vitorpamplona.quartz.ots.op;
import android.util.Log;
import com.vitorpamplona.quartz.ots.StreamDeserializationContext;
import com.vitorpamplona.quartz.ots.Utils;
import java.util.logging.Logger;
/**
* Operations that act on a single message.
*
* @see Op
*/
public abstract class OpUnary extends Op {
@Override
public String _TAG_NAME() {
return "";
}
public OpUnary() {
super();
}
public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag) {
if (tag == OpSHA1._TAG) {
return new OpSHA1();
} else if (tag == OpSHA256._TAG) {
return new OpSHA256();
} else if (tag == OpRIPEMD160._TAG) {
return new OpRIPEMD160();
} else if (tag == OpKECCAK256._TAG) {
return new OpKECCAK256();
} else {
Log.e("OpenTimestamp", "Unknown operation tag: " + tag);
return null; // TODO: Is this OK? Won't it blow up later? Better to throw?
}
}
@Override
public String toString() {
return this._TAG_NAME();
}
}