Author: wang Date: Fri Jul 11 20:54:47 2014 New Revision: 1609833 URL: http://svn.apache.org/r1609833 Log: HDFS-6474. Namenode needs to get the actual keys and iv from the KeyProvider. (wang)
Added: hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/RetryStartFileException.java (with props) Modified: hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/CHANGES-fs-encryption.txt hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/DFSConfigKeys.java hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/protocolPB/PBHelper.java hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/EncryptionZoneManager.java hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/FSDirectory.java hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/FSNamesystem.java hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/proto/hdfs.proto hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/resources/hdfs-default.xml hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/TestEncryptionZonesAPI.java Modified: hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/CHANGES-fs-encryption.txt URL: http://svn.apache.org/viewvc/hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/CHANGES-fs-encryption.txt?rev=1609833&r1=1609832&r2=1609833&view=diff ============================================================================== --- hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/CHANGES-fs-encryption.txt (original) +++ hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/CHANGES-fs-encryption.txt Fri Jul 11 20:54:47 2014 @@ -39,6 +39,9 @@ fs-encryption (Unreleased) HDFS-6635. Refactor encryption zone functionality into new EncryptionZoneManager class. (wang) + HDFS-6474. Namenode needs to get the actual keys and iv from the + KeyProvider. (wang) + OPTIMIZATIONS BUG FIXES Modified: hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/DFSConfigKeys.java URL: http://svn.apache.org/viewvc/hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/DFSConfigKeys.java?rev=1609833&r1=1609832&r2=1609833&view=diff ============================================================================== --- hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/DFSConfigKeys.java (original) +++ hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/DFSConfigKeys.java Fri Jul 11 20:54:47 2014 @@ -561,6 +561,8 @@ public class DFSConfigKeys extends Commo public static final boolean DFS_ENCRYPT_DATA_TRANSFER_DEFAULT = false; public static final String DFS_DATA_ENCRYPTION_ALGORITHM_KEY = "dfs.encrypt.data.transfer.algorithm"; public static final String DFS_TRUSTEDCHANNEL_RESOLVER_CLASS = "dfs.trustedchannel.resolver.class"; + public static final String DFS_NAMENODE_KEY_VERSION_REFRESH_INTERVAL_MS_KEY = "dfs.namenode.key.version.refresh.interval.ms"; + public static final int DFS_NAMENODE_KEY_VERSION_REFRESH_INTERVAL_MS_DEFAULT = 5*60*1000; // Journal-node related configs. These are read on the JN side. public static final String DFS_JOURNALNODE_EDITS_DIR_KEY = "dfs.journalnode.edits.dir"; Modified: hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/protocolPB/PBHelper.java URL: http://svn.apache.org/viewvc/hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/protocolPB/PBHelper.java?rev=1609833&r1=1609832&r2=1609833&view=diff ============================================================================== --- hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/protocolPB/PBHelper.java (original) +++ hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/protocolPB/PBHelper.java Fri Jul 11 20:54:47 2014 @@ -2335,6 +2335,7 @@ public class PBHelper { .setSuite(convert(info.getCipherSuite())) .setKey(getByteString(info.getEncryptedDataEncryptionKey())) .setIv(getByteString(info.getIV())) + .setEzKeyVersionName(info.getEzKeyVersionName()) .build(); } @@ -2346,7 +2347,8 @@ public class PBHelper { CipherSuite suite = convert(proto.getSuite()); byte[] key = proto.getKey().toByteArray(); byte[] iv = proto.getIv().toByteArray(); - return new FileEncryptionInfo(suite, key, iv); + String ezKeyVersionName = proto.getEzKeyVersionName(); + return new FileEncryptionInfo(suite, key, iv, ezKeyVersionName); } } Modified: hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/EncryptionZoneManager.java URL: http://svn.apache.org/viewvc/hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/EncryptionZoneManager.java?rev=1609833&r1=1609832&r2=1609833&view=diff ============================================================================== --- hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/EncryptionZoneManager.java (original) +++ hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/EncryptionZoneManager.java Fri Jul 11 20:54:47 2014 @@ -3,28 +3,50 @@ package org.apache.hadoop.hdfs.server.na import java.io.IOException; import java.util.EnumSet; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.crypto.key.KeyProvider; import org.apache.hadoop.fs.UnresolvedLinkException; import org.apache.hadoop.fs.XAttr; import org.apache.hadoop.fs.XAttrSetFlag; +import org.apache.hadoop.hdfs.DFSConfigKeys; import org.apache.hadoop.hdfs.XAttrHelper; import org.apache.hadoop.hdfs.protocol.EncryptionZone; import org.apache.hadoop.hdfs.protocol.SnapshotAccessControlException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import static org.apache.hadoop.crypto.key.KeyProvider.KeyVersion; import static org.apache.hadoop.hdfs.server.common.HdfsServerConstants .CRYPTO_XATTR_ENCRYPTION_ZONE; /** - * Manages the list of encryption zones in the filesystem. Relies on the - * FSDirectory lock for synchronization. + * Manages the list of encryption zones in the filesystem. + * <p/> + * The EncryptionZoneManager has its own lock, but relies on the FSDirectory + * lock being held for many operations. The FSDirectory lock should not be + * taken if the manager lock is already held. */ public class EncryptionZoneManager { + public static Logger LOG = LoggerFactory.getLogger(EncryptionZoneManager + .class); + /** * EncryptionZoneInt is the internal representation of an encryption zone. The * external representation of an EZ is embodied in an EncryptionZone and @@ -34,9 +56,30 @@ public class EncryptionZoneManager { private final String keyId; private final long inodeId; + private final HashSet<KeyVersion> keyVersions; + private KeyVersion latestVersion; + EncryptionZoneInt(long inodeId, String keyId) { this.keyId = keyId; this.inodeId = inodeId; + keyVersions = Sets.newHashSet(); + latestVersion = null; + } + + KeyVersion getLatestKeyVersion() { + return latestVersion; + } + + void addKeyVersion(KeyVersion version) { + Preconditions.checkNotNull(version); + if (!keyVersions.contains(version)) { + LOG.debug("Key {} has new key version {}", keyId, version); + keyVersions.add(version); + } + // Always set the latestVersion to not get stuck on an old version in + // racy situations. Should eventually converge thanks to the + // monitor. + latestVersion = version; } String getKeyId() { @@ -47,49 +90,265 @@ public class EncryptionZoneManager { return inodeId; } - String getFullPathName() { - return dir.getInode(inodeId).getFullPathName(); - } } - private final Map<Long, EncryptionZoneInt> encryptionZones; + /** + * Protects the <tt>encryptionZones</tt> map and its contents. + */ + private final ReentrantReadWriteLock lock; + + private void readLock() { + lock.readLock().lock(); + } + + private void readUnlock() { + lock.readLock().unlock(); + } + + private void writeLock() { + lock.writeLock().lock(); + } + + private void writeUnlock() { + lock.writeLock().unlock(); + } + + public boolean hasWriteLock() { + return lock.isWriteLockedByCurrentThread(); + } + public boolean hasReadLock() { + return lock.getReadHoldCount() > 0 || hasWriteLock(); + } + + private final Map<Long, EncryptionZoneInt> encryptionZones; private final FSDirectory dir; + private final ScheduledExecutorService monitor; + private final KeyProvider provider; /** * Construct a new EncryptionZoneManager. * * @param dir Enclosing FSDirectory */ - public EncryptionZoneManager(FSDirectory dir) { + public EncryptionZoneManager(FSDirectory dir, Configuration conf, + KeyProvider provider) { + this.dir = dir; + this.provider = provider; + lock = new ReentrantReadWriteLock(); encryptionZones = new HashMap<Long, EncryptionZoneInt>(); + + monitor = Executors.newScheduledThreadPool(1, + new ThreadFactoryBuilder() + .setDaemon(true) + .setNameFormat(EncryptionZoneMonitor.class.getSimpleName() + "-%d") + .build()); + final int refreshMs = conf.getInt( + DFSConfigKeys.DFS_NAMENODE_KEY_VERSION_REFRESH_INTERVAL_MS_KEY, + DFSConfigKeys.DFS_NAMENODE_KEY_VERSION_REFRESH_INTERVAL_MS_DEFAULT + ); + Preconditions.checkArgument(refreshMs >= 0, "%s cannot be negative", + DFSConfigKeys.DFS_NAMENODE_KEY_VERSION_REFRESH_INTERVAL_MS_KEY); + monitor.scheduleAtFixedRate(new EncryptionZoneMonitor(), 0, refreshMs, + TimeUnit.MILLISECONDS); + } + + /** + * Periodically wakes up to fetch the latest version of each encryption + * zone key. + */ + private class EncryptionZoneMonitor implements Runnable { + @Override + public void run() { + LOG.debug("Monitor waking up to refresh encryption zone key versions"); + HashMap<Long, String> toFetch = Maps.newHashMap(); + HashMap<Long, KeyVersion> toUpdate = + Maps.newHashMap(); + // Determine the keyIds to fetch + readLock(); + try { + for (EncryptionZoneInt ezi : encryptionZones.values()) { + toFetch.put(ezi.getINodeId(), ezi.getKeyId()); + } + } finally { + readUnlock(); + } + LOG.trace("Found {} keys to check", toFetch.size()); + // Fetch the key versions while not holding the lock + for (Map.Entry<Long, String> entry : toFetch.entrySet()) { + try { + KeyVersion version = provider.getCurrentKey(entry.getValue()); + toUpdate.put(entry.getKey(), version); + } catch (IOException e) { + LOG.warn("Error while getting the current key for {} {}", + entry.getValue(), e); + } + } + LOG.trace("Fetched {} key versions from KeyProvider", toUpdate.size()); + // Update the key versions for each encryption zone + writeLock(); + try { + for (Map.Entry<Long, KeyVersion> entry : toUpdate.entrySet()) { + EncryptionZoneInt ezi = encryptionZones.get(entry.getKey()); + // zone might have been removed in the intervening time + if (ezi == null) { + continue; + } + ezi.addKeyVersion(entry.getValue()); + } + } finally { + writeUnlock(); + } + } + } + + /** + * Forces the EncryptionZoneMonitor to run, waiting until completion. + */ + @VisibleForTesting + public void kickMonitor() throws Exception { + Future future = monitor.submit(new EncryptionZoneMonitor()); + future.get(); + } + + /** + * Immediately fetches the latest KeyVersion for an encryption zone, + * also updating the encryption zone. + * + * @param iip of the encryption zone + * @return latest KeyVersion + * @throws IOException on KeyProvider error + */ + KeyVersion updateLatestKeyVersion(INodesInPath iip) throws IOException { + EncryptionZoneInt ezi; + readLock(); + try { + ezi = getEncryptionZoneForPath(iip); + } finally { + readUnlock(); + } + if (ezi == null) { + throw new IOException("Cannot update KeyVersion since iip is not within" + + " an encryption zone"); + } + + // Do not hold the lock while doing KeyProvider operations + KeyVersion version = provider.getCurrentKey(ezi.getKeyId()); + + writeLock(); + try { + ezi.addKeyVersion(version); + return version; + } finally { + writeUnlock(); + } } /** * Add a new encryption zone. + * <p/> + * Called while holding the FSDirectory lock. * * @param inodeId of the encryption zone * @param keyId encryption zone key id */ void addEncryptionZone(Long inodeId, String keyId) { + assert dir.hasWriteLock(); final EncryptionZoneInt ez = new EncryptionZoneInt(inodeId, keyId); - encryptionZones.put(inodeId, ez); + writeLock(); + try { + encryptionZones.put(inodeId, ez); + } finally { + writeUnlock(); + } } + /** + * Remove an encryption zone. + * <p/> + * Called while holding the FSDirectory lock. + */ void removeEncryptionZone(Long inodeId) { - encryptionZones.remove(inodeId); + assert dir.hasWriteLock(); + writeLock(); + try { + encryptionZones.remove(inodeId); + } finally { + writeUnlock(); + } } /** * Returns true if an IIP is within an encryption zone. + * <p/> + * Called while holding the FSDirectory lock. */ boolean isInAnEZ(INodesInPath iip) throws UnresolvedLinkException, SnapshotAccessControlException { - return (getEncryptionZoneForPath(iip) != null); + assert dir.hasReadLock(); + readLock(); + try { + return (getEncryptionZoneForPath(iip) != null); + } finally { + readUnlock(); + } + } + + /** + * Returns the path of the EncryptionZoneInt. + * <p/> + * Called while holding the FSDirectory lock. + */ + private String getFullPathName(EncryptionZoneInt ezi) { + assert dir.hasReadLock(); + return dir.getInode(ezi.getINodeId()).getFullPathName(); + } + + KeyVersion getLatestKeyVersion(final INodesInPath iip) { + readLock(); + try { + EncryptionZoneInt ezi = getEncryptionZoneForPath(iip); + if (ezi == null) { + return null; + } + return ezi.getLatestKeyVersion(); + } finally { + readUnlock(); + } + } + + /** + * @return true if the provided <tt>keyVersionName</tt> is the name of a + * valid KeyVersion for the encryption zone of <tt>iip</tt>, + * and <tt>iip</tt> is within an encryption zone. + */ + boolean isValidKeyVersion(final INodesInPath iip, String keyVersionName) { + readLock(); + try { + EncryptionZoneInt ezi = getEncryptionZoneForPath(iip); + if (ezi == null) { + return false; + } + for (KeyVersion ezVersion : ezi.keyVersions) { + if (keyVersionName.equals(ezVersion.getVersionName())) { + return true; + } + } + return false; + } finally { + readUnlock(); + } } + /** + * Looks up the EncryptionZoneInt for a path within an encryption zone. + * Returns null if path is not within an EZ. + * <p/> + * Must be called while holding the manager lock. + */ private EncryptionZoneInt getEncryptionZoneForPath(INodesInPath iip) { + assert hasReadLock(); Preconditions.checkNotNull(iip); final INode[] inodes = iip.getINodes(); for (int i = inodes.length - 1; i >= 0; i--) { @@ -105,8 +364,10 @@ public class EncryptionZoneManager { } /** - * Throws an exception if the provided inode cannot be renamed into the + * Throws an exception if the provided path cannot be renamed into the * destination because of differing encryption zones. + * <p/> + * Called while holding the FSDirectory lock. * * @param srcIIP source IIP * @param dstIIP destination IIP @@ -115,66 +376,101 @@ public class EncryptionZoneManager { */ void checkMoveValidity(INodesInPath srcIIP, INodesInPath dstIIP, String src) throws IOException { - final boolean srcInEZ = (getEncryptionZoneForPath(srcIIP) != null); - final boolean dstInEZ = (getEncryptionZoneForPath(dstIIP) != null); - if (srcInEZ) { - if (!dstInEZ) { - throw new IOException(src + " can't be moved from an encryption zone."); - } - } else { - if (dstInEZ) { - throw new IOException(src + " can't be moved into an encryption zone."); - } - } - - if (srcInEZ || dstInEZ) { + assert dir.hasReadLock(); + readLock(); + try { final EncryptionZoneInt srcEZI = getEncryptionZoneForPath(srcIIP); final EncryptionZoneInt dstEZI = getEncryptionZoneForPath(dstIIP); - Preconditions.checkArgument(srcEZI != null, "couldn't find src EZ?"); - Preconditions.checkArgument(dstEZI != null, "couldn't find dst EZ?"); - if (srcEZI != dstEZI) { - final String srcEZPath = srcEZI.getFullPathName(); - final String dstEZPath = dstEZI.getFullPathName(); - final StringBuilder sb = new StringBuilder(src); - sb.append(" can't be moved from encryption zone "); - sb.append(srcEZPath); - sb.append(" to encryption zone "); - sb.append(dstEZPath); - sb.append("."); - throw new IOException(sb.toString()); + final boolean srcInEZ = (srcEZI != null); + final boolean dstInEZ = (dstEZI != null); + if (srcInEZ) { + if (!dstInEZ) { + throw new IOException( + src + " can't be moved from an encryption zone."); + } + } else { + if (dstInEZ) { + throw new IOException( + src + " can't be moved into an encryption zone."); + } + } + + if (srcInEZ || dstInEZ) { + Preconditions.checkState(srcEZI != null, "couldn't find src EZ?"); + Preconditions.checkState(dstEZI != null, "couldn't find dst EZ?"); + if (srcEZI != dstEZI) { + final String srcEZPath = getFullPathName(srcEZI); + final String dstEZPath = getFullPathName(dstEZI); + final StringBuilder sb = new StringBuilder(src); + sb.append(" can't be moved from encryption zone "); + sb.append(srcEZPath); + sb.append(" to encryption zone "); + sb.append(dstEZPath); + sb.append("."); + throw new IOException(sb.toString()); + } } + } finally { + readUnlock(); } } - XAttr createEncryptionZone(String src, String keyId) throws IOException { - if (dir.isNonEmptyDirectory(src)) { - throw new IOException( - "Attempt to create an encryption zone for a non-empty directory."); - } - - final INodesInPath srcIIP = dir.getINodesInPath4Write(src, false); - final EncryptionZoneInt ezi = getEncryptionZoneForPath(srcIIP); - if (ezi != null) { - throw new IOException("Directory " + src + - " is already in an encryption zone. (" + ezi.getFullPathName() + ")"); - } - - final XAttr keyIdXAttr = - XAttrHelper.buildXAttr(CRYPTO_XATTR_ENCRYPTION_ZONE, keyId.getBytes()); - final List<XAttr> xattrs = Lists.newArrayListWithCapacity(1); - xattrs.add(keyIdXAttr); - final INode inode = - dir.unprotectedSetXAttrs(src, xattrs, EnumSet.of(XAttrSetFlag.CREATE)); - addEncryptionZone(inode.getId(), keyId); - return keyIdXAttr; + /** + * Create a new encryption zone. + * <p/> + * Called while holding the FSDirectory lock. + */ + XAttr createEncryptionZone(String src, String keyId, KeyVersion keyVersion) + throws IOException { + assert dir.hasWriteLock(); + writeLock(); + try { + if (dir.isNonEmptyDirectory(src)) { + throw new IOException( + "Attempt to create an encryption zone for a non-empty directory."); + } + + final INodesInPath srcIIP = dir.getINodesInPath4Write(src, false); + EncryptionZoneInt ezi = getEncryptionZoneForPath(srcIIP); + if (ezi != null) { + throw new IOException("Directory " + src + " is already in an " + + "encryption zone. (" + getFullPathName(ezi) + ")"); + } + + final XAttr keyIdXAttr = XAttrHelper + .buildXAttr(CRYPTO_XATTR_ENCRYPTION_ZONE, keyId.getBytes()); + + final List<XAttr> xattrs = Lists.newArrayListWithCapacity(1); + xattrs.add(keyIdXAttr); + // updating the xattr will call addEncryptionZone, + // done this way to handle edit log loading + dir.unprotectedSetXAttrs(src, xattrs, EnumSet.of(XAttrSetFlag.CREATE)); + // Re-get the new encryption zone add the latest key version + ezi = getEncryptionZoneForPath(srcIIP); + ezi.addKeyVersion(keyVersion); + return keyIdXAttr; + } finally { + writeUnlock(); + } } + /** + * Return the current list of encryption zones. + * <p/> + * Called while holding the FSDirectory lock. + */ List<EncryptionZone> listEncryptionZones() throws IOException { - final List<EncryptionZone> ret = - Lists.newArrayListWithExpectedSize(encryptionZones.size()); - for (EncryptionZoneInt ezi : encryptionZones.values()) { - ret.add(new EncryptionZone(ezi.getFullPathName(), ezi.getKeyId())); + assert dir.hasReadLock(); + readLock(); + try { + final List<EncryptionZone> ret = + Lists.newArrayListWithExpectedSize(encryptionZones.size()); + for (EncryptionZoneInt ezi : encryptionZones.values()) { + ret.add(new EncryptionZone(getFullPathName(ezi), ezi.getKeyId())); + } + return ret; + } finally { + readUnlock(); } - return ret; } } Modified: hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/FSDirectory.java URL: http://svn.apache.org/viewvc/hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/FSDirectory.java?rev=1609833&r1=1609832&r2=1609833&view=diff ============================================================================== --- hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/FSDirectory.java (original) +++ hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/FSDirectory.java Fri Jul 11 20:54:47 2014 @@ -17,6 +17,7 @@ */ package org.apache.hadoop.hdfs.server.namenode; +import static org.apache.hadoop.crypto.key.KeyProvider.KeyVersion; import static org.apache.hadoop.hdfs.server.common.HdfsServerConstants.CRYPTO_XATTR_ENCRYPTION_ZONE; import static org.apache.hadoop.hdfs.server.common.HdfsServerConstants.CRYPTO_XATTR_FILE_ENCRYPTION_INFO; import static org.apache.hadoop.util.Time.now; @@ -35,6 +36,7 @@ import com.google.protobuf.InvalidProtoc import org.apache.hadoop.HadoopIllegalArgumentException; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.crypto.key.KeyProvider; import org.apache.hadoop.fs.ContentSummary; import org.apache.hadoop.fs.FileAlreadyExistsException; import org.apache.hadoop.fs.FileEncryptionInfo; @@ -162,7 +164,7 @@ public class FSDirectory implements Clos } boolean hasReadLock() { - return this.dirLock.getReadHoldCount() > 0; + return this.dirLock.getReadHoldCount() > 0 || hasWriteLock(); } public int getReadHoldCount() { @@ -173,7 +175,8 @@ public class FSDirectory implements Clos return this.dirLock.getWriteHoldCount(); } - final EncryptionZoneManager ezManager; + @VisibleForTesting + public final EncryptionZoneManager ezManager; /** * Caches frequently used file names used in {@link INode} to reuse @@ -224,7 +227,7 @@ public class FSDirectory implements Clos nameCache = new NameCache<ByteArray>(threshold); namesystem = ns; - ezManager = new EncryptionZoneManager(this); + ezManager = new EncryptionZoneManager(this, conf, ns.getProvider()); } private FSNamesystem getFSNamesystem() { @@ -905,16 +908,6 @@ public class FSDirectory implements Clos } } - boolean isInAnEZ(INodesInPath iip) - throws UnresolvedLinkException, SnapshotAccessControlException { - readLock(); - try { - return ezManager.isInAnEZ(iip); - } finally { - readUnlock(); - } - } - /** * Set file replication * @@ -2618,12 +2611,46 @@ public class FSDirectory implements Clos return newXAttrs; } - - XAttr createEncryptionZone(String src, String keyId) + + boolean isInAnEZ(INodesInPath iip) + throws UnresolvedLinkException, SnapshotAccessControlException { + readLock(); + try { + return ezManager.isInAnEZ(iip); + } finally { + readUnlock(); + } + } + + KeyVersion getLatestKeyVersion(INodesInPath iip) { + readLock(); + try { + return ezManager.getLatestKeyVersion(iip); + } finally { + readUnlock(); + } + } + + KeyVersion updateLatestKeyVersion(INodesInPath iip) throws + IOException { + // No locking, this operation does not involve any FSDirectory operations + return ezManager.updateLatestKeyVersion(iip); + } + + boolean isValidKeyVersion(INodesInPath iip, String keyVersionName) { + readLock(); + try { + return ezManager.isValidKeyVersion(iip, keyVersionName); + } finally { + readUnlock(); + } + } + + XAttr createEncryptionZone(String src, String keyId, KeyVersion keyVersion) throws IOException { writeLock(); try { - return ezManager.createEncryptionZone(src, keyId); + return ezManager.createEncryptionZone(src, keyId, keyVersion); } finally { writeUnlock(); } Modified: hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/FSNamesystem.java URL: http://svn.apache.org/viewvc/hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/FSNamesystem.java?rev=1609833&r1=1609832&r2=1609833&view=diff ============================================================================== --- hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/FSNamesystem.java (original) +++ hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/FSNamesystem.java Fri Jul 11 20:54:47 2014 @@ -17,6 +17,7 @@ */ package org.apache.hadoop.hdfs.server.namenode; +import static org.apache.hadoop.crypto.key.KeyProvider.KeyVersion; import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.FS_TRASH_INTERVAL_DEFAULT; import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.FS_TRASH_INTERVAL_KEY; import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.IO_FILE_BUFFER_SIZE_DEFAULT; @@ -100,6 +101,7 @@ import java.io.StringWriter; import java.lang.management.ManagementFactory; import java.net.InetAddress; import java.net.URI; +import java.security.GeneralSecurityException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; @@ -133,6 +135,7 @@ import org.apache.hadoop.conf.Configurat import org.apache.hadoop.crypto.CipherSuite; import org.apache.hadoop.crypto.CryptoCodec; import org.apache.hadoop.crypto.key.KeyProvider; +import org.apache.hadoop.crypto.key.KeyProviderCryptoExtension; import org.apache.hadoop.crypto.key.KeyProviderFactory; import org.apache.hadoop.fs.BatchedRemoteIterator.BatchedListEntries; import org.apache.hadoop.fs.CacheFlag; @@ -533,7 +536,7 @@ public class FSNamesystem implements Nam private final NNConf nnConf; - private KeyProvider provider = null; + private KeyProviderCryptoExtension provider = null; private KeyProvider.Options providerOptions = null; private final CryptoCodec codec; @@ -929,7 +932,8 @@ public class FSNamesystem implements Nam LOG.error(err); throw new RuntimeException(err); } - provider = providers.get(0); + provider = KeyProviderCryptoExtension + .createKeyProviderCryptoExtension(providers.get(0)); if (provider.isTransient()) { final String err = "A KeyProvider was found but it is a transient provider."; @@ -2310,7 +2314,7 @@ public class FSNamesystem implements Nam * CipherSuite from the list provided by the client. Since the client may * be newer, need to handle unknown CipherSuites. * - * @param src path of the file + * @param srcIIP path of the file * @param cipherSuites client-provided list of supported CipherSuites, * in desired order. * @return chosen CipherSuite, or null if file is not in an EncryptionZone @@ -2350,6 +2354,62 @@ public class FSNamesystem implements Nam } /** + * Create a new FileEncryptionInfo for a path. Also chooses an + * appropriate CipherSuite to use from the list provided by the + * client. + * + * @param src Target path + * @param pathComponents Target path split up into path components + * @param cipherSuites List of CipherSuites provided by the client + * @return a new FileEncryptionInfo, or null if path is not within an + * encryption + * zone. + * @throws IOException + */ + private FileEncryptionInfo newFileEncryptionInfo(String src, + byte[][] pathComponents, List<CipherSuite> cipherSuites) + throws IOException { + INodesInPath iip = null; + CipherSuite suite = null; + KeyVersion latestEZKeyVersion = null; + readLock(); + try { + src = FSDirectory.resolvePath(src, pathComponents, dir); + iip = dir.getINodesInPath4Write(src); + // Nothing to do if the path is not within an EZ + if (!dir.isInAnEZ(iip)) { + return null; + } + suite = chooseCipherSuite(iip, cipherSuites); + if (suite != null) { + Preconditions.checkArgument(!suite.equals(CipherSuite.UNKNOWN), + "Chose an UNKNOWN CipherSuite!"); + } + latestEZKeyVersion = dir.getLatestKeyVersion(iip); + } finally { + readUnlock(); + } + + // If the latest key version is null, need to fetch it and update + if (latestEZKeyVersion == null) { + latestEZKeyVersion = dir.updateLatestKeyVersion(iip); + } + Preconditions.checkState(latestEZKeyVersion != null); + + // Generate the EDEK while not holding the lock + KeyProviderCryptoExtension.EncryptedKeyVersion edek = null; + try { + edek = provider.generateEncryptedKey(latestEZKeyVersion); + } catch (GeneralSecurityException e) { + throw new IOException(e); + } + Preconditions.checkNotNull(edek); + + return new FileEncryptionInfo(suite, edek.getEncryptedKey().getMaterial(), + edek.getIv(), edek.getKeyVersionName()); + } + + /** * Create a new file entry in the namespace. * * For description of parameters and exceptions thrown see @@ -2426,26 +2486,62 @@ public class FSNamesystem implements Nam boolean overwrite = flag.contains(CreateFlag.OVERWRITE); waitForLoadingFSImage(); - writeLock(); + + /* + * We want to avoid holding any locks while creating a new + * FileEncryptionInfo, since this can be very slow. Since the path can + * flip flop between being in an encryption zone and not in the meantime, + * we need to recheck the preconditions and generate a new + * FileEncryptionInfo in some circumstances. + * + * A special RetryStartFileException is used to indicate that we should + * retry creation of a FileEncryptionInfo. + */ try { - checkOperation(OperationCategory.WRITE); - checkNameNodeSafeMode("Cannot create file" + src); - src = FSDirectory.resolvePath(src, pathComponents, dir); - startFileInternal(pc, src, permissions, holder, clientMachine, create, - overwrite, createParent, replication, blockSize, cipherSuites, - logRetryCache); - stat = dir.getFileInfo(src, false); - } catch (StandbyException se) { - skipSync = true; - throw se; + boolean shouldContinue = true; + int iters = 0; + while (shouldContinue) { + skipSync = false; + if (iters >= 10) { + throw new IOException("Too many retries because of encryption zone " + + "operations, something might be broken!"); + } + shouldContinue = false; + iters++; + // Optimistically generate a FileEncryptionInfo for this path. + FileEncryptionInfo feInfo = + newFileEncryptionInfo(src, pathComponents, cipherSuites); + + // Try to create the file with this feInfo + writeLock(); + try { + checkOperation(OperationCategory.WRITE); + checkNameNodeSafeMode("Cannot create file" + src); + src = FSDirectory.resolvePath(src, pathComponents, dir); + startFileInternal(pc, src, permissions, holder, clientMachine, create, + overwrite, createParent, replication, blockSize, feInfo, + logRetryCache); + stat = dir.getFileInfo(src, false); + } catch (StandbyException se) { + skipSync = true; + throw se; + } catch (RetryStartFileException e) { + shouldContinue = true; + if (LOG.isTraceEnabled()) { + LOG.trace("Preconditions failed, retrying creation of " + + "FileEncryptionInfo", e); + } + } finally { + writeUnlock(); + } + } } finally { - writeUnlock(); // There might be transactions logged while trying to recover the lease. // They need to be sync'ed even when an exception was thrown. if (!skipSync) { getEditLog().logSync(); } - } + } logAuditEvent(true, "create", src, null, stat); return stat; @@ -2463,11 +2559,11 @@ public class FSNamesystem implements Nam private void startFileInternal(FSPermissionChecker pc, String src, PermissionStatus permissions, String holder, String clientMachine, boolean create, boolean overwrite, boolean createParent, - short replication, long blockSize, List<CipherSuite> cipherSuites, + short replication, long blockSize, FileEncryptionInfo feInfo, boolean logRetryEntry) throws FileAlreadyExistsException, AccessControlException, UnresolvedLinkException, FileNotFoundException, - ParentNotDirectoryException, IOException { + ParentNotDirectoryException, RetryStartFileException, IOException { assert hasWriteLock(); // Verify that the destination does not exist as a directory already. final INodesInPath iip = dir.getINodesInPath4Write(src); @@ -2477,22 +2573,21 @@ public class FSNamesystem implements Nam " already exists as a directory"); } - FileEncryptionInfo feInfo = null; - CipherSuite suite = chooseCipherSuite(iip, cipherSuites); - if (suite != null) { - Preconditions.checkArgument(!suite.equals(CipherSuite.UNKNOWN), - "Chose an UNKNOWN CipherSuite!"); - // TODO: fill in actual key/iv in HDFS-6474 - // For now, populate with dummy data - byte[] key = new byte[suite.getAlgorithmBlockSize()]; - for (int i = 0; i < key.length; i++) { - key[i] = (byte)i; - } - byte[] iv = new byte[suite.getAlgorithmBlockSize()]; - for (int i = 0; i < iv.length; i++) { - iv[i] = (byte)(3+i*2); + if (!dir.isInAnEZ(iip)) { + // If the path is not in an EZ, we don't need an feInfo. + // Null it out in case one was already generated. + feInfo = null; + } else { + // The path is now within an EZ, but no feInfo. Retry. + if (feInfo == null) { + throw new RetryStartFileException(); + } + // It's in an EZ and we have a provided feInfo. Make sure the + // keyVersion of the encryption key used matches one of the keyVersions of + // the key of the encryption zone. + if (!dir.isValidKeyVersion(iip, feInfo.getEzKeyVersionName())) { + throw new RetryStartFileException(); } - feInfo = new FileEncryptionInfo(suite, key, iv); } final INodeFile myFile = INodeFile.valueOf(inode, src, true); @@ -8319,12 +8414,14 @@ public class FSNamesystem implements Nam String keyId = keyIdArg; boolean success = false; try { + KeyVersion keyVersion; if (keyId == null || keyId.isEmpty()) { - keyId = createNewKey(src); + keyId = UUID.randomUUID().toString(); + keyVersion = createNewKey(keyId, src); createdKey = true; } else { - if (provider.getCurrentKey(keyId) == null) { - + keyVersion = provider.getCurrentKey(keyId); + if (keyVersion == null) { /* * It would be nice if we threw something more specific than * IOException when the key is not found, but the KeyProvider API @@ -8336,7 +8433,7 @@ public class FSNamesystem implements Nam throw new IOException("Key " + keyId + " doesn't exist."); } } - createEncryptionZoneInt(src, keyId, cacheEntry != null); + createEncryptionZoneInt(src, keyId, keyVersion, cacheEntry != null); success = true; } catch (AccessControlException e) { logAuditEvent(false, "createEncryptionZone", src); @@ -8351,7 +8448,8 @@ public class FSNamesystem implements Nam } private void createEncryptionZoneInt(final String srcArg, String keyId, - final boolean logRetryCache) throws IOException { + final KeyVersion keyVersion, final boolean logRetryCache) throws + IOException { String src = srcArg; HdfsFileStatus resultingStat = null; checkSuperuserPrivilege(); @@ -8365,7 +8463,7 @@ public class FSNamesystem implements Nam checkNameNodeSafeMode("Cannot create encryption zone on " + src); src = FSDirectory.resolvePath(src, pathComponents, dir); - final XAttr keyIdXAttr = dir.createEncryptionZone(src, keyId); + final XAttr keyIdXAttr = dir.createEncryptionZone(src, keyId, keyVersion); List<XAttr> xAttrs = Lists.newArrayListWithCapacity(1); xAttrs.add(keyIdXAttr); getEditLog().logSetXAttrs(src, xAttrs, logRetryCache); @@ -8377,19 +8475,29 @@ public class FSNamesystem implements Nam logAuditEvent(true, "createEncryptionZone", src, null, resultingStat); } - private String createNewKey(String src) + /** + * Create a new key on the KeyProvider for an encryption zone. + * + * @param keyId id of the key + * @param src path of the encryption zone. + * @return KeyVersion of the created key + * @throws IOException + */ + private KeyVersion createNewKey(String keyId, String src) throws IOException { - final String keyId = UUID.randomUUID().toString(); + Preconditions.checkNotNull(keyId); + Preconditions.checkNotNull(src); // TODO pass in hdfs://HOST:PORT (HDFS-6490) providerOptions.setDescription(src); providerOptions.setBitLength(codec.getCipherSuite() .getAlgorithmBlockSize()*8); + KeyVersion version = null; try { - provider.createKey(keyId, providerOptions); + version = provider.createKey(keyId, providerOptions); } catch (NoSuchAlgorithmException e) { throw new IOException(e); } - return keyId; + return version; } List<EncryptionZone> listEncryptionZones() throws IOException { Added: hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/RetryStartFileException.java URL: http://svn.apache.org/viewvc/hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/RetryStartFileException.java?rev=1609833&view=auto ============================================================================== --- hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/RetryStartFileException.java (added) +++ hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/RetryStartFileException.java Fri Jul 11 20:54:47 2014 @@ -0,0 +1,21 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hdfs.server.namenode; + +public class RetryStartFileException extends Exception { +} Propchange: hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/RetryStartFileException.java ------------------------------------------------------------------------------ svn:eol-style = native Modified: hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/proto/hdfs.proto URL: http://svn.apache.org/viewvc/hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/proto/hdfs.proto?rev=1609833&r1=1609832&r2=1609833&view=diff ============================================================================== --- hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/proto/hdfs.proto (original) +++ hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/proto/hdfs.proto Fri Jul 11 20:54:47 2014 @@ -184,6 +184,7 @@ message FileEncryptionInfoProto { required CipherSuite suite = 1; required bytes key = 2; required bytes iv = 3; + required string ezKeyVersionName = 4; } /** Modified: hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/resources/hdfs-default.xml URL: http://svn.apache.org/viewvc/hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/resources/hdfs-default.xml?rev=1609833&r1=1609832&r2=1609833&view=diff ============================================================================== --- hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/resources/hdfs-default.xml (original) +++ hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/main/resources/hdfs-default.xml Fri Jul 11 20:54:47 2014 @@ -2008,4 +2008,15 @@ </description> </property> +<property> + <name>dfs.namenode.key.version.refresh.interval.ms</name> + <value>300000</value> + <description>How frequently the namenode will attempt to fetch the latest + key version of encryption zone keys from the configured KeyProvider, in + milliseconds. New key versions are created when a key is rolled. This + setting thus controls the window of staleness where an old key version + is used after a key is rolled. + </description> +</property> + </configuration> Modified: hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/TestEncryptionZonesAPI.java URL: http://svn.apache.org/viewvc/hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/TestEncryptionZonesAPI.java?rev=1609833&r1=1609832&r2=1609833&view=diff ============================================================================== --- hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/TestEncryptionZonesAPI.java (original) +++ hadoop/common/branches/fs-encryption/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/TestEncryptionZonesAPI.java Fri Jul 11 20:54:47 2014 @@ -21,17 +21,20 @@ import java.io.File; import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.security.PrivilegedExceptionAction; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.UUID; +import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.crypto.CipherSuite; import org.apache.hadoop.crypto.key.JavaKeyStoreProvider; import org.apache.hadoop.crypto.key.KeyProvider; import org.apache.hadoop.crypto.key.KeyProviderFactory; +import org.apache.hadoop.fs.FSDataInputStream; import org.apache.hadoop.fs.FileEncryptionInfo; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; @@ -39,16 +42,20 @@ import org.apache.hadoop.fs.permission.F import org.apache.hadoop.hdfs.client.HdfsAdmin; import org.apache.hadoop.hdfs.protocol.EncryptionZone; import org.apache.hadoop.hdfs.protocol.LocatedBlocks; +import org.apache.hadoop.hdfs.server.namenode.EncryptionZoneManager; import org.apache.hadoop.security.AccessControlException; import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.test.GenericTestUtils; +import org.apache.log4j.Level; +import org.apache.log4j.Logger; import org.junit.After; import org.junit.Before; import org.junit.Test; -import com.google.common.base.Preconditions; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.fail; public class TestEncryptionZonesAPI { @@ -71,6 +78,7 @@ public class TestEncryptionZonesAPI { JavaKeyStoreProvider.SCHEME_NAME + "://file" + tmpDir + "/test.jks"); cluster = new MiniDFSCluster.Builder(conf).numDataNodes(1).build(); fs = (DistributedFileSystem) createFileSystem(conf); + Logger.getLogger(EncryptionZoneManager.class).setLevel(Level.TRACE); } protected FileSystem createFileSystem(Configuration conf) throws IOException { @@ -382,21 +390,80 @@ public class TestEncryptionZonesAPI { fs.getClient().cipherSuites.add(CipherSuite.AES_CTR_NOPADDING); DFSTestUtil.createFile(fs, new Path(zone, "success3"), 4096, (short) 1, 0xFEED); + // Check KeyProvider state + // Flushing the KP on the NN, since it caches, and init a test one + cluster.getNamesystem().getProvider().flush(); + KeyProvider provider = KeyProviderFactory.getProviders(conf).get(0); + List<String> keys = provider.getKeys(); + assertEquals("Expected NN to have created one key per zone", 1, + keys.size()); + List<KeyProvider.KeyVersion> allVersions = Lists.newArrayList(); + for (String key : keys) { + List<KeyProvider.KeyVersion> versions = provider.getKeyVersions(key); + assertEquals("Should only have one key version per key", 1, + versions.size()); + allVersions.addAll(versions); + } // Check that the specified CipherSuite was correctly saved on the NN for (int i=2; i<=3; i++) { - LocatedBlocks blocks = - fs.getClient().getLocatedBlocks(zone.toString() + "/success2", 0); - FileEncryptionInfo feInfo = blocks.getFileEncryptionInfo(); + FileEncryptionInfo feInfo = + getFileEncryptionInfo(new Path(zone.toString() + + "/success" + i)); assertEquals(feInfo.getCipherSuite(), CipherSuite.AES_CTR_NOPADDING); - // TODO: validate against actual key/iv in HDFS-6474 - byte[] key = feInfo.getEncryptedDataEncryptionKey(); - for (int j = 0; j < key.length; j++) { - assertEquals("Unexpected key byte", (byte)j, key[j]); - } - byte[] iv = feInfo.getIV(); - for (int j = 0; j < iv.length; j++) { - assertEquals("Unexpected IV byte", (byte)(3+j*2), iv[j]); - } } } + + private void validateFiles(Path p1, Path p2, int len) throws Exception { + FSDataInputStream in1 = fs.open(p1); + FSDataInputStream in2 = fs.open(p2); + for (int i=0; i<len; i++) { + assertEquals("Mismatch at byte " + i, in1.read(), in2.read()); + } + in1.close(); + in2.close(); + } + + private FileEncryptionInfo getFileEncryptionInfo(Path path) throws Exception { + LocatedBlocks blocks = fs.getClient().getLocatedBlocks(path.toString(), 0); + return blocks.getFileEncryptionInfo(); + } + + @Test(timeout = 120000) + public void testReadWrite() throws Exception { + final HdfsAdmin dfsAdmin = + new HdfsAdmin(FileSystem.getDefaultUri(conf), conf); + // Create a base file for comparison + final Path baseFile = new Path("/base"); + final int len = 8192; + DFSTestUtil.createFile(fs, baseFile, len, (short) 1, 0xFEED); + // Create the first enc file + final Path zone = new Path("/zone"); + fs.mkdirs(zone); + dfsAdmin.createEncryptionZone(zone, null); + final Path encFile1 = new Path(zone, "myfile"); + DFSTestUtil.createFile(fs, encFile1, len, (short) 1, 0xFEED); + // Read them back in and compare byte-by-byte + validateFiles(baseFile, encFile1, len); + // Roll the key of the encryption zone + List<EncryptionZone> zones = dfsAdmin.listEncryptionZones(); + assertEquals("Expected 1 EZ", 1, zones.size()); + String keyId = zones.get(0).getKeyId(); + cluster.getNamesystem().getProvider().rollNewVersion(keyId); + cluster.getNamesystem().getFSDirectory().ezManager.kickMonitor(); + // Read them back in and compare byte-by-byte + validateFiles(baseFile, encFile1, len); + // Write a new enc file and validate + final Path encFile2 = new Path(zone, "myfile2"); + DFSTestUtil.createFile(fs, encFile2, len, (short) 1, 0xFEED); + // FEInfos should be different + FileEncryptionInfo feInfo1 = getFileEncryptionInfo(encFile1); + FileEncryptionInfo feInfo2 = getFileEncryptionInfo(encFile2); + assertFalse("EDEKs should be different", Arrays.equals( + feInfo1.getEncryptedDataEncryptionKey(), + feInfo2.getEncryptedDataEncryptionKey())); + assertNotEquals("Key was rolled, versions should be different", + feInfo1.getEzKeyVersionName(), feInfo2.getEzKeyVersionName()); + // Contents still equal + validateFiles(encFile1, encFile2, len); + } }