001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.apache.hadoop.hdfs.security.token.block;
020
021import java.io.ByteArrayInputStream;
022import java.io.DataInputStream;
023import java.io.IOException;
024import java.security.SecureRandom;
025import java.util.Arrays;
026import java.util.EnumSet;
027import java.util.HashMap;
028import java.util.Iterator;
029import java.util.Map;
030
031import org.apache.commons.logging.Log;
032import org.apache.commons.logging.LogFactory;
033import org.apache.hadoop.classification.InterfaceAudience;
034import org.apache.hadoop.hdfs.protocol.ExtendedBlock;
035import org.apache.hadoop.hdfs.protocol.datatransfer.InvalidEncryptionKeyException;
036import org.apache.hadoop.io.WritableUtils;
037import org.apache.hadoop.security.UserGroupInformation;
038import org.apache.hadoop.security.token.SecretManager;
039import org.apache.hadoop.security.token.Token;
040import org.apache.hadoop.util.Time;
041
042import com.google.common.annotations.VisibleForTesting;
043import com.google.common.base.Preconditions;
044import org.apache.hadoop.util.Timer;
045
046/**
047 * BlockTokenSecretManager can be instantiated in 2 modes, master mode and slave
048 * mode. Master can generate new block keys and export block keys to slaves,
049 * while slaves can only import and use block keys received from master. Both
050 * master and slave can generate and verify block tokens. Typically, master mode
051 * is used by NN and slave mode is used by DN.
052 */
053@InterfaceAudience.Private
054public class BlockTokenSecretManager extends
055    SecretManager<BlockTokenIdentifier> {
056  public static final Log LOG = LogFactory
057      .getLog(BlockTokenSecretManager.class);
058  
059  // We use these in an HA setup to ensure that the pair of NNs produce block
060  // token serial numbers that are in different ranges.
061  private static final int LOW_MASK  = ~(1 << 31);
062  
063  public static final Token<BlockTokenIdentifier> DUMMY_TOKEN = new Token<BlockTokenIdentifier>();
064
065  private final boolean isMaster;
066  private int nnIndex;
067  
068  /**
069   * keyUpdateInterval is the interval that NN updates its block keys. It should
070   * be set long enough so that all live DN's and Balancer should have sync'ed
071   * their block keys with NN at least once during each interval.
072   */
073  private long keyUpdateInterval;
074  private volatile long tokenLifetime;
075  private int serialNo;
076  private BlockKey currentKey;
077  private BlockKey nextKey;
078  private final Map<Integer, BlockKey> allKeys;
079  private String blockPoolId;
080  private final String encryptionAlgorithm;
081  
082  private final SecureRandom nonceGenerator = new SecureRandom();
083
084  public static enum AccessMode {
085    READ, WRITE, COPY, REPLACE
086  }
087  
088  /**
089   * Timer object for querying the current time. Separated out for
090   * unit testing.
091   */
092  private Timer timer;
093  /**
094   * Constructor for slaves.
095   * 
096   * @param keyUpdateInterval how often a new key will be generated
097   * @param tokenLifetime how long an individual token is valid
098   */
099  public BlockTokenSecretManager(long keyUpdateInterval,
100      long tokenLifetime, String blockPoolId, String encryptionAlgorithm) {
101    this(false, keyUpdateInterval, tokenLifetime, blockPoolId,
102        encryptionAlgorithm);
103  }
104  
105  /**
106   * Constructor for masters.
107   * 
108   * @param keyUpdateInterval how often a new key will be generated
109   * @param tokenLifetime how long an individual token is valid
110   * @param nnIndex namenode index
111   * @param blockPoolId block pool ID
112   * @param encryptionAlgorithm encryption algorithm to use
113   */
114  public BlockTokenSecretManager(long keyUpdateInterval,
115      long tokenLifetime, int nnIndex, String blockPoolId,
116      String encryptionAlgorithm) {
117    this(true, keyUpdateInterval, tokenLifetime, blockPoolId,
118        encryptionAlgorithm);
119    Preconditions.checkArgument(nnIndex == 0 || nnIndex == 1);
120    this.nnIndex = nnIndex;
121    setSerialNo(new SecureRandom().nextInt());
122    generateKeys();
123  }
124  
125  private BlockTokenSecretManager(boolean isMaster, long keyUpdateInterval,
126      long tokenLifetime, String blockPoolId, String encryptionAlgorithm) {
127    this.isMaster = isMaster;
128    this.keyUpdateInterval = keyUpdateInterval;
129    this.tokenLifetime = tokenLifetime;
130    this.allKeys = new HashMap<Integer, BlockKey>();
131    this.blockPoolId = blockPoolId;
132    this.encryptionAlgorithm = encryptionAlgorithm;
133    this.timer = new Timer();
134    generateKeys();
135  }
136  
137  @VisibleForTesting
138  public synchronized void setSerialNo(int serialNo) {
139    this.serialNo = (serialNo & LOW_MASK) | (nnIndex << 31);
140  }
141  
142  public void setBlockPoolId(String blockPoolId) {
143    this.blockPoolId = blockPoolId;
144  }
145
146  /** Initialize block keys */
147  private synchronized void generateKeys() {
148    if (!isMaster)
149      return;
150    /*
151     * Need to set estimated expiry dates for currentKey and nextKey so that if
152     * NN crashes, DN can still expire those keys. NN will stop using the newly
153     * generated currentKey after the first keyUpdateInterval, however it may
154     * still be used by DN and Balancer to generate new tokens before they get a
155     * chance to sync their keys with NN. Since we require keyUpdInterval to be
156     * long enough so that all live DN's and Balancer will sync their keys with
157     * NN at least once during the period, the estimated expiry date for
158     * currentKey is set to now() + 2 * keyUpdateInterval + tokenLifetime.
159     * Similarly, the estimated expiry date for nextKey is one keyUpdateInterval
160     * more.
161     */
162    setSerialNo(serialNo + 1);
163    currentKey = new BlockKey(serialNo, timer.now() + 2
164        * keyUpdateInterval + tokenLifetime, generateSecret());
165    setSerialNo(serialNo + 1);
166    nextKey = new BlockKey(serialNo, timer.now() + 3
167        * keyUpdateInterval + tokenLifetime, generateSecret());
168    allKeys.put(currentKey.getKeyId(), currentKey);
169    allKeys.put(nextKey.getKeyId(), nextKey);
170  }
171
172  /** Export block keys, only to be used in master mode */
173  public synchronized ExportedBlockKeys exportKeys() {
174    if (!isMaster)
175      return null;
176    if (LOG.isDebugEnabled())
177      LOG.debug("Exporting access keys");
178    return new ExportedBlockKeys(true, keyUpdateInterval, tokenLifetime,
179        currentKey, allKeys.values().toArray(new BlockKey[0]));
180  }
181
182  private synchronized void removeExpiredKeys() {
183    long now = timer.now();
184    for (Iterator<Map.Entry<Integer, BlockKey>> it = allKeys.entrySet()
185        .iterator(); it.hasNext();) {
186      Map.Entry<Integer, BlockKey> e = it.next();
187      if (e.getValue().getExpiryDate() < now) {
188        it.remove();
189      }
190    }
191  }
192
193  /**
194   * Set block keys, only to be used in slave mode
195   */
196  public synchronized void addKeys(ExportedBlockKeys exportedKeys)
197      throws IOException {
198    if (isMaster || exportedKeys == null)
199      return;
200    LOG.info("Setting block keys");
201    removeExpiredKeys();
202    this.currentKey = exportedKeys.getCurrentKey();
203    BlockKey[] receivedKeys = exportedKeys.getAllKeys();
204    for (int i = 0; i < receivedKeys.length; i++) {
205      if (receivedKeys[i] == null)
206        continue;
207      this.allKeys.put(receivedKeys[i].getKeyId(), receivedKeys[i]);
208    }
209  }
210
211  /**
212   * Update block keys if update time > update interval.
213   * @return true if the keys are updated.
214   */
215  public synchronized boolean updateKeys(final long updateTime) throws IOException {
216    if (updateTime > keyUpdateInterval) {
217      return updateKeys();
218    }
219    return false;
220  }
221
222  /**
223   * Update block keys, only to be used in master mode
224   */
225  synchronized boolean updateKeys() throws IOException {
226    if (!isMaster)
227      return false;
228
229    LOG.info("Updating block keys");
230    removeExpiredKeys();
231    // set final expiry date of retiring currentKey
232    allKeys.put(currentKey.getKeyId(), new BlockKey(currentKey.getKeyId(),
233        timer.now() + keyUpdateInterval + tokenLifetime,
234        currentKey.getKey()));
235    // update the estimated expiry date of new currentKey
236    currentKey = new BlockKey(nextKey.getKeyId(), timer.now()
237        + 2 * keyUpdateInterval + tokenLifetime, nextKey.getKey());
238    allKeys.put(currentKey.getKeyId(), currentKey);
239    // generate a new nextKey
240    setSerialNo(serialNo + 1);
241    nextKey = new BlockKey(serialNo, timer.now() + 3
242        * keyUpdateInterval + tokenLifetime, generateSecret());
243    allKeys.put(nextKey.getKeyId(), nextKey);
244    return true;
245  }
246
247  /** Generate an block token for current user */
248  public Token<BlockTokenIdentifier> generateToken(ExtendedBlock block,
249      EnumSet<AccessMode> modes) throws IOException {
250    UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
251    String userID = (ugi == null ? null : ugi.getShortUserName());
252    return generateToken(userID, block, modes);
253  }
254
255  /** Generate a block token for a specified user */
256  public Token<BlockTokenIdentifier> generateToken(String userId,
257      ExtendedBlock block, EnumSet<AccessMode> modes) throws IOException {
258    BlockTokenIdentifier id = new BlockTokenIdentifier(userId, block
259        .getBlockPoolId(), block.getBlockId(), modes);
260    return new Token<BlockTokenIdentifier>(id, this);
261  }
262
263  /**
264   * Check if access should be allowed. userID is not checked if null. This
265   * method doesn't check if token password is correct. It should be used only
266   * when token password has already been verified (e.g., in the RPC layer).
267   */
268  public void checkAccess(BlockTokenIdentifier id, String userId,
269      ExtendedBlock block, AccessMode mode) throws InvalidToken {
270    if (LOG.isDebugEnabled()) {
271      LOG.debug("Checking access for user=" + userId + ", block=" + block
272          + ", access mode=" + mode + " using " + id.toString());
273    }
274    if (userId != null && !userId.equals(id.getUserId())) {
275      throw new InvalidToken("Block token with " + id.toString()
276          + " doesn't belong to user " + userId);
277    }
278    if (!id.getBlockPoolId().equals(block.getBlockPoolId())) {
279      throw new InvalidToken("Block token with " + id.toString()
280          + " doesn't apply to block " + block);
281    }
282    if (id.getBlockId() != block.getBlockId()) {
283      throw new InvalidToken("Block token with " + id.toString()
284          + " doesn't apply to block " + block);
285    }
286    if (isExpired(id.getExpiryDate())) {
287      throw new InvalidToken("Block token with " + id.toString()
288          + " is expired.");
289    }
290    if (!id.getAccessModes().contains(mode)) {
291      throw new InvalidToken("Block token with " + id.toString()
292          + " doesn't have " + mode + " permission");
293    }
294  }
295
296  /** Check if access should be allowed. userID is not checked if null */
297  public void checkAccess(Token<BlockTokenIdentifier> token, String userId,
298      ExtendedBlock block, AccessMode mode) throws InvalidToken {
299    BlockTokenIdentifier id = new BlockTokenIdentifier();
300    try {
301      id.readFields(new DataInputStream(new ByteArrayInputStream(token
302          .getIdentifier())));
303    } catch (IOException e) {
304      throw new InvalidToken(
305          "Unable to de-serialize block token identifier for user=" + userId
306              + ", block=" + block + ", access mode=" + mode);
307    }
308    checkAccess(id, userId, block, mode);
309    if (!Arrays.equals(retrievePassword(id), token.getPassword())) {
310      throw new InvalidToken("Block token with " + id.toString()
311          + " doesn't have the correct token password");
312    }
313  }
314
315  private static boolean isExpired(long expiryDate) {
316    return Time.now() > expiryDate;
317  }
318
319  /**
320   * check if a token is expired. for unit test only. return true when token is
321   * expired, false otherwise
322   */
323  static boolean isTokenExpired(Token<BlockTokenIdentifier> token)
324      throws IOException {
325    ByteArrayInputStream buf = new ByteArrayInputStream(token.getIdentifier());
326    DataInputStream in = new DataInputStream(buf);
327    long expiryDate = WritableUtils.readVLong(in);
328    return isExpired(expiryDate);
329  }
330
331  /** set token lifetime. */
332  public void setTokenLifetime(long tokenLifetime) {
333    this.tokenLifetime = tokenLifetime;
334  }
335
336  /**
337   * Create an empty block token identifier
338   * 
339   * @return a newly created empty block token identifier
340   */
341  @Override
342  public BlockTokenIdentifier createIdentifier() {
343    return new BlockTokenIdentifier();
344  }
345
346  /**
347   * Create a new password/secret for the given block token identifier.
348   * 
349   * @param identifier
350   *          the block token identifier
351   * @return token password/secret
352   */
353  @Override
354  protected byte[] createPassword(BlockTokenIdentifier identifier) {
355    BlockKey key = null;
356    synchronized (this) {
357      key = currentKey;
358    }
359    if (key == null)
360      throw new IllegalStateException("currentKey hasn't been initialized.");
361    identifier.setExpiryDate(timer.now() + tokenLifetime);
362    identifier.setKeyId(key.getKeyId());
363    if (LOG.isDebugEnabled()) {
364      LOG.debug("Generating block token for " + identifier.toString());
365    }
366    return createPassword(identifier.getBytes(), key.getKey());
367  }
368
369  /**
370   * Look up the token password/secret for the given block token identifier.
371   * 
372   * @param identifier
373   *          the block token identifier to look up
374   * @return token password/secret as byte[]
375   * @throws InvalidToken
376   */
377  @Override
378  public byte[] retrievePassword(BlockTokenIdentifier identifier)
379      throws InvalidToken {
380    if (isExpired(identifier.getExpiryDate())) {
381      throw new InvalidToken("Block token with " + identifier.toString()
382          + " is expired.");
383    }
384    BlockKey key = null;
385    synchronized (this) {
386      key = allKeys.get(identifier.getKeyId());
387    }
388    if (key == null) {
389      throw new InvalidToken("Can't re-compute password for "
390          + identifier.toString() + ", since the required block key (keyID="
391          + identifier.getKeyId() + ") doesn't exist.");
392    }
393    return createPassword(identifier.getBytes(), key.getKey());
394  }
395  
396  /**
397   * Generate a data encryption key for this block pool, using the current
398   * BlockKey.
399   * 
400   * @return a data encryption key which may be used to encrypt traffic
401   *         over the DataTransferProtocol
402   */
403  public DataEncryptionKey generateDataEncryptionKey() {
404    byte[] nonce = new byte[8];
405    nonceGenerator.nextBytes(nonce);
406    BlockKey key = null;
407    synchronized (this) {
408      key = currentKey;
409    }
410    byte[] encryptionKey = createPassword(nonce, key.getKey());
411    return new DataEncryptionKey(key.getKeyId(), blockPoolId, nonce,
412        encryptionKey, timer.now() + tokenLifetime,
413        encryptionAlgorithm);
414  }
415  
416  /**
417   * Recreate an encryption key based on the given key id and nonce.
418   * 
419   * @param keyId identifier of the secret key used to generate the encryption key.
420   * @param nonce random value used to create the encryption key
421   * @return the encryption key which corresponds to this (keyId, blockPoolId, nonce)
422   * @throws InvalidEncryptionKeyException
423   */
424  public byte[] retrieveDataEncryptionKey(int keyId, byte[] nonce)
425      throws InvalidEncryptionKeyException {
426    BlockKey key = null;
427    synchronized (this) {
428      key = allKeys.get(keyId);
429      if (key == null) {
430        throw new InvalidEncryptionKeyException("Can't re-compute encryption key"
431            + " for nonce, since the required block key (keyID=" + keyId
432            + ") doesn't exist. Current key: " + currentKey.getKeyId());
433      }
434    }
435    return createPassword(nonce, key.getKey());
436  }
437  
438  @VisibleForTesting
439  public synchronized void setKeyUpdateIntervalForTesting(long millis) {
440    this.keyUpdateInterval = millis;
441  }
442
443  @VisibleForTesting
444  public void clearAllKeysForTesting() {
445    allKeys.clear();
446  }
447  
448  @VisibleForTesting
449  public synchronized boolean hasKey(int keyId) {
450    BlockKey key = allKeys.get(keyId);
451    return key != null;
452  }
453
454  @VisibleForTesting
455  public synchronized int getSerialNoForTesting() {
456    return serialNo;
457  }
458  
459}