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}