/*
* 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 java.io.IOException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.TreeMap;
import java.util.logging.Level;
import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.ccnx.ccn.CCNHandle;
import org.ccnx.ccn.KeyManager;
import org.ccnx.ccn.config.SystemConfiguration;
import org.ccnx.ccn.impl.CCNFlowControl.SaveType;
import org.ccnx.ccn.impl.support.ByteArrayCompare;
import org.ccnx.ccn.impl.support.DataUtils;
import org.ccnx.ccn.impl.support.Log;
import static org.ccnx.ccn.impl.support.Log.*;
import static org.ccnx.ccn.io.content.KeyDirectory.PREVIOUS_KEY;
import org.ccnx.ccn.impl.support.Tuple;
import org.ccnx.ccn.io.content.ContentDecodingException;
import org.ccnx.ccn.io.content.ContentEncodingException;
import org.ccnx.ccn.io.content.ContentGoneException;
import org.ccnx.ccn.io.content.ContentNotReadyException;
import org.ccnx.ccn.io.content.KeyValueSet;
import org.ccnx.ccn.io.content.Link;
import org.ccnx.ccn.io.content.LinkAuthenticator;
import org.ccnx.ccn.io.content.PublicKeyObject;
import org.ccnx.ccn.io.content.WrappedKey.WrappedKeyObject;
import org.ccnx.ccn.profiles.VersionMissingException;
import org.ccnx.ccn.profiles.VersioningProfile;
import org.ccnx.ccn.profiles.namespace.NamespaceProfile;
import org.ccnx.ccn.profiles.namespace.ParameterizedName;
import org.ccnx.ccn.profiles.search.LatestVersionPathfinder;
import org.ccnx.ccn.profiles.search.Pathfinder;
import org.ccnx.ccn.profiles.search.Pathfinder.SearchResults;
import org.ccnx.ccn.profiles.security.access.AccessControlManager;
import org.ccnx.ccn.profiles.security.access.AccessControlPolicyMarker;
import org.ccnx.ccn.profiles.security.access.AccessControlProfile;
import org.ccnx.ccn.profiles.security.access.AccessDeniedException;
import org.ccnx.ccn.profiles.security.access.AccessControlPolicyMarker.AccessControlPolicyMarkerObject;
import org.ccnx.ccn.profiles.security.access.group.ACL.ACLObject;
import org.ccnx.ccn.profiles.security.access.group.ACL.ACLOperation;
import org.ccnx.ccn.profiles.security.access.group.GroupAccessControlProfile.PrincipalInfo;
import org.ccnx.ccn.protocol.ContentName;
import org.ccnx.ccn.protocol.ContentObject;
import org.ccnx.ccn.protocol.MalformedContentNameStringException;
import org.ccnx.ccn.protocol.PublisherPublicKeyDigest;
import org.ccnx.ccn.protocol.SignedInfo.ContentType;
/**
* This class implements a basic Group-based access control scheme. For full details,
* see the CCNx Access Control Specification. Management of Groups and Group members is
* handled by the GroupManager and Group classes. This class handles management and updating
* of access control (node) keys stored in the name tree, and any operation that requires
* crawling that tree -- looking at more than one node at once. Operations on single nodes
* are handled by individual component classes (e.g. NodeKey).
*
* TODO refactor this class and its use slightly; right now it is capable of handling multpile
* group and user namespaces, but it itself is limited to a single content namespace. This
* means that we have multiple GACMs, one per protected namespace; we should refactor
* so that we have one ACM of each type, and each manages multiple namespaces. This is
* not a large change, and might be more efficient.
*
* This class is used in updating node keys and by #getEffectiveNodeKey(ContentName).
* To achieve this, we walk up the tree for this node. At each point, we check to
* see if a node key exists. If one exists, we decrypt it if we know an appropriate
* key. Otherwise we return null.
*
* We are going for a low-enumeration approach. We could enumerate node keys and
* see if we have rights on the latest version; but then we need to enumerate keys
* and figure out whether we have a copy of a key or one of its previous keys.
* If we don't know our group memberships, even if we enumerate the node key
* access, we don't know what groups we're a member of.
*
* Node keys and ACLs evolve in the following fashion:
* - if ACL adds rights, by adding a group, we merely add encrypted node key blocks for
* the same node key version (ACL version increases, node key version does not)
* - if an ACL removes rights, by removing a group, we version the ACL and the node key
* (both versions increase)
* - if a group adds rights by adding a member, we merely add key blocks to the group key
* (no change to node key or ACL)
* - if a group removes rights by removing a member, we need to evolve all the node keys
* that point to that node key, at the time of next write using that node key (so we don't
* have to enumerate them). (node key version increases, but ACL version does not).
*
* One could have the node key (NK) point to its ACL version, or vice versa, but they really
* do most efficiently evolve in parallel. One could have the ACL point to group versions,
* and update the ACL and NK together in the last case as well.
* In this last case, we want to update the NK only on next write; if we never write again,
* we never need to generate a new NK (unless we can delete). And we want to wait as long
* as possible, to skip NK updates with no corresponding writes.
* But, a writer needs to determine first what the most recent node key is for a given
* node, and then must determine whether or not that node key must be updated -- whether or
* not the most recent versions of groups are what that node key is encrypted under.
* Ideally we don't want to have it update the ACL, as that allows management access separation --
* we can let writers write the node key without allowing them to write the ACL.
*
* So, we can't store the group version information in the ACL. We don't necessarily
* want a writer to have to pull all the node key blocks to see what version of each
* group the node key is encrypted under.
*
* We could name the node key blocks <prefix>/<access marker>/NK/\#version/<group name>:<group key id>,
* if we could match on partial components, but we can't.
*
* We can name the node key blocks <prefix>/<access marker>/NK/\#version/<group key id> with
* a link pointing to that from NK/\#version/<group name>.
*
* For both read and write, we don't actually care what the ACL says. We only care what
* the node key is. Most efficient option, if we have a full key cache, is to list the
* node key blocks by key id used to encrypt them, and then pull one for a key in our cache.
* On the read side, we're looking at a specific version NK, and we might have rights by going
* through its later siblings. On the write side, we're looking at the latest version NK, and
* we should have rights to one of the key blocks, or we don't have rights.
* If we don't have a full key cache, we have to walk the access hierarchy. In that case,
* the most efficient thing to do is to pull the latest version of the ACL for that node
* (if we have a NK, we have an ACL, and vice versa, so we can enumerate NK and then pull
* ACLs). We then walk that ACL. If we know we are in one of the groups in that ACL, walk
* back to find the group key encrypting that node key. If we don't, walk the groups in that
* ACL to find out if we're in any of them. If we are, pull the current group key, see if
* it works, and start working backwards through group keys, populating the cache in the process,
* to find the relevant group key.
*
* Right answer might be intentional containment. Besides the overall group key structures,
* we make a master list that points to the current versions of all the groups. That would
* have to be writable by anyone who is on the manage list for any group. That would let you
* get, easily, a single list indicating what groups are available and what their versions are.
* Unless NE lets you do that in a single pass, which would be better. (Enumerate name/latestversion,
* not just given name, enumerate versions.)
*
*
* Operational Process:
*
* read:
* - look at content, find data key
* - data key refers to specific node key and version used to encrypt it
* - attempt to retrieve that node key from cache, if get it, done
* - go to specific node key key directory, attempt to find a block we can decrypt using keys in cache;
* if so, done
* - (maybe) for groups that node key is encrypted under which we believe we are a member of,
* attempt to retrieve the group key version we need to decrypt the node key
* - if that node key has been superseded, find the latest version of the node key (if we're not
* allowed access to that, we're not allowed access to the data) and walk first the cache,
* then the groups we believe we're a member of, then the groups we don't know about,
* trying to find a key to read it (== retrieve latest version of node key process)
* - if still can't read node key, attempt to find a new ACL interposed between the data node
* and the old node key node, and see if we have access to its latest node key (== retrieve
* latest version of node key process), and then crawl through previous key blocks till we
* get the one we want
*
* write:
* - find closest node key (non-gone)
* - decrypt its latest version, if can't, have no read access, which means have no write access
* - determine whether it's "dirty" -- needs to be superseded. ACL-changes update node key versions,
* what we need to do is determine whether any groups have updated their keys
* - if so, replace it
* - use it to protect data key
// We don't have a key cached. Either we don't have access, we aren't in one of the
// relevant groups, or we are, but we haven't pulled the appropriate version of the group
// key (because it's old, or because we don't know we're in that group).
// We can get this node key because either we're in one of the groups it was made
// available to, or because it's old, and we have access to one of the groups that
// has current access.
// Want to get the latest version of this node key, then do the walk to figure
// out how to read it. Need to split this code up:
// Given specific version (potentially old):
// - enumerate key blocks and group names
// - if we have one cached, use key
// - for groups we believe we're a member of, pull the link and see what key it points to
// - if it's older than the group key we know, walk back from the group key we know, caching
// all the way (this will err on the side of reading; starting from the current group will
// err on the side of making access control coverage look more extensive)
// - if we know nothing else, pull the latest version and walk that if it's newer than this one
// - if that results in a key, chain backwards to this key
// Given latest version:
// - enumerate key blocks, and group names
// - if we have one cached, just use it
// - walk the groups, starting with the groups we believe we're a member of
// - for groups we believe we're in, check if we're still in, then check for a given key
// - walk the groups we don't know if we're in, see if we're in, and can pull the necessary key
// - given that, unwrap the key and return it
// basic flow -- flag that says whether we believe we have the latest or not, if set, walk
// groups we don't know about, if not set, pull latest and if we get something later, make
// recursive call saying we believe it's the latest (2-depth recursion max)
// As we look at more stuff, we cache more keys, and fall more and more into the cache-only
// path.
*
*/
public class GroupAccessControlManager extends AccessControlManager {
/**
* Marker in a Root object that this is the profile we want.
*/
public static final String PROFILE_NAME_STRING = "/ccnx.org/ccn/profiles/security/access/group/GroupAccessControlProfile";
/**
* This algorithm must be capable of key wrap (RSA, ElGamal, etc).
*/
public static final String DEFAULT_GROUP_KEY_ALGORITHM = "RSA";
public static final int DEFAULT_GROUP_KEY_LENGTH = 1024;
public static final String NODE_KEY_LABEL = "Node Key";
private ArrayList<ParameterizedName> _userStorage = new ArrayList<ParameterizedName>();
private TreeMap<byte[], ParameterizedName> _hashToUserStorageMap = new TreeMap<byte[], ParameterizedName>(byteArrayComparator);
private ArrayList<GroupManager> _groupManager = new ArrayList<GroupManager>();
protected static Comparator<byte[]> byteArrayComparator = new ByteArrayCompare();
private TreeMap<byte[], GroupManager> hashToGroupManagerMap = new TreeMap<byte[], GroupManager>(byteArrayComparator);
private HashMap<ContentName, GroupManager> prefixToGroupManagerMap = new HashMap<ContentName, GroupManager>();
private HashSet<ContentName> _myIdentities = new HashSet<ContentName>();
public GroupAccessControlManager() {
// must call initialize
}
public GroupAccessControlManager(ContentName namespace) throws IOException {
this(namespace, null);
}
public GroupAccessControlManager(ContentName namespace, CCNHandle handle) throws IOException {
this(namespace, GroupAccessControlProfile.groupNamespaceName(namespace),
GroupAccessControlProfile.userNamespaceName(namespace), handle);
}
public GroupAccessControlManager(ContentName namespace, ContentName groupStorage, ContentName userStorage) throws IOException {
this(namespace, groupStorage, userStorage, null);
}
public GroupAccessControlManager(ContentName namespace, ContentName groupStorage,
ContentName userStorage, CCNHandle handle)
throws IOException {
this(namespace, new ContentName[]{groupStorage}, new ContentName[]{userStorage}, handle);
}
public GroupAccessControlManager(ContentName namespace, ContentName[] groupStorage,
ContentName[] userStorage, CCNHandle handle)
throws IOException {
initialize(namespace, groupStorage, userStorage, handle);
}
/**
* Type-specific method to initialize group-based access control for a namespace.
* Other subclasses of AccessControlManager, including subtypes of this one, should
* roll their own if necessary.
* This creates the type of access control manager specified in the policy; as long
* as it is a subtype of GroupAccessControlManager it will work fine. The subtype
* can specialize initializeNamespace to do additional setup if necessary.
* @param namespace
* @param groupStorage
* @param userStorage
* @param handle
* @throws IOException
*/
public static AccessControlManager create(ContentName name, ContentName profileName,
ACL acl, ArrayList<ParameterizedName> parameterizedNames,
KeyValueSet parameters, SaveType saveType, CCNHandle handle) throws IOException, InvalidKeyException {
GroupAccessControlManager gacm = (GroupAccessControlManager)AccessControlPolicyMarker.create(
name, profileName, parameterizedNames, parameters, saveType, handle);
// create ACL and NK at the root of the namespace under access control
gacm.initializeNamespace(acl);
return gacm;
}
private void initialize(ContentName namespace, ContentName[] groupStorage,
ContentName[] userStorage, CCNHandle handle)
throws IOException {
ArrayList<ParameterizedName> parameterizedNames = new ArrayList<ParameterizedName>();
for (ContentName uStorage: userStorage) {
if (null == uStorage)
continue;
ParameterizedName pName = new ParameterizedName(GroupAccessControlProfile.USER_LABEL, uStorage, null);
parameterizedNames.add(pName);
}
for (ContentName gStorage: groupStorage) {
if (null == gStorage)
continue;
ParameterizedName pName = new ParameterizedName(GroupAccessControlProfile.GROUP_LABEL, gStorage, null);
parameterizedNames.add(pName);
}
if (null == handle) handle = CCNHandle.getHandle();
AccessControlPolicyMarker r;
try {
r = new AccessControlPolicyMarker(ContentName.fromNative(GroupAccessControlManager.PROFILE_NAME_STRING), parameterizedNames, null);
} catch (MalformedContentNameStringException e) {
throw new IOException("MalformedContentNameStringException parsing built-in profile name: " + GroupAccessControlManager.PROFILE_NAME_STRING + ": " + e);
}
ContentName policyPrefix = NamespaceProfile.policyNamespace(namespace);
ContentName policyMarkerName = AccessControlProfile.getAccessControlPolicyName(policyPrefix);
AccessControlPolicyMarkerObject policyInformation = new AccessControlPolicyMarkerObject(policyMarkerName, r, SaveType.REPOSITORY, handle);
initialize(policyInformation, handle);
}
@Override
public boolean initialize(AccessControlPolicyMarkerObject policyInformation, CCNHandle handle)
throws IOException {
if (null == handle) {
_handle = CCNHandle.getHandle();
} else {
_handle = handle;
}
_policy = policyInformation;
// set up information based on contents of policy
// also need a static method/command line program to create a Root with the right types of information
// for this access control manager type
int componentCount = policyInformation.getBaseName().count();
componentCount -= NamespaceProfile.policyPostfix().count();
componentCount -= 1;
_namespace = policyInformation.getBaseName().cut(componentCount);
ArrayList<ParameterizedName> parameterizedNames = policyInformation.policy().parameterizedNames();
for (ParameterizedName pName: parameterizedNames) {
String label = pName.label();
if (label.equals(GroupAccessControlProfile.GROUP_LABEL)) {
registerGroupStorage(pName);
} else if (label.equals(GroupAccessControlProfile.USER_LABEL)) {
_userStorage.add(pName);
}
}
return true;
}
public GroupManager groupManager() {
if (_groupManager.size() > 1) throw new RuntimeException("A group manager can only be retrieved by name when there are more than one.");
return _groupManager.get(0);
}
public void registerGroupStorage(ContentName groupStorage) throws IOException {
ParameterizedName pName = new ParameterizedName(GroupAccessControlProfile.GROUP_LABEL, groupStorage, null);
registerGroupStorage(pName);
}
public void registerGroupStorage(ParameterizedName pName) {
GroupManager gm = new GroupManager(this, pName, _handle);
_groupManager.add(gm);
byte[] distinguishingHash = GroupAccessControlProfile.PrincipalInfo.contentPrefixToDistinguishingHash(pName.prefix());
hashToGroupManagerMap.put(distinguishingHash, gm);
prefixToGroupManagerMap.put(pName.prefix(), gm);
}
public void registerUserStorage(ContentName userStorage) throws ContentEncodingException {
ParameterizedName pName = new ParameterizedName(GroupAccessControlProfile.USER_LABEL, userStorage, null);
registerUserStorage(pName);
}
public void registerUserStorage(ParameterizedName userStorage) {
_userStorage.add(userStorage);
_hashToUserStorageMap.put(PrincipalInfo.contentPrefixToDistinguishingHash(userStorage.prefix()), userStorage);
}
public ParameterizedName getUserStorage(ContentName userName) {
for (ParameterizedName storage : _userStorage) {
if (storage.prefix().isPrefixOf(userName)) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)){
Log.info(Log.FAC_ACCESSCONTROL, "Found user storage {0} for user name {1}.", storage, userName);
}
return storage;
}
}
return null;
}
public ParameterizedName getUserStorage(byte [] distinguisingHash) {
return _hashToUserStorageMap.get(distinguisingHash);
}
public GroupManager groupManager(byte[] distinguishingHash) {
GroupManager gm = hashToGroupManagerMap.get(distinguishingHash);
if (gm == null) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "Failed to retrieve a group manager with distinguishing hash " + DataUtils.printHexBytes(distinguishingHash));
}
}
return gm;
}
public GroupManager groupManager(ContentName prefixName) {
GroupManager gm = prefixToGroupManagerMap.get(prefixName);
if (gm == null) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "GroupAccessControlManager: failed to retrieve a group manager with prefix " + prefixName);
}
}
return gm;
}
public boolean isGroupName(ContentName principalPublicKeyName) {
for (GroupManager gm: _groupManager) {
if (gm.isGroup(principalPublicKeyName)) return true;
}
return false;
}
public ContentName groupPublicKeyName(ContentName principalName) {
for (GroupManager gm: _groupManager) {
if (gm.isGroup(principalName)) {
return GroupAccessControlProfile.groupPublicKeyName(gm.getGroupStorage(), principalName);
}
}
return null;
}
public ContentName userPublicKeyName(ContentName principalName) {
for (ParameterizedName storage : _userStorage) {
if (storage.prefix().isPrefixOf(principalName)) {
// MLAC should return parameterized user key name.
return GroupAccessControlProfile.userPublicKeyName(storage, principalName);
}
}
return null;
}
public Tuple<ContentName, String> parsePrefixAndFriendlyNameFromPublicKeyName(ContentName principalPublicKeyName) {
// for each group manager/user namespace, there is a prefix, then a "friendly" name component,
// then optionally a postfix that gets tacked on to go from the prefix to the public key
// e.g. Alice's key in the user namespace might be:
// /parc.com/Users/Alice/Keys/EncryptionKey
// where the distinguising prefix would be computed from /parc.com/Users, Alice is the friendly
// name, and the rest is stored in the group manager for /parc.com/Users, generated out of the
// relevant entry in the Roots spec, and just used in a function to go from (distinguishing hash, friendly name)
// to key name
// loop through the group managers, see if the prefix matches, if so, pull that prefix, and take
// the component after that prefix as the friendly name
for (GroupManager gm: _groupManager) {
if (gm.isGroup(principalPublicKeyName)) {
ContentName prefix = gm.getGroupStorage().prefix();
String friendlyName = principalPublicKeyName.postfix(prefix).stringComponent(0);
return new Tuple<ContentName, String>(prefix, friendlyName);
}
}
// NOTE: as there are multiple user prefixes, each with a distinguishing hash and an optional
// suffix, you have to have a list of those and iterate over them as well.
for (ParameterizedName userStorage: _userStorage) {
if (userStorage.prefix().isPrefixOf(principalPublicKeyName)) {
String friendlyName = principalPublicKeyName.postfix(userStorage.prefix()).stringComponent(0);
return new Tuple<ContentName, String>(userStorage.prefix(), friendlyName);
}
}
return null;
}
// TODO
public ContentName getPrincipalPublicKeyName(byte [] distinguishingHash, String friendlyName) {
// look up the distinguishing hash in the user and group maps
// pull the name prefix and postfix from the tables
// make the key name as <prefix>/friendlyName/<postfix>
return null;
}
/**
* Publish my identity (i.e. my public key) under a specified CCN name
* @param identity the name
* @param myPublicKey my public key
* @throws InvalidKeyException
* @throws ContentEncodingException
* @throws IOException
*/
public void publishMyIdentity(ContentName identity, PublicKey myPublicKey)
throws InvalidKeyException, ContentEncodingException, IOException {
KeyManager km = _handle.keyManager();
if (null == myPublicKey) {
myPublicKey = km.getDefaultPublicKey();
}
PublicKeyObject pko = new PublicKeyObject(identity, myPublicKey, SaveType.REPOSITORY, handle());
pko.save();
Log.finer(Log.FAC_ACCESSCONTROL, "Published identity {0}", identity);
_myIdentities.add(identity);
}
/**
* Add an identity to my set. Assume the key is already published.
*/
public void addMyIdentity(ContentName identity) {
Log.finer(Log.FAC_ACCESSCONTROL, "Adding identity {0}", identity);
_myIdentities.add(identity);
}
/**
* Publish the specified identity (i.e. the public key) of a specified user
* @param userName the name of the user
* @param userPublicKey the public key of the user
* @throws IOException
* @throws MalformedContentNameStringException
*/
public void publishUserIdentity(String userName, PublicKey userPublicKey)
throws IOException, MalformedContentNameStringException {
PublicKeyObject pko = new PublicKeyObject(ContentName.fromNative(userName), userPublicKey, SaveType.REPOSITORY, handle());
System.out.println("saving user pubkey to repo:" + userName);
pko.save();
}
public boolean haveIdentity(ContentName userName) {
if (_myIdentities.contains(userName)) {
return true;
}
for (ContentName identity : _myIdentities) {
if (identity.isPrefixOf(userName)) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "haveIdentity: {0} is not exactly one of my identities, but my identity {1} is its prefix. Claiming it as mine. ",
userName, identity);
}
return true;
}
}
return false;
}
public String nodeKeyLabel() {
return NODE_KEY_LABEL;
}
/**
* Get the latest key for a specified principal
* TODO shortcut slightly -- the principal we have cached might not meet the
* constraints of the link.
* TODO principal is a link to a group or user name, need to use prefix/suffix
* to get actual public key
* @param principal the principal
* @return the public key object
* @throws IOException
* @throws ContentDecodingException
*/
public PublicKeyObject getLatestKeyForPrincipal(Link principal) throws ContentDecodingException, IOException {
if (null == principal) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "Cannot retrieve key for empty principal.");
}
return null;
}
PublicKeyObject pko = null;
boolean isGroup = false;
for (GroupManager gm: _groupManager) {
if (gm.isGroup(principal)) {
// MLAC this should load the correct key for the group
pko = gm.getLatestPublicKeyForGroup(principal);
isGroup = true;
break;
}
}
if (!isGroup) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "Retrieving latest key for user: " + principal.targetName());
}
// MLAC this should use the user storage to find the right public key for the user
ContentName publicKeyName = userPublicKeyName(principal.targetName());
LinkAuthenticator targetAuth = principal.targetAuthenticator();
if (null != targetAuth) {
pko = new PublicKeyObject(publicKeyName, targetAuth.publisher(), handle());
}
else pko = new PublicKeyObject(publicKeyName, handle());
}
return pko;
}
/**
* Creates the root ACL for _namespace.
* This initialization must be done before any other ACL or node key can be read or written.
* @param rootACL the root ACL
* @throws IOException
* @throws ContentGoneException
* @throws ContentNotReadyException
* @throws ContentEncodingException
* @throws InvalidKeyException
*/
public ACLObject initializeNamespace(ACL rootACL)
throws InvalidKeyException, ContentEncodingException, ContentNotReadyException,
ContentGoneException, IOException {
// generates the new node key
generateNewNodeKey(_namespace, null, rootACL);
// write the root ACL
ACLObject aclo = new ACLObject(GroupAccessControlProfile.aclName(_namespace), rootACL, handle());
aclo.save();
return aclo;
}
/**
* Retrieves the latest version of an ACL effective at this node, either stored
* here or at one of its ancestors.
* @param nodeName the name of the node
* @return the ACL object
* @throws IOException
* @throws ContentDecodingException
*/
public ACLObject getEffectiveACLObject(ContentName nodeName) throws ContentDecodingException, IOException {
// Find the closest node that has a non-gone ACL
ACLObject aclo = findAncestorWithACL(nodeName, null);
if (null != aclo) {
// parallel find doesn't get us the latest version. Serial does,
// but it's kind of an artifact.
aclo.update();
} else {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "No ACL found between node {0} and namespace root {1}. Returning root ACL.",
nodeName, getNamespaceRoot());
}
return getACLObjectForNode(getNamespaceRoot());
}
return aclo;
}
private ACLObject findAncestorWithACL(ContentName dataNodeName, ContentName stopPoint) throws ContentDecodingException, IOException {
// selector method, remove when pick faster one.
return findAncestorWithACLInParallel(dataNodeName, stopPoint);
}
/**
* Look for an ACL that is on dataNodeName or above, but below stopPoint. If stopPoint is
* null, then take it to be the root for this AccessControlManager (which we assume to have
* an ACL). This is the old serial search, which has been replaced tyb the parallel search
* below. Keep it here temporarily.
* @param dataNodeName
* @param stopPoint
* @return
* @throws ContentDecodingException
* @throws IOException
*/
@SuppressWarnings("unused")
private ACLObject findAncestorWithACLSerial(ContentName dataNodeName, ContentName stopPoint) throws ContentDecodingException, IOException {
// If dataNodeName is the root of this AccessControlManager, there can be no ACL between
// dataNodeName (inclusive) and the root (exclusive), so return null.
if (getNamespaceRoot().equals(dataNodeName)) return null;
if (null == stopPoint) {
stopPoint = getNamespaceRoot();
} else if (!getNamespaceRoot().isPrefixOf(stopPoint)) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.WARNING)) {
Log.warning(Log.FAC_ACCESSCONTROL, "findAncestorWithACL: Stopping point {0} must be an ancestor of the starting point {1}!",
stopPoint, dataNodeName);
}
throw new IOException("findAncestorWithACL: invalid search space: stopping point " + stopPoint + " must be an ancestor of the starting point " +
dataNodeName + "!");
}
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "findAncestorWithACL: start point {0}, stop before {1}", dataNodeName, stopPoint);
}
int stopCount = stopPoint.count();
ACLObject ancestorACLObject = null;
ContentName parentName = dataNodeName;
ContentName nextParentName = null;
while (null == ancestorACLObject) {
ancestorACLObject = getACLObjectForNodeIfExists(parentName);
if (null != ancestorACLObject) {
if (ancestorACLObject.isGone()) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "Found an ACL object at {0} but its GONE.", ancestorACLObject.getVersionedName());
}
ancestorACLObject = null;
} else {
// got one
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "Found an ACL object at {0}", ancestorACLObject.getVersionedName());
}
break;
}
}
nextParentName = parentName.parent();
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "findAncestorWithACL: no ACL object at node {0}, looking next at {1}", parentName, nextParentName);
}
// stop looking once we're above our namespace, or if we've already checked the top level
if (parentName.count() == stopCount) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "findAncestorWithACL: giving up, next search point would be {0}, stop point is {1}, no ACL found",
parentName, stopPoint);
}
break;
}
parentName = nextParentName;
}
if (null == ancestorACLObject) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL,
"No ACL available in ancestor tree between {0} and {1} (not-inclusive) out of namespace rooted at {2}.",
dataNodeName, stopPoint, getNamespaceRoot());
}
return null;
}
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "Found ACL for {0} at ancestor {1}: ", dataNodeName, ancestorACLObject.getVersionedName());
}
return ancestorACLObject;
}
/**
* Implement a parallel findAncestorWithACL; drop it in separately so we can
* switch back and forth in testing.
*/
private ACLObject findAncestorWithACLInParallel(ContentName dataNodeName, ContentName stopPoint) throws ContentDecodingException, IOException {
// If dataNodeName is the root of this AccessControlManager, there can be no ACL between
// dataNodeName (inclusive) and the root (exclusive), so return null.
if (getNamespaceRoot().equals(dataNodeName)) return null;
if (null == stopPoint) {
stopPoint = getNamespaceRoot();
} else if (!getNamespaceRoot().isPrefixOf(stopPoint)) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.WARNING)) {
Log.warning(Log.FAC_ACCESSCONTROL, "findAncestorWithACLInParallel: Stopping point {0} must be an ancestor of the starting point {1}!",
stopPoint, dataNodeName);
}
throw new IOException("findAncestorWithACLInParallel: invalid search space: stopping point " + stopPoint + " must be an ancestor of the starting point " +
dataNodeName + "!");
}
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "findAncestorWithACLInParallel: start point {0}, stop before {1}", dataNodeName, stopPoint);
}
int stopCount = stopPoint.count();
// Pathfinder searches from start point to stop point inclusive, want exclusive, so hand
// it one level down from stop point.
Pathfinder pathfinder = new LatestVersionPathfinder(dataNodeName, dataNodeName.cut(stopCount+1),
GroupAccessControlProfile.aclPostfix(), true, false, SystemConfiguration.EXTRA_LONG_TIMEOUT,
null, handle());
SearchResults searchResults = pathfinder.waitForResults();
if (null != searchResults.getResult()) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "findAncestorWithACLInParallel: found {0}", searchResults.getResult().name());
}
ACLObject aclo = new ACLObject(searchResults.getResult(), handle());
return aclo;
}
return null;
}
/**
* Try to pull an ACL for a particular node. If it doesn't exist, will time
* out. Use enumeration to decide whether to call this to avoid the timeout.
* @param aclNodeName the node name
* @return the ACL object
* @throws ContentDecodingException
* @throws IOException
*/
public ACLObject getACLObjectForNode(ContentName aclNodeName)
throws ContentDecodingException, IOException {
// Get the latest version of the acl. We don't care so much about knowing what version it was.
ACLObject aclo = new ACLObject(GroupAccessControlProfile.aclName(aclNodeName), handle());
aclo.update();
// if there is no update, this will probably throw an exception -- IO or XMLStream
if (aclo.isGone()) {
// treat as if no acl on node
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "getACLObjectForNode: Asked to get an ACL object for specific node {0}, but ACL there is GONE! Returning null.",
aclNodeName);
}
return null;
}
return aclo;
}
/**
* Try to pull an ACL for a specified node if it exists.
* @param aclNodeName the name of the node
* @return the ACL object
* @throws IOException
* @throws ContentDecodingException
*/
public ACLObject getACLObjectForNodeIfExists(ContentName aclNodeName) throws ContentDecodingException, IOException {
// TODO really want to check simple existence here, but need to integrate with verifier
// use. GLV too slow for negative results. Given that we need latest version, at least
// use the segment we get, and don't pull it twice.
ContentName aclName = GroupAccessControlProfile.aclName(aclNodeName);
ContentObject aclNameList = VersioningProfile.getLatestVersion(aclName,
null, SystemConfiguration.MAX_TIMEOUT, handle().defaultVerifier(), handle());
if (null != aclNameList) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "Found latest version of acl for {0} at {1} type: {2}",
aclNodeName, aclName, aclNameList.signedInfo().getTypeName());
}
ACLObject aclo = new ACLObject(aclNameList, handle());
if (aclo.isGone()) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "ACL object is GONE, returning anyway {0}", aclo.getVersionedName());
}
}
return aclo;
}
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "No ACL found on node: " + aclNodeName);
}
return null;
}
/**
* Get the effective ACL for a node specified by its name
* @param nodeName the name of the node
* @return the effective ACL
* @throws ContentGoneException
* @throws ContentNotReadyException
* @throws ContentDecodingException
* @throws IOException
*/
public ACL getEffectiveACL(ContentName nodeName)
throws ContentNotReadyException, ContentGoneException, ContentDecodingException, IOException {
ACLObject aclo = getEffectiveACLObject(nodeName);
if (null != aclo) {
return aclo.acl();
}
return null;
}
/**
* Adds an ACL to a node that doesn't have one, or replaces one that exists.
* Just writes, doesn't bother to look at any current ACL. Does need to pull
* the effective node key at this node, though, to wrap the old ENK in a new
* node key.
*
* @param nodeName the name of the node
* @param newACL the new ACL
* @return
* @throws InvalidKeyException
* @throws IOException
* @throws ContentGoneException
* @throws ContentNotReadyException
* @throws NoSuchAlgorithmException
*/
public ACL setACL(ContentName nodeName, ACL newACL)
throws AccessDeniedException, InvalidKeyException, ContentNotReadyException, ContentGoneException, IOException, NoSuchAlgorithmException {
// Throws access denied exception if we can't read the old node key.
NodeKey effectiveNodeKey = getEffectiveNodeKey(nodeName);
// generates the new node key, wraps it under the new acl, and wraps the old node key
generateNewNodeKey(nodeName, effectiveNodeKey, newACL);
// write the acl
ACLObject aclo = new ACLObject(GroupAccessControlProfile.aclName(nodeName), newACL, handle());
aclo.save();
return aclo.acl();
}
/**
* Adds an ACL to a node that doesn't have one, or replaces one that exists.
* Just writes, doesn't bother to look at any current ACL. Does need to pull
* the effective node key at this node, though, to wrap the old ENK in a new
* node key. Gets handed in effective ACL, to avoid having to search.
*
* @param nodeName the name of the node
* @param newACL the new ACL
* @return
* @throws InvalidKeyException
* @throws IOException
* @throws ContentGoneException
* @throws ContentNotReadyException
* @throws NoSuchAlgorithmException
*/
public ACL setACL(ContentName nodeName, ACL newACL, ACLObject effectiveACLObject)
throws AccessDeniedException, InvalidKeyException, ContentNotReadyException, ContentGoneException, IOException, NoSuchAlgorithmException {
// Throws access denied exception if we can't read the old node key.
NodeKey effectiveNodeKey = getEffectiveNodeKey(nodeName, effectiveACLObject);
// generates the new node key, wraps it under the new acl, and wraps the old node key
generateNewNodeKey(nodeName, effectiveNodeKey, newACL);
// write the acl
ACLObject aclo = new ACLObject(GroupAccessControlProfile.aclName(nodeName), newACL, handle());
aclo.save();
return aclo.acl();
}
/**
* Delete the ACL at this node if one exists, returning control to the
* next ACL upstream.
* We simply add a supserseded by block at this node, wrapping this key in the key of the upstream
* node. If we don't have read access at that node, throw AccessDeniedException.
* Then we write a GONE block here for the ACL, and a new node key version with a superseded by block.
* The superseded by block should probably be encrypted not with the ACL in force, but with the effective
* node key of the parent -- that will be derivable from the appropriate ACL, and will have the right semantics
* if a new ACL is interposed later. In the meantime, all the people with the newly in-force ancestor
* ACL should be able to read this content.
* @param nodeName
* @throws IOException
* @throws ContentDecodingException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
*/
public void deleteACL(ContentName nodeName)
throws ContentDecodingException, IOException, InvalidKeyException, NoSuchAlgorithmException {
// First, find ACL at this node if one exists.
ACLObject thisNodeACL = getACLObjectForNodeIfExists(nodeName);
if (null == thisNodeACL) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "Asked to delete ACL for node {0} that doesn't have one. Doing nothing.", nodeName);
}
return;
} else if (thisNodeACL.isGone()) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "Asked to delete ACL for node {0} that has already been deleted. Doing nothing.", nodeName);
}
return;
}
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "Deleting ACL for node {0} latest version: {1}", nodeName, thisNodeACL.getVersionedName());
}
// We know we have an ACL at this node. So we know we have a node key at this
// node. Get the latest version of this node key.
NodeKey nk = getLatestNodeKeyForNode(nodeName);
// Next, find the node key that would be in force here after this deletion.
// Do that by getting the effective node key at the parent
ContentName parentName = nodeName.parent();
NodeKey effectiveParentNodeKey = getEffectiveNodeKey(parentName);
// And then deriving what the effective node key would be here, if
// we inherited from the parent
NodeKey ourEffectiveNodeKeyFromParent =
effectiveParentNodeKey.computeDescendantNodeKey(nodeName, nodeKeyLabel());
// Generate a superseded block for this node, wrapping its key in the parent.
// TODO want to wrap key in parent's effective key, but can't point to that -- no way to name an
// effective node key... need one.
// need to mangl stored key name into superseded block name, need to wrap
// in ourEffNodeKeyFromParent, make sure stored key id points up to our ENKFP.storedKeyName()
PrincipalKeyDirectory.addSupersededByBlock(nk.storedNodeKeyName(), nk.nodeKey(),
ourEffectiveNodeKeyFromParent.storedNodeKeyName(),
ourEffectiveNodeKeyFromParent.storedNodeKeyID(),
effectiveParentNodeKey.nodeKey(), handle());
// Then mark the ACL as gone.
thisNodeACL.saveAsGone();
}
/**
* Pulls the ACL for this node, if one exists, and modifies it to include
* the following changes, then stores the result using setACL, updating
* the node key if necessary in the process.
*
* @param nodeName the name of the node
* @param ACLUpdates the updates to the ACL
* @return the updated ACL
* @throws IOException
* @throws ContentDecodingException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
*/
public ACL updateACL(ContentName nodeName, ArrayList<ACL.ACLOperation> ACLUpdates)
throws ContentDecodingException, IOException, InvalidKeyException, NoSuchAlgorithmException {
ACLObject currentACL = getACLObjectForNodeIfExists(nodeName);
ACL newACL = null;
if (null != currentACL) {
newACL = currentACL.acl();
} else {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "Adding brand new ACL to node {0}", nodeName);
}
//TODO: if no operations is specified, then a new empty ACL is created...
newACL = new ACL();
}
LinkedList<Link> newReaders = newACL.update(ACLUpdates);
if ((null == newReaders) || (null == currentACL)) {
// null newReaders means we revoked someone.
// null currentACL means we're starting from scratch
// Set the ACL and update the node key.
return setACL(nodeName, newACL);
}
// If we get back a list of new readers, it means all we have to do
// is add key blocks for them, not update the node key. (And it means
// we have a node key for this node.)
// Wait to save the new ACL till we are sure we're allowed to do this.
PrincipalKeyDirectory keyDirectory = null;
try {
// If we can't read the node key, we can't update. Get the effective node key.
// Better be a node key here... and we'd better be allowed to read it.
NodeKey latestNodeKey = getLatestNodeKeyForNode(nodeName);
if (null == latestNodeKey) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "Cannot read the latest node key for {0}", nodeName);
}
throw new AccessDeniedException("Cannot read the latest node key for " + nodeName);
}
keyDirectory = new PrincipalKeyDirectory(this, latestNodeKey.storedNodeKeyName(), handle());
for (Link principal : newReaders) {
PublicKeyObject latestKey = getLatestKeyForPrincipal(principal);
if (!latestKey.available()) {
latestKey.waitForData(SystemConfiguration.getDefaultTimeout());
}
if (latestKey.available()) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "updateACL: Adding wrapped key block for reader: " + latestKey.getVersionedName());
}
try {
keyDirectory.addWrappedKeyBlock(latestNodeKey.nodeKey(), latestKey.getVersionedName(), latestKey.publicKey());
} catch (VersionMissingException e) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.WARNING)) {
Log.warning(Log.FAC_ACCESSCONTROL, "UNEXPECTED: latest key for principal: " + latestKey.getVersionedName() + " has no version? Skipping.");
}
}
} else {
// Do we use an old key or give up?
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "updateACL: No key for {0} found. Skipping.", principal);
}
}
}
} finally {
if (null != keyDirectory) {
keyDirectory.stopEnumerating();
}
}
// If we got here, we got the node key we were updating, so we are allowed
// to at least read this stuff (though maybe not write it). Save the acl.
currentACL.save(newACL);
return newACL;
}
/**
* Add readers to a specified node
* @param nodeName the name of the node
* @param newReaders the list of new readers
* @return the updated ACL
* @throws IOException
* @throws ContentDecodingException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
*/
public ACL addReaders(ContentName nodeName, ArrayList<Link> newReaders)
throws InvalidKeyException, ContentDecodingException, IOException, NoSuchAlgorithmException {
ArrayList<ACLOperation> ops = new ArrayList<ACLOperation>();
for(Link reader : newReaders){
ops.add(ACLOperation.addReaderOperation(reader));
}
return updateACL(nodeName, ops);
}
/**
* Remove readers from a specified node
* @param nodeName the name of the node
* @param removedReaders the list of removed readers
* @return the updated ACL
* @throws IOException
* @throws ContentDecodingException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
*/
public ACL removeReaders(ContentName nodeName, ArrayList<Link> removedReaders)
throws InvalidKeyException, ContentDecodingException, IOException, NoSuchAlgorithmException {
ArrayList<ACLOperation> ops = new ArrayList<ACLOperation>();
for(Link reader : removedReaders){
ops.add(ACLOperation.removeReaderOperation(reader));
}
return updateACL(nodeName, ops);
}
/**
* Add writers to a specified node.
* @param nodeName the name of the node
* @param newWriters the list of new writers
* @return the updated ACL
* @throws IOException
* @throws ContentDecodingException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
*/
public ACL addWriters(ContentName nodeName, ArrayList<Link> newWriters)
throws InvalidKeyException, ContentDecodingException, IOException, NoSuchAlgorithmException {
ArrayList<ACLOperation> ops = new ArrayList<ACLOperation>();
for(Link writer : newWriters){
ops.add(ACLOperation.addWriterOperation(writer));
}
return updateACL(nodeName, ops);
}
/**
* Remove writers from a specified node.
* @param nodeName the name of the node
* @param removedWriters the list of removed writers
* @return the updated ACL
* @throws IOException
* @throws ContentDecodingException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
*/
public ACL removeWriters(ContentName nodeName, ArrayList<Link> removedWriters)
throws InvalidKeyException, ContentDecodingException, IOException, NoSuchAlgorithmException {
ArrayList<ACLOperation> ops = new ArrayList<ACLOperation>();
for(Link writer : removedWriters){
ops.add(ACLOperation.removeWriterOperation(writer));
}
return updateACL(nodeName, ops);
}
/**
* Add managers to a specified node
* @param nodeName the name of the node
* @param newManagers the list of new managers
* @return the updated ACL
* @throws IOException
* @throws ContentDecodingException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
*/
public ACL addManagers(ContentName nodeName, ArrayList<Link> newManagers)
throws InvalidKeyException, ContentDecodingException, IOException, NoSuchAlgorithmException {
ArrayList<ACLOperation> ops = new ArrayList<ACLOperation>();
for(Link manager: newManagers){
ops.add(ACLOperation.addManagerOperation(manager));
}
return updateACL(nodeName, ops);
}
/**
* Remove managers from a specified node
* @param nodeName the name of the node
* @param removedManagers the list of removed managers
* @return the updated ACL
* @throws IOException
* @throws ContentDecodingException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
*/
public ACL removeManagers(ContentName nodeName, ArrayList<Link> removedManagers)
throws InvalidKeyException, ContentDecodingException, IOException, NoSuchAlgorithmException {
ArrayList<ACLOperation> ops = new ArrayList<ACLOperation>();
for(Link manager: removedManagers){
ops.add(ACLOperation.removeManagerOperation(manager));
}
return updateACL(nodeName, ops);
}
/**
* Get the ancestor node key in force at this node (if we can decrypt it),
* including a key at this node itself. We use the fact that ACLs and
* node keys are co-located; if you have one, you have the other.
* @param nodeName the name of the node
* @return null means while node keys exist, we can't decrypt any of them --
* we have no read access to this node (which implies no write access)
* @throws IOException
* @throws ContentDecodingException
* @throws InvalidCipherTextException
* @throws AccessDeniedException
* @throws InvalidKeyException
* @throws IOException if something is wrong (e.g. no node keys at all)
* @throws NoSuchAlgorithmException
*/
protected NodeKey findAncestorWithNodeKey(ContentName nodeName)
throws InvalidKeyException, AccessDeniedException,
ContentDecodingException, IOException, NoSuchAlgorithmException {
// climb up looking for node keys, then make sure that one isn't GONE
// if it isn't, call read-side routine to figure out how to decrypt it
ACLObject effectiveACL = findAncestorWithACL(nodeName, null);
if (null != effectiveACL) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "Got ACL named: {0} attempting to retrieve node key from {1}",
effectiveACL.getVersionedName(), AccessControlProfile.accessRoot(effectiveACL.getVersionedName()));
}
} else {
// We're not searching at the namespace root; because we assume we have
// already made sure we have an ACL there. So if we get back NULL, we
// go stratight to our namespace root.
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "No ACL found between node {0} and namespace root {1}, assume ACL is at namespace root.",
nodeName, getNamespaceRoot());
}
}
return getLatestNodeKeyForNode(
(null != effectiveACL) ? AccessControlProfile.accessRoot(effectiveACL.getBaseName()) : getNamespaceRoot());
}
/**
* Write path: get the latest node key for a node.
* @param nodeName the name of the node
* @return the corresponding node key
* @throws IOException
* @throws ContentDecodingException
* @throws AccessDeniedException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
*/
public NodeKey getLatestNodeKeyForNode(ContentName nodeName)
throws InvalidKeyException, AccessDeniedException,
ContentDecodingException, IOException, NoSuchAlgorithmException {
ContentName nodeKeyPrefix = GroupAccessControlProfile.nodeKeyName(nodeName);
ContentObject co = VersioningProfile.getLatestVersion(nodeKeyPrefix,
null, SystemConfiguration.MAX_TIMEOUT, handle().defaultVerifier(), handle());
ContentName nodeKeyVersionedName = null;
if (co != null) {
nodeKeyVersionedName = co.name().subname(0, nodeKeyPrefix.count() + 1);
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.FINE)) {
Log.fine(Log.FAC_ACCESSCONTROL, "getLatestNodeKeyForNode: {0} is the latest version found for {1}.", nodeKeyVersionedName, nodeName);
}
} else {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.FINE)) {
Log.fine(Log.FAC_ACCESSCONTROL, "getLatestNodeKeyForNode: no latest version found for {0}.", nodeName);
}
return null;
}
// DKS TODO this may not handle ACL deletion correctly -- we need to make sure that this
// key wasn't superseded by something that isn't a later version of itself.
// then, pull the node key we can decrypt
return getNodeKeyByVersionedName(nodeKeyVersionedName, null);
}
/**
* Read path:
* Retrieve a specific node key from a given location, as specified by a
* key it was used to wrap, and, if possible, find a key we can use to
* unwrap the node key.
*
* Throw an exception if there is no node key block at the appropriate name.
* @param nodeKeyName
* @param nodeKeyIdentifier
* @return the node key
* @throws IOException
* @throws ContentDecodingException
* @throws AccessDeniedException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
*/
public NodeKey getSpecificNodeKey(ContentName nodeKeyName, byte [] nodeKeyIdentifier)
throws InvalidKeyException, AccessDeniedException,
ContentDecodingException, IOException, NoSuchAlgorithmException {
if ((null == nodeKeyName) && (null == nodeKeyIdentifier)) {
throw new IllegalArgumentException("Node key name and identifier cannot both be null!");
}
// We should know what node key to use (down to the version), but we have to find the specific
// wrapped key copy we can decrypt.
NodeKey nk = getNodeKeyByVersionedName(nodeKeyName, nodeKeyIdentifier);
if (null == nk) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.WARNING)) {
Log.warning(Log.FAC_ACCESSCONTROL, "No decryptable node key available at {0}, access denied.", nodeKeyName);
}
return null;
}
return nk;
}
/**
* We have the name of a specific version of a node key. Now we just need to figure
* out which of our keys can be used to decrypt it.
* @param nodeKeyName
* @param nodeKeyIdentifier
* @return
* @throws IOException
* @throws AccessDeniedException
* @throws ContentDecodingException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
*/
NodeKey getNodeKeyByVersionedName(ContentName nodeKeyName, byte [] nodeKeyIdentifier)
throws AccessDeniedException, InvalidKeyException,
ContentDecodingException, IOException, NoSuchAlgorithmException {
NodeKey nk = null;
PrincipalKeyDirectory keyDirectory = null;
try {
// First thing to do -- try cache directly before we even consider enumerating.
Key targetKey = handle().keyManager().getSecureKeyCache().getKey(nodeKeyName, nodeKeyIdentifier);
if (null == targetKey) {
// start enumerating; if we get a cache hit don't need it, but just in case.
keyDirectory = new PrincipalKeyDirectory(this, nodeKeyName, handle());
// this will handle the caching.
targetKey = keyDirectory.getUnwrappedKey(nodeKeyIdentifier);
}
if (null != targetKey) {
nk = new NodeKey(nodeKeyName, targetKey);
} else {
throw new AccessDeniedException("Access denied: cannot retrieve key " + DataUtils.printBytes(nodeKeyIdentifier) + " at name " + nodeKeyName);
}
} finally {
if (null != keyDirectory) {
keyDirectory.stopEnumerating();
}
}
return nk;
}
/**
* Write path:
* Get the effective node key in force at this node, used to derive keys to
* encrypt content. Vertical chaining. Works if you ask for node which has
* a node key.
* @param nodeName
* @return
* @throws AccessDeniedException
* @throws ContentEncodingException
* @throws ContentDecodingException
* @throws IOException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
*/
public NodeKey getEffectiveNodeKey(ContentName nodeName)
throws AccessDeniedException, InvalidKeyException, ContentEncodingException,
ContentDecodingException, IOException, NoSuchAlgorithmException {
// Get the ancestor node key in force at this node.
NodeKey nodeKey = findAncestorWithNodeKey(nodeName);
if (null == nodeKey) {
throw new AccessDeniedException("Cannot retrieve node key for node: " + nodeName + ".");
}
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "Found node key at {0}", nodeKey.storedNodeKeyName());
}
NodeKey effectiveNodeKey = nodeKey.computeDescendantNodeKey(nodeName, nodeKeyLabel());
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "Computing effective node key for {0} using stored node key {1}", nodeName, effectiveNodeKey.storedNodeKeyName());
}
return effectiveNodeKey;
}
/**
* Write path:
* Get the effective node key in force at this node, used to derive keys to
* encrypt content. Vertical chaining. Works if you ask for node which has
* a node key. Hand in node with existing node key to avoid search.
* @param nodeName
* @return
* @throws AccessDeniedException
* @throws ContentEncodingException
* @throws ContentDecodingException
* @throws IOException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
*/
public NodeKey getEffectiveNodeKey(ContentName nodeName, ACLObject effectiveACL)
throws AccessDeniedException, InvalidKeyException, ContentEncodingException,
ContentDecodingException, IOException, NoSuchAlgorithmException {
// Get the ancestor node key in force at this node.
NodeKey nodeKey = getLatestNodeKeyForNode(
AccessControlProfile.accessRoot(effectiveACL.getBaseName()));
if (null == nodeKey) {
throw new AccessDeniedException("Cannot retrieve node key for node: " + nodeName + ".");
}
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "Found node key at {0}", nodeKey.storedNodeKeyName());
}
NodeKey effectiveNodeKey = nodeKey.computeDescendantNodeKey(nodeName, nodeKeyLabel());
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "Computing effective node key for {0} using stored node key {1}", nodeName, effectiveNodeKey.storedNodeKeyName());
}
return effectiveNodeKey;
}
/**
* Like #getEffectiveNodeKey(ContentName), except checks to see if node
* key is dirty and updates it if necessary.
* @param nodeName
* @return
* @throws AccessDeniedException
* @throws IOException
* @throws ContentGoneException
* @throws ContentNotReadyException
* @throws ContentEncodingException
* @throws ContentDecodingException
* @throws AccessDeniedException, InvalidKeyException
* @throws NoSuchAlgorithmException
*/
public NodeKey getFreshEffectiveNodeKey(ContentName nodeName)
throws AccessDeniedException, InvalidKeyException, ContentDecodingException,
ContentEncodingException, ContentNotReadyException, ContentGoneException, IOException, NoSuchAlgorithmException {
Log.finest(FAC_ACCESSCONTROL, "GACM.getFreshEffectiveNodeKey");
NodeKey nodeKey = findAncestorWithNodeKey(nodeName);
if (null == nodeKey) {
throw new AccessDeniedException("Cannot retrieve node key for node: " + nodeName + ".");
}
// This should be the latest node key; i.e. not superseded.
if (nodeKeyIsDirty(nodeKey.storedNodeKeyName())) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "getFreshEffectiveNodeKey: Found node key at {0}, updating.", nodeKey.storedNodeKeyName());
}
ContentName nodeKeyNodeName = GroupAccessControlProfile.accessRoot(nodeKey.storedNodeKeyName());
ACLObject acl = getACLObjectForNode(nodeKeyNodeName);
nodeKey = generateNewNodeKey(nodeKeyNodeName, nodeKey, acl.acl());
} else {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "getFreshEffectiveNodeKey: Found node key at {0}", nodeKey.storedNodeKeyName());
}
}
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "getFreshEffectiveNodeKey: retrieved node key for node {0} label {1}: {2}", nodeName, nodeKeyLabel(), nodeKey);
}
NodeKey effectiveNodeKey = nodeKey.computeDescendantNodeKey(nodeName, nodeKeyLabel());
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "getFreshEffectiveNodeKey: computed effective node key for node {0} label {1}: {2} using stored node key {3}"
, nodeName, nodeKeyLabel(), effectiveNodeKey, effectiveNodeKey.storedNodeKeyName());
}
return effectiveNodeKey;
}
/**
* Do we need to update this node key?
* First, we look to see whether or not we know the key is dirty -- i.e.
* does it have a superseded block (if it's gone, it will also have a
* superseded block). If not, we have to really check...
* Basically, we look at all the entities this node key is encrypted for,
* and determine whether any of them have a new version of their public
* key. If so, the node key is dirty.
*
* The initial implementation of this will be simple and slow -- iterating through
* groups and assuming the active object system will keep updating itself whenever
* a new key appears. Eventually, we might want an index directory of all the
* versions of keys, so that one name enumeration request might give us information
* about whether keys have been updated. (Or some kind of aggregate versioning,
* that tell us a) whether any groups have changed their versions, or b) just the
* ones we care about have.)
*
* This can be called by anyone -- the data about whether a node key is dirty
* is visible to anyone. Fixing a dirty node key requires access, though.
* @param theNodeKeyName this might be the name of the node where the NK is stored,
* or the NK name itself.
* We assume this exists -- that there at some point has been a node key here.
* TODO ephemeral node key naming
* @return
* @throws IOException
* @throws ContentDecodingException
*/
public boolean nodeKeyIsDirty(ContentName theNodeKeyName) throws ContentDecodingException, IOException {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.FINE)) {
Log.fine(Log.FAC_ACCESSCONTROL, "NodeKeyIsDirty({0}) called.", theNodeKeyName);
}
// first, is this a node key name?
if (!GroupAccessControlProfile.isNodeKeyName(theNodeKeyName)) {
// assume it's a data node name.
theNodeKeyName = GroupAccessControlProfile.nodeKeyName(theNodeKeyName);
}
// get the requested version of this node key; or if unversioned, get the latest.
PrincipalKeyDirectory nodeKeyDirectory = null;
try {
nodeKeyDirectory = new PrincipalKeyDirectory(this, theNodeKeyName, handle());
nodeKeyDirectory.waitForChildren();
if (nodeKeyDirectory.hasSupersededBlock()) {
return true;
}
for (PrincipalInfo principal : nodeKeyDirectory.getCopyOfPrincipals().values()) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.FINE)) {
Log.fine(Log.FAC_ACCESSCONTROL, "NodeKeyIsDirty: found principal called {0}", principal.friendlyName());
}
if (principal.isGroup()) {
Group theGroup = groupManager(principal.distinguishingHash()).getGroup(principal.friendlyName(), SystemConfiguration.EXTRA_LONG_TIMEOUT);
if (theGroup.publicKeyVersion().after(principal.versionTimestamp())) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.FINE)) {
Log.fine(Log.FAC_ACCESSCONTROL, "NodeKeyIsDirty: the key of principal {0} is out of date", principal.friendlyName());
}
return true;
} else {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.FINE)) {
Log.fine(Log.FAC_ACCESSCONTROL, "NodeKeyIsDirty: the key of principal {0} is up to date", principal.friendlyName());
}
}
} else {
// DKS TODO -- for now, don't handle versioning of non-group keys
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "User key for {0}, not checking version.", principal.friendlyName());
}
// Technically, we're not handling versioning for user keys, but be nice. Start
// by seeing if we have a link to the key in our user space.
// If the principal isn't available in our enumerated list, have to go get its key
// from the wrapped key object.
}
}
return false;
} finally {
if (null != nodeKeyDirectory)
nodeKeyDirectory.stopEnumerating();
}
}
/**
* Would we update this data key if we were doing reencryption?
* This one is simpler -- what node key is the data key encrypted under, and is
* that node key dirty?
*
* This can be called by anyone -- the data about whether a data key is dirty
* is visible to anyone. Fixing a dirty key requires access, though.
*
* @param dataName
* @return
* @throws IOException
* @throws ContentNotReadyException
* @throws ContentDecodingException
*/
public boolean dataKeyIsDirty(ContentName dataName) throws ContentNotReadyException, IOException {
// TODO -- do we need to check whether there *is* a key?
// The trick: we need the header information in the wrapped key; we don't need to unwrap it.
// ephemeral key naming
WrappedKeyObject wrappedDataKey = new WrappedKeyObject(GroupAccessControlProfile.dataKeyName(dataName), handle());
return nodeKeyIsDirty(wrappedDataKey.wrappedKey().wrappingKeyName());
}
/**
* Find the key to use to wrap a data key at this node for encryption. This requires
* the current effective node key, and wrapping this data key in it. If the
* current node key is dirty, this causes a new one to be generated.
* If data at the current node is public, this returns null. Does not check
* to see whether content is excluded from encryption (e.g. by being access
* control data).
* @param dataNodeName the node for which to find a data key wrapping key
* @param publisher in case output key retrieval needs to be specialized by publisher
* @return if null, the data is to be unencrypted.
* @param newRandomDataKey
* @throws AccessDeniedException
* @throws InvalidKeyException
* @throws ContentEncodingException
* @throws IOException
* @throws NoSuchAlgorithmException
*/
@Override
public NodeKey getDataKeyWrappingKey(ContentName dataNodeName, PublisherPublicKeyDigest publisher)
throws AccessDeniedException, InvalidKeyException,
ContentEncodingException, IOException, NoSuchAlgorithmException {
NodeKey effectiveNodeKey = getFreshEffectiveNodeKey(dataNodeName);
if (null == effectiveNodeKey) {
throw new AccessDeniedException("Cannot retrieve effective node key for node: " + dataNodeName + ".");
}
return effectiveNodeKey;
}
/**
* Retrieve the node key wrapping this data key for decryption.
* @throws IOException
* @throws ContentDecodingException
* @throws ContentEncodingException
* @throws ContentGoneException
* @throws ContentNotReadyException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
*/
@Override
public Key getDataKeyWrappingKey(ContentName dataNodeName, WrappedKeyObject wrappedDataKeyObject)
throws InvalidKeyException, ContentNotReadyException, ContentGoneException, ContentEncodingException,
ContentDecodingException, IOException, NoSuchAlgorithmException {
NodeKey enk = getNodeKeyForObject(dataNodeName, wrappedDataKeyObject);
if (null != enk) {
return enk.nodeKey();
}
return null;
}
/**
* Get the data key wrapping key if we happened to have cached a copy of the decryption key.
* @param dataNodeName
* @param wrappedDataKeyObject
* @param cachedWrappingKey
* @return
* @throws ContentEncodingException
* @throws InvalidKeyException
*/
@Override
public Key getDataKeyWrappingKey(ContentName dataNodeName, ContentName wrappingKeyName, Key cachedWrappingKey) throws InvalidKeyException, ContentEncodingException {
NodeKey cachedWrappingKeyNK = new NodeKey(wrappingKeyName, cachedWrappingKey);
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "getDataKeyWrappingKey: retrieved cached stored node key for node {0} label {1}: {2}", dataNodeName, nodeKeyLabel(), cachedWrappingKeyNK);
}
NodeKey enk = cachedWrappingKeyNK.computeDescendantNodeKey(dataNodeName, nodeKeyLabel());
if (null != enk) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "getDataKeyWrappingKey: used cache to compute effective node key for node {0} label {1}: {2}", dataNodeName, nodeKeyLabel(), enk);
}
return enk.nodeKey();
} else {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.finer(Log.FAC_ACCESSCONTROL, "getDataKeyWrappingKey: cannot compute effective node key for node {0} label {1} from cached key {2}",
dataNodeName, nodeKeyLabel(), cachedWrappingKeyNK);
}
}
return null;
}
/**
* We've looked for a node key we can decrypt at the expected node key location,
* but no dice. See if a new ACL has been interposed granting us rights at a lower
* portion of the tree.
* @param dataNodeName
* @param wrappingKeyName
* @param wrappingKeyIdentifier
* @return
* @throws IOException
* @throws ContentDecodingException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
*/
protected NodeKey getNodeKeyUsingInterposedACL(ContentName dataNodeName,
ContentName wrappingKeyName, byte[] wrappingKeyIdentifier)
throws ContentDecodingException, IOException, InvalidKeyException, NoSuchAlgorithmException {
ContentName stopPoint = AccessControlProfile.accessRoot(wrappingKeyName);
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "getNodeKeyUsingInterposedACL: looking for an ACL above {0} but below {1}",
dataNodeName, stopPoint);
}
ACLObject nearestACL = findAncestorWithACL(dataNodeName, stopPoint);
// TODO update to make sure non-gone....
if (null == nearestACL) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "Node key {0} is the nearest ACL to {1}", wrappingKeyName , dataNodeName);
}
return null;
}
NodeKey currentNodeKey = getLatestNodeKeyForNode(GroupAccessControlProfile.accessRoot(nearestACL.getVersionedName()));
// We have retrieved the current node key at the node where the ACL was interposed.
// But the data key is wrapped in the previous node key that was at this node prior to the ACL interposition.
// So we need to retrieve the previous node key, which was wrapped with KeyDirectory.addPreviousKeyBlock
// at the time the ACL was interposed.
ContentName previousKeyName = new ContentName(currentNodeKey.storedNodeKeyName(), PREVIOUS_KEY);
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.FINER)) {
Log.finer(Log.FAC_ACCESSCONTROL, "getNodeKeyUsingInterposedACL: retrieving previous key at {0}", previousKeyName);
}
WrappedKeyObject wrappedPreviousNodeKey = new WrappedKeyObject(previousKeyName, _handle);
wrappedPreviousNodeKey.update();
Key pnk = wrappedPreviousNodeKey.wrappedKey().unwrapKey(currentNodeKey.nodeKey());
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.FINER)) {
Log.finer(Log.FAC_ACCESSCONTROL, "getNodeKeyUsingInterposedACL: returning previous node key for node {0}", currentNodeKey.storedNodeKeyName());
}
NodeKey previousNodeKey = new NodeKey(currentNodeKey.storedNodeKeyName(), pnk);
return previousNodeKey;
}
/**
* Make a new node key and encrypt it under the given ACL.
* If there is a previous node key (oldEffectiveNodeKey not null), it is wrapped in the new node key.
* Put all the blocks into the aggregating writer, but don't flush.
*
* @param nodeName
* @param oldEffectiveNodeKey
* @param effectiveACL
* @return
* @throws IOException
* @throws ContentGoneException
* @throws ContentNotReadyException
* @throws ContentEncodingException
* @throws InvalidKeyException
*/
protected NodeKey generateNewNodeKey(ContentName nodeName, NodeKey oldEffectiveNodeKey, ACL effectiveACL)
throws InvalidKeyException, ContentEncodingException, ContentNotReadyException,
ContentGoneException, IOException {
// Get the name of the key directory; this is unversioned. Make a new version of it.
ContentName nodeKeyDirectoryName = VersioningProfile.addVersion(GroupAccessControlProfile.nodeKeyName(nodeName));
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "GenerateNewNodeKey: generating new node key {0}", nodeKeyDirectoryName);
Log.info(Log.FAC_ACCESSCONTROL, "GenerateNewNodeKey: for node {0} with old effective node key {1}", nodeName, oldEffectiveNodeKey);
}
// Now, generate the node key.
if (effectiveACL.publiclyReadable()) {
// TODO Put something here that will represent public; need to then make it so that key-reading code will do
// the right thing when it encounters it.
throw new UnsupportedOperationException("Need to implement public node key representation!");
}
byte [] nodeKeyBytes = new byte[NodeKey.DEFAULT_NODE_KEY_LENGTH];
_random.nextBytes(nodeKeyBytes);
Key nodeKey = new SecretKeySpec(nodeKeyBytes, NodeKey.DEFAULT_NODE_KEY_ALGORITHM);
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.FINER)) {
Log.finer(Log.FAC_ACCESSCONTROL, "GenerateNewNodeKey: for node {0} the new node key is {1}", nodeName, DataUtils.printHexBytes(nodeKey.getEncoded()));
}
// Now, wrap it under the keys listed in its ACL.
// Make a key directory. If we give it a versioned name. Don't start enumerating; we don't need to.
PrincipalKeyDirectory nodeKeyDirectory = new PrincipalKeyDirectory(this, nodeKeyDirectoryName, false, handle());
NodeKey theNodeKey = new NodeKey(nodeKeyDirectoryName, nodeKey);
// Add a key block for every reader on the ACL. As managers and writers can read, they are all readers.
// TODO -- pulling public keys here; could be slow; might want to manage concurrency over acl.
for (Link aclEntry : effectiveACL.contents()) {
PublicKeyObject entryPublicKey = null;
boolean isInGroupManager = false;
for (GroupManager gm: _groupManager) {
if (gm.isGroup(aclEntry)) {
// MLAC this should load the correct key for the group
entryPublicKey = gm.getLatestPublicKeyForGroup(aclEntry);
isInGroupManager = true;
break;
}
}
if (! isInGroupManager) {
// MLAC should pull parameterized user key name
// Calls update. Will get latest version if name unversioned.
ContentName principalKeyName = userPublicKeyName(aclEntry.targetName());
if (aclEntry.targetAuthenticator() != null) {
entryPublicKey = new PublicKeyObject(principalKeyName, aclEntry.targetAuthenticator().publisher(), handle());
} else {
entryPublicKey = new PublicKeyObject(principalKeyName, handle());
}
}
entryPublicKey.waitForData(SystemConfiguration.getDefaultTimeout());
try {
nodeKeyDirectory.addWrappedKeyBlock(nodeKey, entryPublicKey.getVersionedName(), entryPublicKey.publicKey());
} catch (VersionMissingException ve) {
Log.logException("Unexpected version missing exception for public key " + entryPublicKey.getVersionedName(), ve);
throw new IOException("Unexpected version missing exception for public key " + entryPublicKey.getVersionedName() + ": " + ve);
}
}
// Add a superseded by block to the previous key. Two cases: old effective node key is at the same level
// as us (we are superseding it entirely), or we are interposing a key (old key is above or below us).
// OK, here are the options:
// Replaced node key is a derived node key -- we are interposing an ACL
// Replaced node key is a stored node key
// -- we are updating that node key to a new version
// NK/vn replaced by NK/vn+k -- new node key will be later version of previous node key
// -- we don't get called if we are deleting an ACL here -- no new node key is added.
if (oldEffectiveNodeKey != null) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.FINER)) {
Log.finer(Log.FAC_ACCESSCONTROL, "GenerateNewNodeKey: old effective node key is not null.");
}
if (oldEffectiveNodeKey.isDerivedNodeKey()) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.FINER)) {
Log.finer(Log.FAC_ACCESSCONTROL, "GenerateNewNodeKey: old effective node key is derived node key.");
}
// Interposing an ACL.
// Add a previous key block wrapping the previous key. There is nothing to link to.
nodeKeyDirectory.addPreviousKeyBlock(oldEffectiveNodeKey.nodeKey(), nodeKeyDirectoryName, nodeKey);
} else {
// We're replacing a previous version of this key. New version should have a previous key
// entry
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.FINER)) {
Log.finer(Log.FAC_ACCESSCONTROL, "GenerateNewNodeKey: old effective node key is not a derived node key.");
}
try {
if (!VersioningProfile.isLaterVersionOf(nodeKeyDirectoryName, oldEffectiveNodeKey.storedNodeKeyName())) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.WARNING)) {
Log.warning(Log.FAC_ACCESSCONTROL, "GenerateNewNodeKey: Unexpected: replacing node key stored at {0} with new node key {1}" +
" but latter is not later version of the former.", oldEffectiveNodeKey.storedNodeKeyName(), nodeKeyDirectoryName);
}
}
} catch (VersionMissingException vex) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.WARNING)) {
Log.warning(Log.FAC_ACCESSCONTROL, "Very unexpected version missing exception when replacing node key : {0}", vex);
}
// Add a previous key link to the old version of the key.
// TODO do we need to add publisher?
nodeKeyDirectory.waitForChildren();
nodeKeyDirectory.addPreviousKeyLink(oldEffectiveNodeKey.storedNodeKeyName(), null);
// OK, just add superseded-by block to the old directory.
PrincipalKeyDirectory.addSupersededByBlock(
oldEffectiveNodeKey.storedNodeKeyName(), oldEffectiveNodeKey.nodeKey(),
theNodeKey.storedNodeKeyName(), theNodeKey.storedNodeKeyID(), theNodeKey.nodeKey(), handle());
}
}
}
// Return the key for use, along with its name.
return theNodeKey;
}
/**
*
* @param nodeName
* @param wko
* @return
* @throws ContentNotReadyException
* @throws ContentGoneException
* @throws InvalidKeyException
* @throws ContentEncodingException
* @throws ContentDecodingException
* @throws IOException
* @throws NoSuchAlgorithmException
*/
public NodeKey getNodeKeyForObject(ContentName nodeName, WrappedKeyObject wko)
throws ContentNotReadyException, ContentGoneException, InvalidKeyException, ContentEncodingException,
ContentDecodingException, IOException, NoSuchAlgorithmException {
// First, we go and look for the node key where the data key suggests
// it should be, and attempt to decrypt it from there.
NodeKey nk = null;
try {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "getNodeKeyForObject: trying to get specific node key at {0}", wko.wrappedKey().wrappingKeyName());
}
nk = getSpecificNodeKey(wko.wrappedKey().wrappingKeyName(),
wko.wrappedKey().wrappingKeyIdentifier());
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "getNodeKeyForObject: got specific node key {0} at {1}", nk, wko.wrappedKey().wrappingKeyName());
}
} catch (AccessDeniedException ex) {
// ignore
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "getNodeKeyForObject: ignoring access denied exception as we're gong to try harder: {0}", ex.getMessage());
}
}
if (null == nk) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "getNodeKeyForObject: trying to get node key using interposed ACL for {0}", wko.wrappedKey().wrappingKeyName());
}
// OK, we will have gotten an exception if the node key simply didn't exist
// there, so this means that we don't have rights to read it there.
// The only way we might have rights not visible from this link is if an
// ACL has been interposed between where we are and the node key, and that
// ACL does give us rights.
nk = getNodeKeyUsingInterposedACL(nodeName, wko.wrappedKey().wrappingKeyName(),
wko.wrappedKey().wrappingKeyIdentifier());
if (null == nk) {
// Still can't find one we can read. Give up. Return null, and allow caller to throw the
// access exception.
return null;
}
}
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "getNodeKeyForObject: retrieved stored node key for node {0} label {1}: {2}", nodeName, nodeKeyLabel(), nk);
}
NodeKey enk = nk.computeDescendantNodeKey(nodeName, nodeKeyLabel());
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "getNodeKeyForObject: computed effective node key for node {0} label {1}: {2}", nodeName, nodeKeyLabel(), enk);
}
return enk;
}
/**
* Overrides the method of the same name in AccessControlManager.
* GroupAccessControlManager specifies additional content that is not to be protected,
* such as group metadata.
*/
public boolean isProtectedContent(ContentName name, PublisherPublicKeyDigest publisher, ContentType contentType, CCNHandle handle) {
if (isGroupName(name)) {
// Don't encrypt the group metadata
return false;
}
return super.isProtectedContent(name, publisher, contentType, handle);
}
public boolean haveKnownGroupMemberships() {
for (GroupManager gm: _groupManager) {
if (gm.haveKnownGroupMemberships()) return true;
}
return false;
}
}