org.apache.hadoop.hdfs.protocol.datatransfer.sasl.SaslDataTransferServer.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.hadoop.hdfs.protocol.datatransfer.sasl.SaslDataTransferServer.java

Source

/**
 * 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.protocol.datatransfer.sasl;

import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_DATA_TRANSFER_PROTECTION_KEY;
import static org.apache.hadoop.hdfs.protocol.datatransfer.sasl.DataTransferSaslUtil.*;

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.sasl.AuthorizeCallback;
import javax.security.sasl.RealmCallback;
import javax.security.sasl.SaslException;

import org.apache.commons.codec.binary.Base64;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.crypto.CipherOption;
import org.apache.hadoop.hdfs.net.Peer;
import org.apache.hadoop.hdfs.protocol.DatanodeID;
import org.apache.hadoop.hdfs.protocol.datatransfer.IOStreamPair;
import org.apache.hadoop.hdfs.protocol.datatransfer.InvalidEncryptionKeyException;
import org.apache.hadoop.hdfs.protocol.proto.DataTransferProtos.DataTransferEncryptorMessageProto.DataTransferEncryptorStatus;
import org.apache.hadoop.hdfs.security.token.block.BlockPoolTokenSecretManager;
import org.apache.hadoop.hdfs.security.token.block.BlockTokenIdentifier;
import org.apache.hadoop.hdfs.server.datanode.DNConf;
import org.apache.hadoop.security.SaslPropertiesResolver;
import org.apache.hadoop.security.SecurityUtil;
import org.apache.hadoop.security.UserGroupInformation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Charsets;
import com.google.common.collect.Lists;

/**
 * Negotiates SASL for DataTransferProtocol on behalf of a server.  There are
 * two possible supported variants of SASL negotiation: either a general-purpose
 * negotiation supporting any quality of protection, or a specialized
 * negotiation that enforces privacy as the quality of protection using a
 * cryptographically strong encryption key.
 *
 * This class is used in the DataNode for handling inbound connections.
 */
@InterfaceAudience.Private
public class SaslDataTransferServer {

    private static final Logger LOG = LoggerFactory.getLogger(SaslDataTransferServer.class);

    private final BlockPoolTokenSecretManager blockPoolTokenSecretManager;
    private final DNConf dnConf;

    /**
     * Creates a new SaslDataTransferServer.
     *
     * @param dnConf configuration of DataNode
     * @param blockPoolTokenSecretManager used for checking block access tokens
     *   and encryption keys
     */
    public SaslDataTransferServer(DNConf dnConf, BlockPoolTokenSecretManager blockPoolTokenSecretManager) {
        this.blockPoolTokenSecretManager = blockPoolTokenSecretManager;
        this.dnConf = dnConf;
    }

    /**
     * Receives SASL negotiation from a peer on behalf of a server.
     *
     * @param peer connection peer
     * @param underlyingOut connection output stream
     * @param underlyingIn connection input stream
     * @param int xferPort data transfer port of DataNode accepting connection
     * @param datanodeId ID of DataNode accepting connection
     * @return new pair of streams, wrapped after SASL negotiation
     * @throws IOException for any error
     */
    public IOStreamPair receive(Peer peer, OutputStream underlyingOut, InputStream underlyingIn, int xferPort,
            DatanodeID datanodeId) throws IOException {
        if (dnConf.getEncryptDataTransfer()) {
            LOG.debug("SASL server doing encrypted handshake for peer = {}, datanodeId = {}", peer, datanodeId);
            return getEncryptedStreams(peer, underlyingOut, underlyingIn);
        } else if (!UserGroupInformation.isSecurityEnabled()) {
            LOG.debug(
                    "SASL server skipping handshake in unsecured configuration for " + "peer = {}, datanodeId = {}",
                    peer, datanodeId);
            return new IOStreamPair(underlyingIn, underlyingOut);
        } else if (SecurityUtil.isPrivilegedPort(xferPort)) {
            LOG.debug("SASL server skipping handshake in secured configuration for " + "peer = {}, datanodeId = {}",
                    peer, datanodeId);
            return new IOStreamPair(underlyingIn, underlyingOut);
        } else if (dnConf.getSaslPropsResolver() != null) {
            LOG.debug("SASL server doing general handshake for peer = {}, datanodeId = {}", peer, datanodeId);
            return getSaslStreams(peer, underlyingOut, underlyingIn);
        } else if (dnConf.getIgnoreSecurePortsForTesting()) {
            // It's a secured cluster using non-privileged ports, but no SASL.  The
            // only way this can happen is if the DataNode has
            // ignore.secure.ports.for.testing configured, so this is a rare edge case.
            LOG.debug("SASL server skipping handshake in secured configuration with no SASL "
                    + "protection configured for peer = {}, datanodeId = {}", peer, datanodeId);
            return new IOStreamPair(underlyingIn, underlyingOut);
        } else {
            // The error message here intentionally does not mention
            // ignore.secure.ports.for.testing.  That's intended for dev use only.
            // This code path is not expected to execute ever, because DataNode startup
            // checks for invalid configuration and aborts.
            throw new IOException(String.format(
                    "Cannot create a secured " + "connection if DataNode listens on unprivileged port (%d) and no "
                            + "protection is defined in configuration property %s.",
                    datanodeId.getXferPort(), DFS_DATA_TRANSFER_PROTECTION_KEY));
        }
    }

    /**
     * Receives SASL negotiation for specialized encrypted handshake.
     *
     * @param peer connection peer
     * @param underlyingOut connection output stream
     * @param underlyingIn connection input stream
     * @return new pair of streams, wrapped after SASL negotiation
     * @throws IOException for any error
     */
    private IOStreamPair getEncryptedStreams(Peer peer, OutputStream underlyingOut, InputStream underlyingIn)
            throws IOException {
        if (peer.hasSecureChannel() || dnConf.getTrustedChannelResolver().isTrusted(getPeerAddress(peer))) {
            return new IOStreamPair(underlyingIn, underlyingOut);
        }

        Map<String, String> saslProps = createSaslPropertiesForEncryption(dnConf.getEncryptionAlgorithm());

        if (LOG.isDebugEnabled()) {
            LOG.debug("Server using encryption algorithm " + dnConf.getEncryptionAlgorithm());
        }

        CallbackHandler callbackHandler = new SaslServerCallbackHandler(new PasswordFunction() {
            @Override
            public char[] apply(String userName) throws IOException {
                return encryptionKeyToPassword(getEncryptionKeyFromUserName(userName));
            }
        });
        return doSaslHandshake(underlyingOut, underlyingIn, saslProps, callbackHandler);
    }

    /**
     * The SASL handshake for encrypted vs. general-purpose uses different logic
     * for determining the password.  This interface is used to parameterize that
     * logic.  It's similar to a Guava Function, but we need to let it throw
     * exceptions.
     */
    private interface PasswordFunction {

        /**
         * Returns the SASL password for the given user name.
         *
         * @param userName SASL user name
         * @return SASL password
         * @throws IOException for any error
         */
        char[] apply(String userName) throws IOException;
    }

    /**
     * Sets user name and password when asked by the server-side SASL object.
     */
    private static final class SaslServerCallbackHandler implements CallbackHandler {

        private final PasswordFunction passwordFunction;

        /**
         * Creates a new SaslServerCallbackHandler.
         *
         * @param passwordFunction for determing the user's password
         */
        public SaslServerCallbackHandler(PasswordFunction passwordFunction) {
            this.passwordFunction = passwordFunction;
        }

        @Override
        public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
            NameCallback nc = null;
            PasswordCallback pc = null;
            AuthorizeCallback ac = null;
            for (Callback callback : callbacks) {
                if (callback instanceof AuthorizeCallback) {
                    ac = (AuthorizeCallback) callback;
                } else if (callback instanceof PasswordCallback) {
                    pc = (PasswordCallback) callback;
                } else if (callback instanceof NameCallback) {
                    nc = (NameCallback) callback;
                } else if (callback instanceof RealmCallback) {
                    continue; // realm is ignored
                } else {
                    throw new UnsupportedCallbackException(callback,
                            "Unrecognized SASL DIGEST-MD5 Callback: " + callback);
                }
            }

            if (pc != null) {
                pc.setPassword(passwordFunction.apply(nc.getDefaultName()));
            }

            if (ac != null) {
                ac.setAuthorized(true);
                ac.setAuthorizedID(ac.getAuthorizationID());
            }
        }
    }

    /**
     * Given a secret manager and a username encoded for the encrypted handshake,
     * determine the encryption key.
     * 
     * @param userName containing the keyId, blockPoolId, and nonce.
     * @return secret encryption key.
     * @throws IOException
     */
    private byte[] getEncryptionKeyFromUserName(String userName) throws IOException {
        String[] nameComponents = userName.split(NAME_DELIMITER);
        if (nameComponents.length != 3) {
            throw new IOException("Provided name '" + userName + "' has " + nameComponents.length
                    + " components instead of the expected 3.");
        }
        int keyId = Integer.parseInt(nameComponents[0]);
        String blockPoolId = nameComponents[1];
        byte[] nonce = Base64.decodeBase64(nameComponents[2]);
        return blockPoolTokenSecretManager.retrieveDataEncryptionKey(keyId, blockPoolId, nonce);
    }

    /**
     * Receives SASL negotiation for general-purpose handshake.
     *
     * @param peer connection peer
     * @param underlyingOut connection output stream
     * @param underlyingIn connection input stream
     * @return new pair of streams, wrapped after SASL negotiation
     * @throws IOException for any error
     */
    private IOStreamPair getSaslStreams(Peer peer, OutputStream underlyingOut, InputStream underlyingIn)
            throws IOException {
        if (peer.hasSecureChannel() || dnConf.getTrustedChannelResolver().isTrusted(getPeerAddress(peer))) {
            return new IOStreamPair(underlyingIn, underlyingOut);
        }

        SaslPropertiesResolver saslPropsResolver = dnConf.getSaslPropsResolver();
        Map<String, String> saslProps = saslPropsResolver.getServerProperties(getPeerAddress(peer));

        CallbackHandler callbackHandler = new SaslServerCallbackHandler(new PasswordFunction() {
            @Override
            public char[] apply(String userName) throws IOException {
                return buildServerPassword(userName);
            }
        });
        return doSaslHandshake(underlyingOut, underlyingIn, saslProps, callbackHandler);
    }

    /**
     * Calculates the expected correct password on the server side for the
     * general-purpose handshake.  The password consists of the block access
     * token's password (known to the DataNode via its secret manager).  This
     * expects that the client has supplied a user name consisting of its
     * serialized block access token identifier.
     *
     * @param userName SASL user name containing serialized block access token
     *   identifier
     * @return expected correct SASL password
     * @throws IOException for any error
     */
    private char[] buildServerPassword(String userName) throws IOException {
        BlockTokenIdentifier identifier = deserializeIdentifier(userName);
        byte[] tokenPassword = blockPoolTokenSecretManager.retrievePassword(identifier);
        return (new String(Base64.encodeBase64(tokenPassword, false), Charsets.UTF_8)).toCharArray();
    }

    /**
     * Deserializes a base64-encoded binary representation of a block access
     * token.
     *
     * @param str String to deserialize
     * @return BlockTokenIdentifier deserialized from str
     * @throws IOException if there is any I/O error
     */
    private BlockTokenIdentifier deserializeIdentifier(String str) throws IOException {
        BlockTokenIdentifier identifier = new BlockTokenIdentifier();
        identifier.readFields(new DataInputStream(new ByteArrayInputStream(Base64.decodeBase64(str))));
        return identifier;
    }

    /**
     * This method actually executes the server-side SASL handshake.
     *
     * @param underlyingOut connection output stream
     * @param underlyingIn connection input stream
     * @param saslProps properties of SASL negotiation
     * @param callbackHandler for responding to SASL callbacks
     * @return new pair of streams, wrapped after SASL negotiation
     * @throws IOException for any error
     */
    private IOStreamPair doSaslHandshake(OutputStream underlyingOut, InputStream underlyingIn,
            Map<String, String> saslProps, CallbackHandler callbackHandler) throws IOException {

        DataInputStream in = new DataInputStream(underlyingIn);
        DataOutputStream out = new DataOutputStream(underlyingOut);

        SaslParticipant sasl = SaslParticipant.createServerSaslParticipant(saslProps, callbackHandler);

        int magicNumber = in.readInt();
        if (magicNumber != SASL_TRANSFER_MAGIC_NUMBER) {
            throw new InvalidMagicNumberException(magicNumber, dnConf.getEncryptDataTransfer());
        }
        try {
            // step 1
            byte[] remoteResponse = readSaslMessage(in);
            byte[] localResponse = sasl.evaluateChallengeOrResponse(remoteResponse);
            sendSaslMessage(out, localResponse);

            // step 2 (server-side only)
            List<CipherOption> cipherOptions = Lists.newArrayList();
            remoteResponse = readSaslMessageAndNegotiationCipherOptions(in, cipherOptions);
            localResponse = sasl.evaluateChallengeOrResponse(remoteResponse);

            // SASL handshake is complete
            checkSaslComplete(sasl, saslProps);

            CipherOption cipherOption = null;
            if (sasl.isNegotiatedQopPrivacy()) {
                // Negotiate a cipher option
                cipherOption = negotiateCipherOption(dnConf.getConf(), cipherOptions);
                if (cipherOption != null) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Server using cipher suite " + cipherOption.getCipherSuite().getName());
                    }
                }
            }

            // If negotiated cipher option is not null, wrap it before sending.
            sendSaslMessageAndNegotiatedCipherOption(out, localResponse, wrap(cipherOption, sasl));

            // If negotiated cipher option is not null, we will use it to create 
            // stream pair.
            return cipherOption != null
                    ? createStreamPair(dnConf.getConf(), cipherOption, underlyingOut, underlyingIn, true)
                    : sasl.createStreamPair(out, in);
        } catch (IOException ioe) {
            if (ioe instanceof SaslException && ioe.getCause() != null
                    && ioe.getCause() instanceof InvalidEncryptionKeyException) {
                // This could just be because the client is long-lived and hasn't gotten
                // a new encryption key from the NN in a while. Upon receiving this
                // error, the client will get a new encryption key from the NN and retry
                // connecting to this DN.
                sendInvalidKeySaslErrorMessage(out, ioe.getCause().getMessage());
            } else {
                sendGenericSaslErrorMessage(out, ioe.getMessage());
            }
            throw ioe;
        }
    }

    /**
     * Sends a SASL negotiation message indicating an invalid key error.
     *
     * @param out stream to receive message
     * @param message to send
     * @throws IOException for any error
     */
    private static void sendInvalidKeySaslErrorMessage(DataOutputStream out, String message) throws IOException {
        sendSaslMessage(out, DataTransferEncryptorStatus.ERROR_UNKNOWN_KEY, null, message);
    }
}