/*
* Copyright (C) 2015 hops.io.
*
* Licensed 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 io.hops.metadata.security.token.block;
import io.hops.exception.StorageException;
import io.hops.metadata.HdfsVariables;
import io.hops.metadata.common.entity.Variable;
import io.hops.transaction.handler.HDFSOperationType;
import io.hops.transaction.handler.HopsTransactionalRequestHandler;
import io.hops.transaction.lock.LockFactory;
import io.hops.transaction.lock.TransactionLockTypes.LockType;
import io.hops.transaction.lock.TransactionLocks;
import org.apache.hadoop.hdfs.security.token.block.BlockKey;
import org.apache.hadoop.hdfs.security.token.block.BlockTokenIdentifier;
import org.apache.hadoop.hdfs.security.token.block.BlockTokenSecretManager;
import org.apache.hadoop.hdfs.security.token.block.DataEncryptionKey;
import org.apache.hadoop.hdfs.security.token.block.ExportedBlockKeys;
import org.apache.hadoop.hdfs.server.namenode.Namesystem;
import org.apache.hadoop.util.Time;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Collection;
import java.util.Map;
/**
* Persisted version of the BlockTokenSecretManager to be used by the NameNode
* We add persistence by overriding only the methods used by the NameNode
*/
public class NameNodeBlockTokenSecretManager extends BlockTokenSecretManager {
private Namesystem namesystem;
/**
* Constructor for masters.
*/
public NameNodeBlockTokenSecretManager(long keyUpdateInterval,
long tokenLifetime, String blockPoolId, String encryptionAlgorithm,
Namesystem namesystem) throws IOException {
super(true, keyUpdateInterval, tokenLifetime, blockPoolId,
encryptionAlgorithm);
this.namesystem = namesystem;
this.setSerialNo(new SecureRandom().nextInt());
if (isLeader()) {
// TODO[Hooman]: Since Master is keeping the serialNo locally, so whenever
// A namenode crashes it should remove all keys from the database.
this.generateKeys();
} else {
retrieveBlockKeys();
}
}
@Override
public void setSerialNo(int serialNo) {
this.serialNo = serialNo;
}
private void generateKeys() throws IOException {
if (!isMaster) {
return;
}
/*
* Need to set estimated expiry dates for currentKey and nextKey so that if
* NN crashes, DN can still expire those keys. NN will stop using the newly
* generated currentKey after the first keyUpdateInterval, however it may
* still be used by DN and Balancer to generate new tokens before they get a
* chance to sync their keys with NN. Since we require keyUpdInterval to be
* long enough so that all live DN's and Balancer will sync their keys with
* NN at least once during the period, the estimated expiry date for
* currentKey is set to now() + 2 * keyUpdateInterval + tokenLifetime.
* Similarly, the estimated expiry date for nextKey is one keyUpdateInterval
* more.
*/
setSerialNo(serialNo + 1);
currentKey = new BlockKey(serialNo,
Time.now() + 2 * keyUpdateInterval + tokenLifetime, generateSecret());
currentKey.setKeyType(BlockKey.KeyType.CurrKey);
setSerialNo(serialNo + 1);
nextKey = new BlockKey(serialNo,
Time.now() + 3 * keyUpdateInterval + tokenLifetime, generateSecret());
nextKey.setKeyType(BlockKey.KeyType.NextKey);
addBlockKeys();
}
@Override
public ExportedBlockKeys exportKeys() throws IOException {
if (!isMaster) {
return null;
}
if (LOG.isDebugEnabled()) {
LOG.debug("Exporting access keys");
}
BlockKey[] allKeys = getAllKeysAndSync();
return new ExportedBlockKeys(true, keyUpdateInterval, tokenLifetime,
currentKey, allKeys);
}
@Override
public boolean updateKeys(final long updateTime) throws IOException {
if (updateTime > keyUpdateInterval) {
return updateKeys();
}
return false;
}
@Override
public boolean updateKeys() throws IOException {
if (!isMaster) {
return false;
}
if (isLeader()) {
LOG.info("Updating block keys");
return updateBlockKeys();
} else {
retrieveBlockKeys();
}
return true;
}
@Override
public DataEncryptionKey generateDataEncryptionKey() throws IOException {
byte[] nonce = new byte[8];
nonceGenerator.nextBytes(nonce);
BlockKey key = getBlockKeyByType(BlockKey.KeyType.CurrKey);
byte[] encryptionKey = createPassword(nonce, key.getKey());
return new DataEncryptionKey(key.getKeyId(), blockPoolId, nonce,
encryptionKey, Time.now() + tokenLifetime, encryptionAlgorithm);
}
@Override
protected byte[] createPassword(BlockTokenIdentifier identifier) {
BlockKey key;
try {
key = getBlockKeyByType(BlockKey.KeyType.CurrKey);
} catch (IOException ex) {
throw new IllegalStateException(
"currentKey hasn't been initialized. [" + ex.getMessage() + "]");
}
if (key == null) {
throw new IllegalStateException("currentKey hasn't been initialized.");
}
identifier.setExpiryDate(Time.now() + tokenLifetime);
identifier.setKeyId(key.getKeyId());
if (LOG.isDebugEnabled()) {
LOG.debug("Generating block token for " + identifier.toString());
}
return createPassword(identifier.getBytes(), key.getKey());
}
@Override
public byte[] retrievePassword(BlockTokenIdentifier identifier)
throws InvalidToken {
if (isExpired(identifier.getExpiryDate())) {
throw new InvalidToken(
"Block token with " + identifier.toString() + " is expired.");
}
BlockKey key = null;
try {
key = getBlockKeyById(identifier.getKeyId());
} catch (IOException ex) {
}
if (key == null) {
throw new InvalidToken(
"Can't re-compute password for " + identifier.toString() +
", since the required block key (keyID=" + identifier.getKeyId() +
") doesn't exist.");
}
return createPassword(identifier.getBytes(), key.getKey());
}
public void generateKeysIfNeeded() throws IOException {
if (isLeader()) {
retrieveBlockKeys();
if (currentKey == null && nextKey == null) {
generateKeys();
}
}
}
private void retrieveBlockKeys() throws IOException {
currentKey = getBlockKeyByType(BlockKey.KeyType.CurrKey);
nextKey = getBlockKeyByType(BlockKey.KeyType.NextKey);
}
private void addBlockKeys() throws IOException {
new HopsTransactionalRequestHandler(HDFSOperationType.ADD_BLOCK_TOKENS) {
@Override
public void acquireLock(TransactionLocks locks) throws IOException {
LockFactory lf = LockFactory.getInstance();
locks.add(
lf.getVariableLock(Variable.Finder.BlockTokenKeys, LockType.WRITE));
}
@Override
public Object performTask() throws StorageException, IOException {
HdfsVariables.updateBlockTokenKeys(currentKey, nextKey);
return null;
}
}.handle();
}
private BlockKey getBlockKeyById(int keyId) throws IOException {
return HdfsVariables.getAllBlockTokenKeysByIDLW().get(keyId);
}
private BlockKey getBlockKeyByType(BlockKey.KeyType keytype)
throws IOException {
return HdfsVariables.getAllBlockTokenKeysByTypeLW().get(keytype.ordinal());
}
private BlockKey[] getAllKeysAndSync() throws IOException {
BlockKey[] allKeysArr = null;
Collection<BlockKey> allKeys = getAllKeys();
if (allKeys != null) {
for (BlockKey key : allKeys) {
if (key.isCurrKey()) {
currentKey = key;
} else if (key.isNextKey()) {
nextKey = key;
}
}
allKeysArr = allKeys.toArray(new BlockKey[allKeys.size()]);
}
return allKeysArr;
}
private Collection<BlockKey> getAllKeys() throws IOException {
return HdfsVariables.getAllBlockTokenKeysByIDLW().values();
}
private boolean updateBlockKeys() throws IOException {
return (Boolean) new HopsTransactionalRequestHandler(
HDFSOperationType.UPDATE_BLOCK_KEYS) {
@Override
public void acquireLock(TransactionLocks locks) throws IOException {
LockFactory lf = LockFactory.getInstance();
locks.add(
lf.getVariableLock(Variable.Finder.BlockTokenKeys, LockType.WRITE));
}
@Override
public Object performTask() throws StorageException, IOException {
Map<Integer, BlockKey> keys =
HdfsVariables.getAllBlockTokenKeysByType();
if (keys.isEmpty()) {
if(LOG.isDebugEnabled()) {
LOG.debug("keys is not generated yet to be updated");
}
return false;
}
// set final expiry date of retiring currentKey
// also modifying this key to mark it as 'simple key' instead of 'current key'
BlockKey currentKeyFromDB =
keys.get(BlockKey.KeyType.CurrKey.ordinal());
currentKeyFromDB
.setExpiryDate(Time.now() + keyUpdateInterval + tokenLifetime);
currentKeyFromDB.setKeyType(BlockKey.KeyType.SimpleKey);
// after above update, we only have a key marked as 'next key'
// the 'next key' becomes the 'current key'
// update the estimated expiry date of new currentKey
BlockKey nextKeyFromDB = keys.get(BlockKey.KeyType.NextKey.ordinal());
currentKey = new BlockKey(nextKeyFromDB.getKeyId(),
Time.now() + 2 * keyUpdateInterval + tokenLifetime,
nextKeyFromDB.getKey());
currentKey.setKeyType(BlockKey.KeyType.CurrKey);
// generate a new nextKey
setSerialNo(serialNo + 1);
nextKey = new BlockKey(serialNo,
Time.now() + 3 * keyUpdateInterval + tokenLifetime,
generateSecret());
nextKey.setKeyType(BlockKey.KeyType.NextKey);
HdfsVariables
.updateBlockTokenKeys(currentKey, nextKey, currentKeyFromDB);
return true;
}
}.handle();
}
private boolean isLeader() {
if (namesystem != null) {
return namesystem.isLeader();
}
return false;
}
}