/*
* Part of the CCNx Java Library.
*
* Copyright (C) 2008-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.profiles.security.access.group;
import static org.ccnx.ccn.io.content.KeyDirectory.GROUP_PRIVATE_KEY;
import static org.ccnx.ccn.io.content.KeyDirectory.GROUP_PUBLIC_KEY;
import org.bouncycastle.util.Arrays;
import org.ccnx.ccn.impl.security.crypto.CCNDigestHelper;
import org.ccnx.ccn.impl.support.DataUtils;
import org.ccnx.ccn.impl.support.Log;
import org.ccnx.ccn.impl.support.Tuple;
import org.ccnx.ccn.io.content.ContentEncodingException;
import org.ccnx.ccn.profiles.CCNProfile;
import org.ccnx.ccn.profiles.VersionMissingException;
import org.ccnx.ccn.profiles.VersioningProfile;
import org.ccnx.ccn.profiles.namespace.ParameterizedName;
import org.ccnx.ccn.profiles.security.access.AccessControlProfile;
import org.ccnx.ccn.protocol.CCNTime;
import org.ccnx.ccn.protocol.Component;
import org.ccnx.ccn.protocol.ContentName;
/**
* This is a sub-Profile of AccessControlProfile defining naming conventions used in a group-based
* access control scheme (where one can create Groups of users and other groups, and give rights to
* nametrees based on group membership).
* For descriptions of data, and how this access control system functions, see the separate CCNx Access
* Control Specifications Document.
*
* This class specifies how a number of access control elements are named:
* - users, and their keys
* - groups, and their keys
* - access control lists (ACLs)
* - node keys, and their encryption under ACL member keys
* - if used, markers indicating where to find ACLs/node keys
*/
public class GroupAccessControlProfile extends AccessControlProfile implements CCNProfile {
// These may eventually want to move somewhere more general
public static final Component GROUP_PREFIX = new Component("Groups");
public static final Component USER_PREFIX = new Component("Users");
// The labels used to tag group or user storage information in AccessControlPolicyMarkerObjects
public static final String GROUP_LABEL = "Group";
public static final String USER_LABEL = "User";
public static final String GROUP_MEMBERSHIP_LIST_NAME = "MembershipList";
public static final String GROUP_POINTER_TO_PARENT_GROUP_NAME = "PointerToParentGroup";
public static final String ACL_NAME = "ACL";
public static final byte [] ACL_NAME_BYTES = Component.parseNative(ACL_NAME);
public static final String NODE_KEY_NAME = "NK";
public static final byte [] NODE_KEY_NAME_BYTES = Component.parseNative(NODE_KEY_NAME);
// These two must be the same length
public static final byte [] USER_PRINCIPAL_PREFIX = Component.parseNative("p");
public static final byte [] GROUP_PRINCIPAL_PREFIX = Component.parseNative("g");
public static final ContentName ACL_POSTFIX = new ContentName(ACCESS_CONTROL_MARKER_BYTES, ACL_NAME_BYTES);
/**
* This class records information about a CCN principal.
* This information includes:
* - principal type (group, etc),
* - friendly name (the name the principal is known by)
* - version
*
* We define a mapping between name components and principals:
* <TYPE_PREFIX>:<NAMESPACE_HASH>:<FRIENDLY_NAME>:<VERSION>
*
*/
public static class PrincipalInfo {
// Number of parts expected in a PI component representation
// private static final int PI_COMPONENT_COUNT = 4;
// However long our distinguishing hashes should be
public static final int DISTINGUISHING_HASH_LENGTH = 8;
private byte [] _typeMarker;
private byte[] _distinguishingHash;
private String _friendlyName;
private CCNTime _versionTimestamp;
/**
* Parse the principal info for a specified public key name
* @param isGroup whether the principal is a group
* @param publicKeyName the public key name
* @return the corresponding principal info
* @throws VersionMissingException
* @throws ContentEncodingException
*/
public PrincipalInfo(GroupAccessControlManager accessControlManager, ContentName publicKeyName) throws VersionMissingException, ContentEncodingException {
boolean isGroup = accessControlManager.isGroupName(publicKeyName);
_typeMarker = (isGroup ? GROUP_PRINCIPAL_PREFIX : USER_PRINCIPAL_PREFIX);
_versionTimestamp = VersioningProfile.getLastVersionAsTimestamp(publicKeyName);
// Now need to parse the principal name into a distinguishing hash and friendly name
// How to do this depends on which group it is; there might be a postfix for the
// name to deal with
Tuple<ContentName, String> distinguishingPrefixAndFriendlyName = accessControlManager.parsePrefixAndFriendlyNameFromPublicKeyName(publicKeyName);
_distinguishingHash = contentPrefixToDistinguishingHash(distinguishingPrefixAndFriendlyName.first());
_friendlyName = distinguishingPrefixAndFriendlyName.second();
}
public PrincipalInfo(byte [] principalInfoNameComponent) {
if (!PrincipalInfo.isPrincipalNameComponent(principalInfoNameComponent) || (principalInfoNameComponent.length <= USER_PRINCIPAL_PREFIX.length))
throw new IllegalArgumentException("Not a valid principal name component!");
int pos = 0;
try {
// The group and user principal prefixes are of the same length
_typeMarker = new byte[GROUP_PRINCIPAL_PREFIX.length];
System.arraycopy(principalInfoNameComponent, pos, _typeMarker, 0, _typeMarker.length);
pos += _typeMarker.length;
pos += CCNProfile.COMPONENT_SEPARATOR.length;
// The distinguishing hash is of length DISTINGUISHING_HASH_LENGTH
_distinguishingHash = new byte[DISTINGUISHING_HASH_LENGTH];
System.arraycopy(principalInfoNameComponent, pos, _distinguishingHash, 0, _distinguishingHash.length);
pos += _distinguishingHash.length;
pos += CCNProfile.COMPONENT_SEPARATOR.length;
// friendly name until the next COMPONENT_SEPARATOR
// We only check for the first byte of COMPONENT_SEPARATOR
// since that byte is known to not appear in a friendly name
int fnLength = 0;
while (principalInfoNameComponent[pos + fnLength] != CCNProfile.COMPONENT_SEPARATOR[0]) fnLength++;
byte[] friendlyNameBytes = new byte[fnLength];
System.arraycopy(principalInfoNameComponent, pos, friendlyNameBytes, 0, fnLength);
_friendlyName = Component.printNative(friendlyNameBytes);
pos += fnLength;
pos += CCNProfile.COMPONENT_SEPARATOR.length;
// the rest is the timestamp
byte[] timestampBytes = new byte[principalInfoNameComponent.length - pos];
System.arraycopy(principalInfoNameComponent, pos, timestampBytes, 0, timestampBytes.length);
_versionTimestamp = new CCNTime(timestampBytes);
} catch (Exception e) {
// we're having some trouble here...
Log.severe(Log.FAC_ACCESSCONTROL, "PrincipalInfo: error in parsing component {0}",
Component.printURI(principalInfoNameComponent));
Log.severe(Log.FAC_ACCESSCONTROL, "PrincipalInfo: typeMarker {0}, distinguishing hash {1}, friendly name {2}, timestamp {3}",
Component.printURI(_typeMarker), Component.printURI(_distinguishingHash),
_friendlyName, _versionTimestamp);
System.exit(1);
}
}
/**
* Principal names for links to wrapped key blocks take the form:
* {GROUP_PRINCIPAL_PREFIX | PRINCIPAL_PREFIX} COMPONENT_SEPARATOR distinguisingHash COMPONENT_SEPARATOR friendlName COMPONENT_SEPARATOR timestamp as 12-bit binary
* This allows a single enumeration of a wrapped key directory to determine
* not only which principals the keys are wrapped for, but also what versions of their
* private keys the keys are wrapped under (also determinable from the contents of the
* wrapped key blocks, but to do that you have to pull the wrapped key block).
* These serve as the name of a link to the actual wrapped key block.
*/
public byte[] toNameComponent() {
byte [] prefix = (isGroup() ? GROUP_PRINCIPAL_PREFIX : USER_PRINCIPAL_PREFIX);
byte [] bytePrincipal = Component.parseNative(friendlyName());
byte [] byteTime = versionTimestamp().toBinaryTime();
byte [] component = new byte[prefix.length + distinguishingHash().length + bytePrincipal.length +
byteTime.length + 3*COMPONENT_SEPARATOR.length];
// java 1.6 has much better functions for array copying
int offset = 0;
System.arraycopy(prefix, 0, component, offset, prefix.length);
offset += prefix.length;
System.arraycopy(COMPONENT_SEPARATOR, 0, component, offset, COMPONENT_SEPARATOR.length);
offset += COMPONENT_SEPARATOR.length;
System.arraycopy(distinguishingHash(), 0, component, offset, distinguishingHash().length);
offset += distinguishingHash().length;
System.arraycopy(COMPONENT_SEPARATOR, 0, component, offset, COMPONENT_SEPARATOR.length);
offset += COMPONENT_SEPARATOR.length;
System.arraycopy(bytePrincipal, 0, component, offset, bytePrincipal.length);
offset += bytePrincipal.length;
System.arraycopy(COMPONENT_SEPARATOR, 0, component, offset, COMPONENT_SEPARATOR.length);
offset += COMPONENT_SEPARATOR.length;
System.arraycopy(byteTime, 0, component, offset, byteTime.length);
return component;
}
public boolean isGroup() { return Arrays.areEqual(GROUP_PRINCIPAL_PREFIX, _typeMarker); }
public String friendlyName() { return _friendlyName; }
public byte[] distinguishingHash() { return _distinguishingHash; }
public CCNTime versionTimestamp() { return _versionTimestamp; }
/**
* Returns whether a specified name component is the name of a principal
* @param nameComponent the name component
* @return
*/
public static boolean isPrincipalNameComponent(byte [] nameComponent) {
return (DataUtils.isBinaryPrefix(GroupAccessControlProfile.USER_PRINCIPAL_PREFIX, nameComponent) ||
DataUtils.isBinaryPrefix(GroupAccessControlProfile.GROUP_PRINCIPAL_PREFIX, nameComponent));
}
/**
* A first stab
* @throws ContentEncodingException
*/
public static byte [] contentPrefixToDistinguishingHash(ContentName name) {
byte[] fullDigest;
byte[] encoded;
try {
encoded = name.encode();
} catch (ContentEncodingException e) {
// Should never happen
throw new RuntimeException(e);
}
fullDigest = CCNDigestHelper.digest(encoded);
// Ensure that the distinguishing hash is always exactly of length DISTINGUISHING_HASH_LENGTH
// to enable correct parsing of a byte[] representing a PrincipalInfo
if (fullDigest.length > DISTINGUISHING_HASH_LENGTH) {
byte [] returnedDigest = new byte[DISTINGUISHING_HASH_LENGTH];
System.arraycopy(fullDigest, 0, returnedDigest, 0, DISTINGUISHING_HASH_LENGTH);
return returnedDigest;
} else if (fullDigest.length < DISTINGUISHING_HASH_LENGTH) {
byte [] returnedDigest = new byte[DISTINGUISHING_HASH_LENGTH];
System.arraycopy(fullDigest, 0, returnedDigest, 0, fullDigest.length);
return returnedDigest;
}
return fullDigest;
}
@Override
public String toString() {
return String.format("%s : %s", _friendlyName, DataUtils.printHexBytes(_distinguishingHash));
}
}
/**
* Returns whether the specified name is the name of a node key
* @param name the name
* @return
*/
public static boolean isNodeKeyName(ContentName name) {
if (!isAccessName(name) || !VersioningProfile.hasTerminalVersion(name)) {
return false;
}
int versionComponent = VersioningProfile.findLastVersionComponent(name);
if (name.stringComponent(versionComponent - 1).equals(NODE_KEY_NAME)) {
return true;
}
return false;
}
/**
* Get the name of the node key for a given content node, if there is one.
* This is nodeName/<access marker>/NK, with a version then added for a specific node key.
* @param nodeName the name of the content node
* @return the name of the corresponding node key
*/
public static ContentName nodeKeyName(ContentName nodeName) {
return new ContentName(accessRoot(nodeName), ACCESS_CONTROL_MARKER_BYTES, NODE_KEY_NAME_BYTES);
}
/**
* Get the name of the access control list (ACL) for a given content node.
* This is nodeName/<access marker>/ACL.
* @param nodeName the name of the content node
* @return the name of the corresponding ACL
*/
public static ContentName aclName(ContentName nodeName) {
ContentName baseName = accessRoot(nodeName);
return baseName.append(aclPostfix());
}
public static ContentName aclPostfix() {
return ACL_POSTFIX;
}
/**
* Get the name of the user namespace.
* This assumes a top-level namespace, where the group information is stored in
* namespace/Groups and namespace/Users..
* @param namespace the top-level name space
* @return the name of the user namespace
*/
public static ContentName userNamespaceName(ContentName namespace) {
return new ContentName(accessRoot(namespace), USER_PREFIX);
}
/**
* Get the name of the namespace for a specified user.
* @param userNamespace the name of the user namespace
* @param userName the user name
* @return the name of the namespace for the user
*/
public static ContentName userNamespaceName(ContentName userNamespace,
String userName) {
return new ContentName(userNamespace, userName);
}
/**
* Get the name of the group namespace.
* This assumes a top-level namespace, where the group information is stored in
* namespace/Groups and namespace/Users..
* @param namespace the top-level name space
* @return the name of the group namespace
*/
public static ContentName groupNamespaceName(ContentName namespace) {
return new ContentName(accessRoot(namespace), GROUP_PREFIX);
}
/**
* Get the name of the namespace for a specified group.
* @param namespace the top-level namespace
* @param groupFriendlyName the name of the group
* @return the name of the namespace for the group
*/
public static ContentName groupName(ContentName namespace, String groupFriendlyName) {
return new ContentName(groupNamespaceName(namespace), groupFriendlyName);
}
/**
* Get the name of a group public key.
* This is the unversioned root. The actual public key is stored at the latest version of
* this name. The private key and decoding blocks are stored under that version, with
* the segments of the group public key.
* @param groupNamespaceName the namespace of the group
* @param groupFriendlyName the name of the group
* @return the name of the group public key
*/
public static ContentName groupPublicKeyName(ParameterizedName groupStorage, String groupFriendlyName) {
ContentName groupFullName = new ContentName(groupStorage.prefix(), groupFriendlyName);
return groupPublicKeyName(groupStorage, groupFullName);
}
/**
* Get the name of the public key of a group specified by its full name
* @param groupFullName the full name of the group
* @return the name of the group public key
*/
public static ContentName groupPublicKeyName(ParameterizedName groupStorage, ContentName groupFullName) {
if (groupStorage.suffix() != null) {
return new ContentName(groupFullName, groupStorage.suffix(), GROUP_PUBLIC_KEY);
}
return new ContentName(groupFullName, GROUP_PUBLIC_KEY);
}
public static ContentName userPublicKeyName(ParameterizedName userStorage, ContentName userName) {
if (null != userStorage.suffix()) {
return userName.append(userStorage.suffix());
}
return userName;
}
/**
* Get the name of a group membership list for a specified group
* @param groupNamespaceName the namespace of the group
* @param groupFriendlyName the name of the group
* @return the name of the group membership list
*/
public static ContentName groupMembershipListName(ParameterizedName groupNamespaceName, String groupFriendlyName) {
return new ContentName(groupNamespaceName.prefix(), groupFriendlyName, GROUP_MEMBERSHIP_LIST_NAME);
}
/**
* Get the friendly name of a specified group
* @param groupName the full name of the group
* @return the friendly name of the group
*/
public static String groupNameToFriendlyName(ContentName groupName) {
return Component.printNative(groupName.lastComponent());
}
/**
* Get the name of a group private key key directory (containing the encrypted key blocks).
* We hang the wrapped private key directly off the public key version.
* @param groupPublicKeyNameAndVersion the versioned name of the group public key
* @return the versioned name of the group private key
*/
public static ContentName groupPrivateKeyDirectory(ContentName groupPublicKeyNameAndVersion) {
return groupPublicKeyNameAndVersion;
}
/**
* Get the name of the private key block in a group private key directory, without version;
* useful for checking cache status.
* @param groupFullName
* @return
*/
public static ContentName groupPrivateKeyBlockName(ContentName groupPublicKeyNameAndVersion) {
return new ContentName(groupPrivateKeyDirectory(groupPublicKeyNameAndVersion), GROUP_PRIVATE_KEY);
}
public static ContentName groupPointerToParentGroupName(ContentName groupFullName) {
return new ContentName(groupFullName, GROUP_POINTER_TO_PARENT_GROUP_NAME);
}
}