/*
* Part of the CCNx Java Library.
*
* Copyright (C) 2008, 2009, 2012 Palo Alto Research Center, Inc.
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License version 2.1
* as published by the Free Software Foundation.
* This library 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 for more details. You should have received
* a copy of the GNU Lesser General Public License along with this library;
* if not, write to the Free Software Foundation, Inc., 51 Franklin Street,
* Fifth Floor, Boston, MA 02110-1301 USA.
*/
package org.ccnx.ccn.protocol;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map.Entry;
import org.ccnx.ccn.impl.encoding.CCNProtocolDTags;
import org.ccnx.ccn.impl.encoding.GenericXMLEncodable;
import org.ccnx.ccn.impl.encoding.XMLDecoder;
import org.ccnx.ccn.impl.encoding.XMLEncodable;
import org.ccnx.ccn.impl.encoding.XMLEncoder;
import org.ccnx.ccn.impl.support.DataUtils;
import org.ccnx.ccn.impl.support.Log;
import org.ccnx.ccn.io.content.ContentDecodingException;
import org.ccnx.ccn.io.content.ContentEncodingException;
/**
* SignedInfo is the metadata portion of a ContentObject that contains information about that
* object which is signed by the publisher. It incluedes a timestamp, which doesn't
* imply a certified notion of time -- it requires loose consistency within that publisher
* only. It lets you order things with the same name, or signed by the same publisher. From the security
* point of view it acts primarily as a nonce, and we use a timestamp for it to make it slightly
* more useful than a random nonce.
* <p>
* You can think of the SignedInfo as
* <br>
* a) the stuff "about" a piece of CCN data that it is legal to expect routers to understand and
* use (as opposed to names, which are opaque), which is
* <br>
* b) pulled out as metadata rather than baked into the names both to allow this opaqueness and
* to deal with the fact that name matching is strictly component-wise ordered (you can't
* look for /parc/foo/*/bar for reasons of routing efficiency, except through a slower
* search style interaction), and these items want to be matched in any order -- in other
* words, sometimes you want to find /obj/timestamp and sometimes /obj/publisher and
* you can't decide which should go first in the name.
*/
public class SignedInfo extends GenericXMLEncodable implements XMLEncodable {
/*
* The binary encodings for the types are chosen to print well as they go by in packet dumps.
*/
public enum ContentType {DATA, ENCR, GONE, KEY, LINK, NACK};
public static final byte [] DATA_VAL = new byte[]{(byte)0x0c, (byte)0x04, (byte)0xc0};
public static final byte [] ENCR_VAL = new byte[]{(byte)0x10, (byte)0xd0, (byte)0x91};
public static final byte [] GONE_VAL = new byte[]{(byte)0x18, (byte)0xe3, (byte)0x44};
public static final byte [] KEY_VAL = new byte[]{(byte)0x28, (byte)0x46, (byte)0x3f};
public static final byte [] LINK_VAL = new byte[]{(byte)0x2c, (byte)0x83, (byte)0x4a};
public static final byte [] NACK_VAL = new byte[]{(byte)0x34, (byte)0x00, (byte)0x8a};
protected static final HashMap<ContentType, String> ContentTypeNames = new HashMap<ContentType, String>();
protected static final HashMap<String, ContentType> ContentNameTypes = new HashMap<String, ContentType>();
protected static final HashMap<ContentType, byte[]> ContentTypeValues = new HashMap<ContentType, byte[]>();
// This doesn't actually work as a hash table; lookup of byte [] doesn't have a proper hashCode function.
// turns into object ==.
protected static final HashMap<byte[], ContentType> ContentValueTypes = new HashMap<byte[], ContentType>();
// These are encoded as 3-byte binary values, whose base64 encodings
// are chosen to make sense and look like the tags.
static {
ContentTypeNames.put(ContentType.DATA, "DATA");
ContentTypeNames.put(ContentType.ENCR, "ENCR");
ContentTypeNames.put(ContentType.GONE, "GONE");
ContentTypeNames.put(ContentType.KEY, "KEY/");
ContentTypeNames.put(ContentType.LINK, "LINK");
ContentTypeNames.put(ContentType.NACK, "NACK");
ContentNameTypes.put("DATA", ContentType.DATA);
ContentNameTypes.put("ENCR", ContentType.ENCR);
ContentNameTypes.put("GONE", ContentType.GONE);
ContentNameTypes.put("KEY/", ContentType.KEY);
ContentNameTypes.put("LINK", ContentType.LINK);
ContentNameTypes.put("NACK", ContentType.NACK);
ContentTypeValues.put(ContentType.DATA, DATA_VAL);
ContentTypeValues.put(ContentType.ENCR, ENCR_VAL);
ContentTypeValues.put(ContentType.GONE, GONE_VAL);
ContentTypeValues.put(ContentType.KEY, KEY_VAL);
ContentTypeValues.put(ContentType.LINK, LINK_VAL);
ContentTypeValues.put(ContentType.NACK, NACK_VAL);
ContentValueTypes.put(DATA_VAL, ContentType.DATA);
ContentValueTypes.put(ENCR_VAL, ContentType.ENCR);
ContentValueTypes.put(GONE_VAL, ContentType.GONE);
ContentValueTypes.put(KEY_VAL, ContentType.KEY);
ContentValueTypes.put(LINK_VAL, ContentType.LINK);
ContentValueTypes.put(NACK_VAL, ContentType.NACK);
}
protected PublisherPublicKeyDigest _publisher;
protected CCNTime _timestamp;
protected ContentType _type;
protected KeyLocator _locator;
protected Integer _freshnessSeconds;
protected byte [] _finalBlockID;
protected byte [] _extOpt;
/**
* Constructor
* @param publisher
* @param locator
*/
public SignedInfo(
PublisherPublicKeyDigest publisher,
KeyLocator locator
) {
this(publisher, null, null, locator);
}
/**
* Constructor
* @param publisher
* @param type
* @param locator
*/
public SignedInfo(
PublisherPublicKeyDigest publisher,
ContentType type,
KeyLocator locator
) {
this(publisher, null, type, locator);
}
/**
* Constructor
* @param publisher
* @param timestamp
* @param type
* @param locator
*/
public SignedInfo(
PublisherPublicKeyDigest publisher,
CCNTime timestamp,
ContentType type,
KeyLocator locator) {
this(publisher, timestamp, type, locator, null, null);
}
/**
* Constructor
* @param publisher
* @param type
* @param locator
* @param freshnessSeconds
* @param finalBlockID
*/
public SignedInfo(
PublisherPublicKeyDigest publisher,
ContentType type,
KeyLocator locator,
Integer freshnessSeconds,
byte [] finalBlockID
) {
this(publisher, null, type, locator, freshnessSeconds, finalBlockID);
}
/**
* Constructor
* @param publisher
* @param timestamp
* @param type
* @param locator
* @param freshnessSeconds
* @param finalBlockID
*/
public SignedInfo(
PublisherPublicKeyDigest publisher,
CCNTime timestamp,
ContentType type,
KeyLocator locator,
Integer freshnessSeconds,
byte [] finalBlockID
) {
this(publisher, timestamp, type, locator, freshnessSeconds, finalBlockID, null);
};
/**
* Constructor
* @param publisher
* @param timestamp
* @param type
* @param locator
* @param freshnessSeconds
* @param finalBlockID
* @param extOpt
*/
public SignedInfo(
PublisherPublicKeyDigest publisher,
CCNTime timestamp,
ContentType type,
KeyLocator locator,
Integer freshnessSeconds,
byte [] finalBlockID,
byte [] extOpt
) {
super();
this._publisher = publisher;
if (null == timestamp) {
this._timestamp = CCNTime.now(); // msec only
} else {
this._timestamp = timestamp;
}
this._type = (null == type) ? ContentType.DATA : type;
this._locator = locator;
this._freshnessSeconds = freshnessSeconds;
this._finalBlockID = finalBlockID;
this._extOpt = extOpt;
}
/**
* Copy constructor
* @param other
*/
public SignedInfo(SignedInfo other) {
this(other.getPublisherKeyID(),
other.getTimestamp(),
other.getType(),
other.getKeyLocator(),
other.getFreshnessSeconds(),
other.getFinalBlockID(),
other.getExtOpt());
}
/**
* For decoders
*/
public SignedInfo() {}
/**
* Implement Cloneable
*/
public SignedInfo clone() {
// more clonage needed
KeyLocator kl = getKeyLocator();
return new SignedInfo(getPublisherKeyID(), getTimestamp(), getType(),
null == kl ? null : kl.clone(),
getFreshnessSeconds(), getFinalBlockID());
}
/**
* Do we have a publisher?
* @return True if the publisher is empty
*/
public boolean emptyPublisher() {
if ((null != getPublisherKeyID()) && (0 != getPublisher().length))
return false;
return true;
}
/**
* True if we're using the default content type
* @return true if we're using the default content type
*/
public boolean defaultContentType() {
return ((null == _type) || (ContentType.DATA == _type));
}
/**
* Do we have a timestamp?
* @return true if timestamp is empty, false if we have one
*/
public boolean emptyTimestamp() {
return (null == _timestamp);
}
/**
* Do we have a key locator
* @return true if KeyLocator is empty, false if we have one
*/
public boolean emptyKeyLocator() {
return (null == _locator);
}
/**
* Do we have an ExtOpt
* @return true if KeyLocator is empty, false if we have one
*/
public boolean emptyExtOpt() {
return (null == _extOpt);
}
/**
* Return the publisher
* @return the publisher
*/
public final byte[] getPublisher() { return _publisher.digest(); }
public final PublisherPublicKeyDigest getPublisherKeyID() { return _publisher; }
/**
* Get the timestamp
* @return the timestamp
*/
public final CCNTime getTimestamp() { return _timestamp; }
/**
* Get the KeyLocator. This is optional in encoding, and can be null
* @return the KeyLocator
*/
public final KeyLocator getKeyLocator() { return _locator; }
/**
* Get the freshness seconds
* @return the freshnessSeconds
*/
public final int getFreshnessSeconds() { return _freshnessSeconds; }
/**
* Do we have a value for freshnessSeconds?
* @return true if the freshnessSeconds is emtpy, false if it is set
*/
public boolean emptyFreshnessSeconds() {
return (null == _freshnessSeconds);
}
/**
* Get the finalBlockID as binary
* @return the finalBlockID
*/
public final byte [] getFinalBlockID() { return _finalBlockID; }
/**
* Do we have a finalBlockID set?
* @return true if the finalBlockID is empty, false if we have one set
*/
public boolean emptyFinalBlockID() {
return (null == _finalBlockID);
}
/**
* Set the finalBlockID for this set of content segments
* @param finalBlockID the new finalBlockID as binary
*/
public void setFinalBlockID(byte [] finalBlockID) { _finalBlockID = finalBlockID; }
/**
* Get the extOpt as binary
* @return the extOpt
*/
public void setExtOpt(byte [] extOpt) { _extOpt = extOpt; }
/**
* Get the extOpt as binary
* @return the extOpt
*/
public final byte [] getExtOpt() { return _extOpt; }
/**
* Set the content type for this content object
* @param type
*/
public void setType(ContentType type) {
if (null == type) {
_type = ContentType.DATA;
} else {
_type = type;
}
}
/**
* Get our content type.
* @return
*/
public final ContentType getType() {
if (null == _type)
return ContentType.DATA;
return _type;
}
/**
* Get the String representation of our content type.
* @return
*/
public String getTypeName() { return typeToName(getType()); }
/**
* String/enum conversions, unnecessary, will be removed
* @param type
* @return
*/
public static final String typeToName(ContentType type) {
if (ContentTypeNames.get(type) == null) {
Log.warning("Cannot find name for type: " + type);
}
return ContentTypeNames.get(type);
}
/**
* String/enum conversions, unnecessary, will be removed
* @param name
* @return
*/
public static final ContentType nameToType(String name) {
return ContentNameTypes.get(name);
}
/**
* Get our type as a binary value
* @return the binary value of our type
*/
public byte [] getTypeValue() { return typeToValue(getType()); }
/**
* Convert between ContentType and its binary representation.
* Unfortunately, straight hash table lookup doesn't work right on byte array
* keys. Have to do straight comparison. Could speed it up from linear
* search, but for 5 types, might not matter.
* @param type
* @return the binary value
*/
public static final byte [] typeToValue(ContentType type) {
if (ContentTypeValues.get(type) == null) {
Log.warning("Cannot find name for type: " + type);
}
return ContentTypeValues.get(type);
}
/**
* Convert between ContentType and its binary representation.
* Unfortunately, straight hash table lookup doesn't work right on byte array
* keys. Have to do straight comparison. Could speed it up from linear
* search, but for 5 types, might not matter.
* @param value
* @return the ContentType
*/
public static final ContentType valueToType(byte [] value) {
for (Entry<byte [], ContentType> entry : ContentValueTypes.entrySet()) {
if (Arrays.equals(value, entry.getKey()))
return entry.getValue();
}
return null;
}
@Override
public void decode(XMLDecoder decoder) throws ContentDecodingException {
decoder.readStartElement(getElementLabel());
if (decoder.peekStartElement(CCNProtocolDTags.PublisherPublicKeyDigest)) {
_publisher = new PublisherPublicKeyDigest();
_publisher.decode(decoder);
}
if (decoder.peekStartElement(CCNProtocolDTags.Timestamp)) {
_timestamp = decoder.readDateTime(CCNProtocolDTags.Timestamp);
}
if (decoder.peekStartElement(CCNProtocolDTags.Type)) {
byte [] binType = decoder.readBinaryElement(CCNProtocolDTags.Type);
_type = valueToType(binType);
if (null == _type) {
throw new ContentDecodingException("Cannot parse signedInfo type: " + DataUtils.printHexBytes(binType) + " " + binType.length + " bytes.");
}
} else {
_type = ContentType.DATA; // default
}
if (decoder.peekStartElement(CCNProtocolDTags.FreshnessSeconds)) {
_freshnessSeconds = decoder.readIntegerElement(CCNProtocolDTags.FreshnessSeconds);
}
if (decoder.peekStartElement(CCNProtocolDTags.FinalBlockID)) {
_finalBlockID = decoder.readBinaryElement(CCNProtocolDTags.FinalBlockID);
}
if (decoder.peekStartElement(CCNProtocolDTags.KeyLocator)) {
_locator = new KeyLocator();
_locator.decode(decoder);
}
if (decoder.peekStartElement(CCNProtocolDTags.ExtOpt)) {
_extOpt = decoder.readBinaryElement(CCNProtocolDTags.ExtOpt);
}
decoder.readEndElement();
}
@Override
public void encode(XMLEncoder encoder) throws ContentEncodingException {
if (!validate()) {
throw new ContentEncodingException("Cannot encode " + this.getClass().getName() + ": field values missing.");
}
encoder.writeStartElement(getElementLabel());
if (!emptyPublisher()) {
getPublisherKeyID().encode(encoder);
}
if (!emptyTimestamp()) {
encoder.writeDateTime(CCNProtocolDTags.Timestamp, getTimestamp());
}
if (!defaultContentType()) {
// DATA is default, element is optional, so omit if DATA
encoder.writeElement(CCNProtocolDTags.Type, getTypeValue());
}
if (!emptyFreshnessSeconds()) {
encoder.writeElement(CCNProtocolDTags.FreshnessSeconds, getFreshnessSeconds());
}
if (!emptyFinalBlockID()) {
encoder.writeElement(CCNProtocolDTags.FinalBlockID, getFinalBlockID());
}
if (!emptyKeyLocator()) {
getKeyLocator().encode(encoder);
}
if (!emptyExtOpt()) {
encoder.writeElement(CCNProtocolDTags.ExtOpt, getExtOpt());
}
encoder.writeEndElement();
}
@Override
public long getElementLabel() { return CCNProtocolDTags.SignedInfo; }
@Override
public boolean validate() {
// We don't do partial matches any more, even though encoder/decoder
// is still pretty generous.
if (emptyPublisher() || emptyTimestamp())
return false;
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(_finalBlockID);
result = prime
* result
+ ((_freshnessSeconds == null) ? 0 : _freshnessSeconds
.hashCode());
result = prime * result
+ ((_locator == null) ? 0 : _locator.hashCode());
result = prime * result
+ ((_publisher == null) ? 0 : _publisher.hashCode());
result = prime * result
+ ((_timestamp == null) ? 0 : _timestamp.hashCode());
result = prime * result + ((_type == null) ? 0 : _type.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
SignedInfo other = (SignedInfo) obj;
if (!Arrays.equals(_finalBlockID, other._finalBlockID))
return false;
if (_freshnessSeconds == null) {
if (other._freshnessSeconds != null)
return false;
} else if (!_freshnessSeconds.equals(other._freshnessSeconds))
return false;
if (_locator == null) {
if (other._locator != null)
return false;
} else if (!_locator.equals(other._locator))
return false;
if (_publisher == null) {
if (other._publisher != null)
return false;
} else if (!_publisher.equals(other._publisher))
return false;
if (_timestamp == null) {
if (other._timestamp != null)
return false;
} else if (!_timestamp.equals(other._timestamp))
return false;
if (_type == null) {
if (other._type != null)
return false;
} else if (!_type.equals(other._type))
return false;
return true;
}
public String toString() {
StringBuffer s = new StringBuffer();
if (_type != null) s.append(String.format("si: type=%s", typeToName(_type)));
s.append(String.format("si: timestamp=%s", _timestamp));
s.append(String.format("si: pub=%s", _publisher));
if (_locator != null) s.append(String.format("si: loc=%s", _locator));
if (_freshnessSeconds != null) s.append(String.format("si: type=%s", _freshnessSeconds));
return s.toString();
}
}