/* * Part of the CCNx Java Library. * * Copyright (C) 2008, 2009, 2011 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.HashMap; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Level; import org.ccnx.ccn.CCNHandle; import org.ccnx.ccn.config.SystemConfiguration; import org.ccnx.ccn.impl.CCNFlowControl.SaveType; import org.ccnx.ccn.impl.support.DataUtils; import org.ccnx.ccn.impl.support.Log; import org.ccnx.ccn.io.content.ContentDecodingException; import org.ccnx.ccn.io.content.ContentEncodingException; import org.ccnx.ccn.io.content.ContentGoneException; import org.ccnx.ccn.io.content.ContentNotReadyException; import org.ccnx.ccn.io.content.KeyDirectory; import org.ccnx.ccn.io.content.Link; import org.ccnx.ccn.io.content.Link.LinkObject; import org.ccnx.ccn.io.content.WrappedKey.WrappedKeyObject; import org.ccnx.ccn.profiles.VersionMissingException; import org.ccnx.ccn.profiles.security.access.AccessDeniedException; import org.ccnx.ccn.profiles.security.access.group.GroupAccessControlProfile.PrincipalInfo; import org.ccnx.ccn.protocol.ContentName; /** * This structure is used for representing both node keys and group * (private) keys. We encapsulate functionality to walk such a directory * and find our target key here. * * We store links providing additional information about how to retrieve * this key -- e.g. a link from a given group or principal name to a key ID-named * block, in case a group member does not know an earlier version of their * group public key. Or links to keys this key supercedes or precedes. */ public class PrincipalKeyDirectory extends KeyDirectory { GroupAccessControlManager _manager; // to get at GroupManager /** * Maps the friendly names of principals (typically groups) to their information. */ HashMap<String, PrincipalInfo> _principals = new HashMap<String, PrincipalInfo>(); final ReadWriteLock _principalsLock = new ReentrantReadWriteLock(); /** * Directory name should be versioned, else we pull the latest version; start * enumeration. * @param manager the access control manager. * @param directoryName the root of the KeyDirectory. * @param handle * @throws IOException */ public PrincipalKeyDirectory(GroupAccessControlManager manager, ContentName directoryName, CCNHandle handle) throws IOException { this(manager, directoryName, true, handle); } /** * Directory name should be versioned, else we pull the latest version. * @param manager the access control manager - must not be null * @param directoryName the root of the KeyDirectory. * @param handle * @throws IOException */ public PrincipalKeyDirectory(GroupAccessControlManager manager, ContentName directoryName, boolean enumerate, CCNHandle handle) throws IOException { super(directoryName, enumerate, handle); _manager = manager; // now that our class's variables are set up we can call the superclass's initialize method. super.initialize(enumerate); } /** * Defer initialization until the end of our constructor since this * class's variables are not set up yet, so we're not ready for callbacks. * @see PrincipalKeyDirectory#PrincipalKeyDirectory(GroupAccessControlManager, ContentName, boolean, CCNHandle) * @see KeyDirectory#KeyDirectory(ContentName, boolean, CCNHandle) */ @Override protected void initialize(boolean startEnumerating) throws IOException { } /** * Called each time new data comes in, gets to parse it and load processed * arrays. */ @Override protected void processNewChild(byte [] wkChildName) { if (PrincipalInfo.isPrincipalNameComponent(wkChildName)) { addPrincipal(wkChildName); } else super.processNewChild(wkChildName); } /** * Return a copy to avoid synchronization problems. * @throws ContentNotReadyException */ public HashMap<String, PrincipalInfo> getCopyOfPrincipals() throws ContentNotReadyException { if (!hasChildren()) { throw new ContentNotReadyException("Need to call waitForData(); assuming directory known to be non-empty!"); } HashMap<String, PrincipalInfo> copy = new HashMap<String, PrincipalInfo>(); try { _principalsLock.readLock().lock(); for (String key: _principals.keySet()) { PrincipalInfo value = _principals.get(key); copy.put(key, value); } } finally { _principalsLock.readLock().unlock(); } return copy; } /** * Adds a principal name * @param wkChildName the principal name */ protected void addPrincipal(byte [] wkChildName) { PrincipalInfo pi = new PrincipalInfo(wkChildName); _principalsLock.writeLock().lock(); try{ _principals.put(pi.friendlyName(), pi); }finally{ _principalsLock.writeLock().unlock(); } } /** * Store an additional link object pointing to the wrapped key object * in the KeyDirectory. The link object is named with the Principal's name * to allow searching the KeyDirectory by Principal name rather than KeyID. */ @Override public WrappedKeyObject addWrappedKeyBlock(Key secretKeyToWrap, ContentName publicKeyName, PublicKey publicKey) throws ContentEncodingException, IOException, InvalidKeyException, VersionMissingException { WrappedKeyObject wko = super.addWrappedKeyBlock(secretKeyToWrap, publicKeyName, publicKey); LinkObject lo = new LinkObject(getWrappedKeyNameForPrincipal(publicKeyName), new Link(wko.getVersionedName()), SaveType.REPOSITORY, _handle); lo.save(); return wko; } @Override protected KeyDirectory factory(ContentName name) throws IOException { return new PrincipalKeyDirectory(_manager, name, _handle); } /** * Returns the wrapped key object corresponding to a specified principal. * @param principalName the principal. * @return the corresponding wrapped key object. * @throws IOException * @throws ContentNotReadyException * @throws ContentDecodingException */ protected WrappedKeyObject getWrappedKeyForPrincipal(String principalName) throws ContentNotReadyException, ContentDecodingException, IOException { if (!hasChildren()) { throw new ContentNotReadyException("Need to call waitForData(); assuming directory known to be non-empty!"); } PrincipalInfo pi = null; try{ _principalsLock.readLock().lock(); if (!_principals.containsKey(principalName)) { return null; } pi = _principals.get(principalName); }finally{ _principalsLock.readLock().unlock(); } if (null == pi) { if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) { Log.info(Log.FAC_ACCESSCONTROL, "No block available for principal: {0}", principalName); } return null; } ContentName principalLinkName = getWrappedKeyNameForPrincipal(pi); // This should be a link to the actual key block // TODO DKS should wait on link data... LinkObject principalLink = new LinkObject(principalLinkName, _handle); if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) { Log.info(Log.FAC_ACCESSCONTROL, "Retrieving wrapped key for principal {0} at {1}", principalName, principalLink.getTargetName()); } ContentName wrappedKeyName = principalLink.getTargetName(); return getWrappedKey(wrappedKeyName); } /** * Returns the wrapped key name for a specified principal. * @param isGroup whether the principal is a group. * @param principalName the name of the principal. * @param principalVersion the version of the principal. * @return the corresponding wrapped key name. */ protected ContentName getWrappedKeyNameForPrincipal(PrincipalInfo pi) { ContentName principalLinkName = new ContentName(_namePrefix, pi.toNameComponent()); return principalLinkName; } /** * Returns the wrapped key name for a principal specified by the name of its public key. * @param principalPublicKeyName the name of the public key of the principal. * @return the corresponding wrapped key name. * @throws VersionMissingException * @throws ContentEncodingException */ protected ContentName getWrappedKeyNameForPrincipal(ContentName principalPublicKeyName) throws VersionMissingException, ContentEncodingException { PrincipalInfo info = new PrincipalInfo(_manager, principalPublicKeyName); return getWrappedKeyNameForPrincipal(info); } @Override protected Key findUnwrappedKey(byte[] expectedKeyID) throws IOException, ContentNotReadyException, InvalidKeyException, ContentDecodingException, NoSuchAlgorithmException { if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.FINEST)) { Log.finest(Log.FAC_ACCESSCONTROL, "PrincipalKeyDirectory.findUnwrappedKey({0})", DataUtils.printHexBytes(expectedKeyID)); } Key unwrappedKey = super.findUnwrappedKey(expectedKeyID); if (unwrappedKey == null) { // This is the current key. Enumerate principals and see if we can get a key to unwrap. if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) { Log.info(Log.FAC_ACCESSCONTROL, "PrincipalKeyDirectory.findUnwrappedKey: at latest version of key {0}, attempting to unwrap.", getName()); } // Assumption: if this key was encrypted directly for me, I would have had a cache // hit already. The assumption is that I pre-load my cache with my own private key(s). // So I don't care about principal entries if I get here, I only care about groups. // Groups may come in three types: ones I know I am a member of, but don't have this // particular key version for, ones I don't know anything about, and ones I believe // I'm not a member of but someone might have added me. if (_manager.haveKnownGroupMemberships()) { unwrappedKey = unwrapKeyViaKnownGroupMembership(); } if (unwrappedKey == null) { // OK, we don't have any groups we know we are a member of. Do the other ones. // Slower, as we crawl the groups tree. unwrappedKey = unwrapKeyViaNotKnownGroupMembership(); } return unwrappedKey; } return unwrappedKey; } protected Key unwrapKeyViaKnownGroupMembership() throws InvalidKeyException, ContentDecodingException, IOException, NoSuchAlgorithmException { Key unwrappedKey = null; try{ _principalsLock.readLock().lock(); if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) { Log.info(Log.FAC_ACCESSCONTROL, "PrincipalKeyDirectory.unwrapKeyViaKnownGroupMembership: the directory has {0} principals.", _principals.size()); } for (String principal : _principals.keySet()) { PrincipalInfo pInfo = _principals.get(principal); GroupManager pgm = _manager.groupManager(pInfo.distinguishingHash()); if ((pgm == null) || (! pgm.isGroup(principal, SystemConfiguration.EXTRA_LONG_TIMEOUT)) || (! pgm.amKnownGroupMember(principal))) { // On this pass, only do groups that I think I'm a member of. Do them // first as it is likely faster. continue; } // I know I am a member of this group, or at least I was last time I checked. // Attempt to get this version of the group private key as I don't have it in my cache. try { Key principalKey = pgm.getVersionedPrivateKeyForGroup(pInfo); unwrappedKey = unwrapKeyForPrincipal(principal, principalKey); if (null == unwrappedKey) continue; } catch (AccessDeniedException aex) { // we're not a member continue; } } } finally { _principalsLock.readLock().unlock(); } return unwrappedKey; } protected Key unwrapKeyViaNotKnownGroupMembership() throws InvalidKeyException, ContentDecodingException, IOException, NoSuchAlgorithmException { Key unwrappedKey = null; try{ _principalsLock.readLock().lock(); for (PrincipalInfo pInfo : _principals.values()) { String principal = pInfo.friendlyName(); if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) { Log.info(Log.FAC_ACCESSCONTROL, "PrincipalKeyDirectory.unwrapKeyViaNotKnownGroupMembership: the KD secret key is wrapped under the key of principal {0}", pInfo); } GroupManager pgm = _manager.groupManager(pInfo.distinguishingHash()); if ((pgm == null) || (! pgm.isGroup(principal, SystemConfiguration.EXTRA_LONG_TIMEOUT)) || (pgm.amKnownGroupMember(principal))) { // On this pass, only do groups that I don't think I'm a member of. if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.FINER)) { Log.finer(Log.FAC_ACCESSCONTROL, "PrincipalKeyDirectory.unwrapKeyViaNotKnownGroupMembership: skipping principal {0}.", principal); } continue; } if (pgm.amCurrentGroupMember(principal)) { if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.FINER)) { Log.finer(Log.FAC_ACCESSCONTROL, "PrincipalKeyDirectory.unwrapKeyViaNotKnownGroupMembership: I am a member of group {0} ", principal); } try { Key principalKey = pgm.getVersionedPrivateKeyForGroup(pInfo); unwrappedKey = unwrapKeyForPrincipal(principal, principalKey); if (null == unwrappedKey) { if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.WARNING)) { Log.warning(Log.FAC_ACCESSCONTROL, "Unexpected: we are a member of group {0} but get a null key.", principal); } continue; } } catch (AccessDeniedException aex) { if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.WARNING)) { Log.warning(Log.FAC_ACCESSCONTROL, "Unexpected: we are a member of group " + principal + " but get an access denied exception when we try to get its key: " + aex.getMessage()); } continue; } } else { if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) { Log.info(Log.FAC_ACCESSCONTROL, "PrincipalKeyDirectory.unwrapKeyViaNotKnownGroupMembership: I am not a member of group {0} ", principal); } } } } finally { _principalsLock.readLock().unlock(); } return unwrappedKey; } /** * Unwrap the key wrapped under a specified principal, with a specified unwrapping key. * @param principal * @param unwrappingKey * @return * @throws ContentGoneException * @throws ContentNotReadyException * @throws ContentDecodingException * @throws InvalidKeyException * @throws IOException * @throws NoSuchAlgorithmException */ protected Key unwrapKeyForPrincipal(String principal, Key unwrappingKey) throws InvalidKeyException, ContentNotReadyException, ContentDecodingException, ContentGoneException, IOException, NoSuchAlgorithmException { Key unwrappedKey = null; if (null == unwrappingKey) { if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) { Log.info(Log.FAC_ACCESSCONTROL, "Null unwrapping key. Cannot unwrap."); } return null; } WrappedKeyObject wko = getWrappedKeyForPrincipal(principal); // checks hasChildren if (null != wko.wrappedKey()) { unwrappedKey = wko.wrappedKey().unwrapKey(unwrappingKey); } else { try{ _principalsLock.readLock().lock(); if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) { Log.info(Log.FAC_ACCESSCONTROL, "Unexpected: retrieved version {0} of {1} group key, but cannot retrieve wrapped key object.", _principals.get(principal), principal); } }finally{ _principalsLock.readLock().unlock(); } } return unwrappedKey; } }