/*
* Part of the CCNx Java Library.
*
* Copyright (C) 2008, 2009 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.profiles.security.access.group;
import java.security.InvalidKeyException;
import java.security.Key;
import java.util.Arrays;
import java.util.logging.Level;
import javax.crypto.spec.SecretKeySpec;
import org.ccnx.ccn.impl.security.crypto.CCNDigestHelper;
import org.ccnx.ccn.impl.security.crypto.KeyDerivationFunction;
import org.ccnx.ccn.impl.support.DataUtils;
import org.ccnx.ccn.impl.support.Log;
import org.ccnx.ccn.io.content.ContentEncodingException;
import org.ccnx.ccn.io.content.WrappedKey;
import org.ccnx.ccn.profiles.VersionMissingException;
import org.ccnx.ccn.profiles.VersioningProfile;
import org.ccnx.ccn.protocol.CCNTime;
import org.ccnx.ccn.protocol.ContentName;
/**
* This class represents node keys.
* It includes methods for computing derived node keys for descendant nodes
* using a key derivation function. For a definition and description of node keys,
* see the CCNx Access Control Specification.
*/
public class NodeKey {
/**
* Default data key length in bytes. No real reason this can't be bumped up to 32. It
* acts as the seed for a KDF, not an encryption key.
*/
public static final int DEFAULT_NODE_KEY_LENGTH = 16;
/**
* The keys we're wrapping are really seeds for a KDF, not keys in their own right.
* Eventually we'll use CMAC, so call them AES...
*/
public static final String DEFAULT_NODE_KEY_ALGORITHM = "AES";
/**
* Default key label for key derivation function.
*/
public static final String DEFAULT_KEY_LABEL = "NodeKey";
/**
* KeyID for empty keys (signaling no encryption).
*/
public static final byte [] NULL_NODE_KEY_ID = "NULL_KEY".getBytes();
/**
* The node this key is associated with, with <access marker> information stripped.
*/
private ContentName _nodeName;
/**
* The full name of the stored node key that is either this key itself,
* or the ancestor node key this is derived from, including its version information.
*/
private ContentName _storedNodeKeyName;
private byte [] _storedNodeKeyID;
/**
* The unwrapped node key
*/
private Key _nodeKey;
/**
* Constructor for a node key specified by its name and key bytes
* interpreted as a key for DEFAULT_NODE_KEY_ALGORITHM.
* @param nodeKeyName the name of the node key
* @param unwrappedNodeKey the unwrapped node key
*/
public NodeKey(ContentName nodeKeyName, byte [] unwrappedNodeKey) {
this(nodeKeyName, new SecretKeySpec(unwrappedNodeKey, DEFAULT_NODE_KEY_ALGORITHM));
}
/**
* Constructor for a node key specified by its name and key.
* @param nodeKeyName the name of the node key
* @param unwrappedNodeKey the unwrapped node key
*/
public NodeKey(ContentName nodeKeyName, Key unwrappedNodeKey) {
if (null == nodeKeyName) {
throw new IllegalArgumentException("NodeKey: key name and key cannot be null!");
}
// DKS TODO make sure the version is of the NK, not the blocks underneath it.
if (!VersioningProfile.hasTerminalVersion(nodeKeyName)) {
throw new IllegalArgumentException("Expect stored node key name to be versioned: " + nodeKeyName);
}
_storedNodeKeyName = nodeKeyName;
_storedNodeKeyID = (null == unwrappedNodeKey) ? nullNodeKeyID() : generateKeyID(unwrappedNodeKey.getEncoded());
_nodeKey = unwrappedNodeKey;
_nodeName = GroupAccessControlProfile.accessRoot(nodeKeyName);
if ((null == _nodeName) || (!GroupAccessControlProfile.isNodeKeyName(nodeKeyName))) {
throw new IllegalArgumentException("NodeKey: key name " + nodeKeyName + " is not a valid node key name.");
}
}
/**
* Constructor for a node key derived (via a key derivation function)
* from an ancestor node key.
* @param nodeName the name of the node
* @param derivedNodeKey the derived node key
* @param ancestorNodeKeyName the name of the ancestor node key
* @param ancestorNodeKeyID the digest of the ancestor node key
*/
protected NodeKey(ContentName nodeName, byte [] derivedNodeKey,
ContentName ancestorNodeKeyName, byte [] ancestorNodeKeyID) {
if (!VersioningProfile.hasTerminalVersion(ancestorNodeKeyName)) {
// DKS TODO make sure the version is of the NK, not the blocks underneath it.
throw new IllegalArgumentException("Expect stored node key name to be versioned: " + ancestorNodeKeyName);
}
_storedNodeKeyName = ancestorNodeKeyName;
_storedNodeKeyID = ancestorNodeKeyID;
_nodeName = nodeName;
_nodeKey = new SecretKeySpec(derivedNodeKey, DEFAULT_NODE_KEY_ALGORITHM);
}
/**
* Computes the descendant node key for a specified descendant node
* using the key derivation function.
* @param descendantNodeName the name of the descendant node
* @param keyLabel the label of the key
* @return the node key
* @throws InvalidKeyException
* @throws ContentEncodingException
*/
public NodeKey computeDescendantNodeKey(ContentName descendantNodeName, String keyLabel)
throws InvalidKeyException, ContentEncodingException {
if (nodeName().equals(descendantNodeName)) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL,"Asked to compute ourselves as our own descendant (node key " + nodeName() +"), returning this.");
}
return this;
}
if (!nodeName().isPrefixOf(descendantNodeName)) {
throw new IllegalArgumentException("Node " + descendantNodeName + " is not a child of this node " + nodeName());
}
byte [] derivedKey = KeyDerivationFunction.DeriveKeyForNode(nodeName(), nodeKey().getEncoded(), keyLabel, descendantNodeName);
return new NodeKey(descendantNodeName, derivedKey, storedNodeKeyName(), storedNodeKeyID());
}
public NodeKey computeDescendantNodeKey(ContentName descendantNodeName)
throws InvalidKeyException, ContentEncodingException {
return computeDescendantNodeKey(descendantNodeName, DEFAULT_KEY_LABEL);
}
/**
* Get the node name.
* @return the node name.
*/
public ContentName nodeName() { return _nodeName; }
/**
* Get the stored node key name.
* @return the stored node key name.
*/
public ContentName storedNodeKeyName() { return _storedNodeKeyName; }
/**
* Get the stored node key ID
* @return the stored node key ID
*/
public byte [] storedNodeKeyID() { return _storedNodeKeyID; }
/**
* Get the node key
* @return the node key
*/
public Key nodeKey() { return _nodeKey; }
/**
* Check whether the node key is derived from an ancestor node key
* via the key derivation function
* @return
*/
public boolean isDerivedNodeKey() {
return (!nodeName().isPrefixOf(storedNodeKeyName()));
}
/**
* Emtpy key, signaling no encryption.
* @return
*/
public boolean isNullNodeKey() {
return (null == nodeKey());
}
public static byte [] nullNodeKeyID() {
return NULL_NODE_KEY_ID;
}
/**
* Get the version of the stored node key name
* @return the version
*/
public CCNTime nodeKeyVersion() {
try {
return VersioningProfile.getLastVersionAsTimestamp(storedNodeKeyName());
} catch (VersionMissingException e) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.WARNING)) {
Log.warning(Log.FAC_ACCESSCONTROL, "Unexpected: name that was confirmed to have a version on construction throws a VersionMissingException: " + storedNodeKeyName());
}
throw new IllegalStateException("Unexpected: name that was confirmed to have a version on construction throws a VersionMissingException: " + storedNodeKeyName());
}
}
/**
* Returns a digest of the node key.
* @return the digest
*/
public byte [] generateKeyID() {
return generateKeyID(nodeKey().getEncoded());
}
/**
* Returns a digest of a specified key
* @param key the key
* @return the digest
*/
public static byte [] generateKeyID(byte [] key) {
return CCNDigestHelper.digest(key);
}
/**
* Returns a digest of a specified key
* @param key the key
* @return the digest
*/
public static byte [] generateKeyID(Key key) {
if (null == key)
return null;
return CCNDigestHelper.digest(key.getEncoded());
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((_nodeKey == null) ? 0 : Arrays.hashCode(_nodeKey.getEncoded()));
result = prime * result
+ ((_nodeName == null) ? 0 : _nodeName.hashCode());
result = prime * result + Arrays.hashCode(_storedNodeKeyID);
result = prime
* result
+ ((_storedNodeKeyName == null) ? 0 : _storedNodeKeyName
.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;
NodeKey other = (NodeKey) obj;
if (_nodeKey == null) {
if (other._nodeKey != null)
return false;
} else if (other._nodeKey == null) {
return false;
} else if (!Arrays.equals(_nodeKey.getEncoded(), other._nodeKey.getEncoded())) {
return false;
}
if (_nodeName == null) {
if (other._nodeName != null)
return false;
} else if (!_nodeName.equals(other._nodeName))
return false;
if (!Arrays.equals(_storedNodeKeyID, other._storedNodeKeyID))
return false;
if (_storedNodeKeyName == null) {
if (other._storedNodeKeyName != null)
return false;
} else if (!_storedNodeKeyName.equals(other._storedNodeKeyName))
return false;
return true;
}
@Override
public String toString() {
if (null == _nodeKey) {
return "NodeKey for node: " + _nodeName + " Stored at: " + _storedNodeKeyName +
" Stored ID: " + DataUtils.printHexBytes(_storedNodeKeyID) +
" Key: null" + " Key id: null";
}
// not great to print out keys, but nec for debugging
return "NodeKey for node: " + _nodeName + " Stored at: " + _storedNodeKeyName +
" Stored ID: " + DataUtils.printHexBytes(_storedNodeKeyID) +
" Key: " + DataUtils.printHexBytes(_nodeKey.getEncoded()) +
" Key id: " + DataUtils.printHexBytes(WrappedKey.wrappingKeyIdentifier(_nodeKey));
}
}