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

Java tutorial

Introduction

Here is the source code for org.apache.hadoop.hdfs.protocol.datatransfer.sasl.DataTransferSaslUtil.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.fs.CommonConfigurationKeysPublic.HADOOP_RPC_PROTECTION;
import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.HADOOP_SECURITY_SASL_PROPS_RESOLVER_CLASS;
import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_DATA_TRANSFER_PROTECTION_KEY;
import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_DATA_TRANSFER_SASL_PROPS_RESOLVER_CLASS_KEY;
import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_ENCRYPT_DATA_TRANSFER_CIPHER_KEY_BITLENGTH_KEY;
import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_ENCRYPT_DATA_TRANSFER_CIPHER_KEY_BITLENGTH_DEFAULT;
import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_ENCRYPT_DATA_TRANSFER_CIPHER_SUITES_KEY;
import static org.apache.hadoop.hdfs.protocolPB.PBHelper.vintPrefixed;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.security.sasl.Sasl;

import org.apache.commons.codec.binary.Base64;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.crypto.CipherOption;
import org.apache.hadoop.crypto.CipherSuite;
import org.apache.hadoop.crypto.CryptoCodec;
import org.apache.hadoop.crypto.CryptoInputStream;
import org.apache.hadoop.crypto.CryptoOutputStream;
import org.apache.hadoop.hdfs.net.Peer;
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;
import org.apache.hadoop.hdfs.protocol.proto.DataTransferProtos.DataTransferEncryptorMessageProto.DataTransferEncryptorStatus;
import org.apache.hadoop.hdfs.protocol.proto.HdfsProtos.CipherOptionProto;
import org.apache.hadoop.hdfs.protocolPB.PBHelper;
import org.apache.hadoop.security.SaslPropertiesResolver;
import org.apache.hadoop.security.SaslRpcServer.QualityOfProtection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.net.InetAddresses;
import com.google.protobuf.ByteString;

/**
 * Utility methods implementing SASL negotiation for DataTransferProtocol.
 */
@InterfaceAudience.Private
public final class DataTransferSaslUtil {

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

    /**
     * Delimiter for the three-part SASL username string.
     */
    public static final String NAME_DELIMITER = " ";

    /**
     * Sent by clients and validated by servers. We use a number that's unlikely
     * to ever be sent as the value of the DATA_TRANSFER_VERSION.
     */
    public static final int SASL_TRANSFER_MAGIC_NUMBER = 0xDEADBEEF;

    /**
     * Checks that SASL negotiation has completed for the given participant, and
     * the negotiated quality of protection is included in the given SASL
     * properties and therefore acceptable.
     *
     * @param sasl participant to check
     * @param saslProps properties of SASL negotiation
     * @throws IOException for any error
     */
    public static void checkSaslComplete(SaslParticipant sasl, Map<String, String> saslProps) throws IOException {
        if (!sasl.isComplete()) {
            throw new IOException("Failed to complete SASL handshake");
        }
        Set<String> requestedQop = ImmutableSet.copyOf(Arrays.asList(saslProps.get(Sasl.QOP).split(",")));
        String negotiatedQop = sasl.getNegotiatedQop();
        LOG.debug("Verifying QOP, requested QOP = {}, negotiated QOP = {}", requestedQop, negotiatedQop);
        if (!requestedQop.contains(negotiatedQop)) {
            throw new IOException(String.format(
                    "SASL handshake completed, but " + "channel does not have acceptable quality of protection, "
                            + "requested = %s, negotiated = %s",
                    requestedQop, negotiatedQop));
        }
    }

    /**
     * Check whether requested SASL Qop contains privacy.
     * 
     * @param saslProps properties of SASL negotiation
     * @return boolean true if privacy exists
     */
    public static boolean requestedQopContainsPrivacy(Map<String, String> saslProps) {
        Set<String> requestedQop = ImmutableSet.copyOf(Arrays.asList(saslProps.get(Sasl.QOP).split(",")));
        return requestedQop.contains("auth-conf");
    }

    /**
     * Creates SASL properties required for an encrypted SASL negotiation.
     *
     * @param encryptionAlgorithm to use for SASL negotation
     * @return properties of encrypted SASL negotiation
     */
    public static Map<String, String> createSaslPropertiesForEncryption(String encryptionAlgorithm) {
        Map<String, String> saslProps = Maps.newHashMapWithExpectedSize(3);
        saslProps.put(Sasl.QOP, QualityOfProtection.PRIVACY.getSaslQop());
        saslProps.put(Sasl.SERVER_AUTH, "true");
        saslProps.put("com.sun.security.sasl.digest.cipher", encryptionAlgorithm);
        return saslProps;
    }

    /**
     * For an encrypted SASL negotiation, encodes an encryption key to a SASL
     * password.
     *
     * @param encryptionKey to encode
     * @return key encoded as SASL password
     */
    public static char[] encryptionKeyToPassword(byte[] encryptionKey) {
        return new String(Base64.encodeBase64(encryptionKey, false), Charsets.UTF_8).toCharArray();
    }

    /**
     * Returns InetAddress from peer.  The getRemoteAddressString has the form
     * [host][/ip-address]:port.  The host may be missing.  The IP address (and
     * preceding '/') may be missing.  The port preceded by ':' is always present.
     *
     * @param peer
     * @return InetAddress from peer
     */
    public static InetAddress getPeerAddress(Peer peer) {
        String remoteAddr = peer.getRemoteAddressString().split(":")[0];
        int slashIdx = remoteAddr.indexOf('/');
        return InetAddresses
                .forString(slashIdx != -1 ? remoteAddr.substring(slashIdx + 1, remoteAddr.length()) : remoteAddr);
    }

    /**
     * Creates a SaslPropertiesResolver from the given configuration.  This method
     * works by cloning the configuration, translating configuration properties
     * specific to DataTransferProtocol to what SaslPropertiesResolver expects,
     * and then delegating to SaslPropertiesResolver for initialization.  This
     * method returns null if SASL protection has not been configured for
     * DataTransferProtocol.
     *
     * @param conf configuration to read
     * @return SaslPropertiesResolver for DataTransferProtocol, or null if not
     *   configured
     */
    public static SaslPropertiesResolver getSaslPropertiesResolver(Configuration conf) {
        String qops = conf.get(DFS_DATA_TRANSFER_PROTECTION_KEY);
        if (qops == null || qops.isEmpty()) {
            LOG.debug("DataTransferProtocol not using SaslPropertiesResolver, no "
                    + "QOP found in configuration for {}", DFS_DATA_TRANSFER_PROTECTION_KEY);
            return null;
        }
        Configuration saslPropsResolverConf = new Configuration(conf);
        saslPropsResolverConf.set(HADOOP_RPC_PROTECTION, qops);
        Class<? extends SaslPropertiesResolver> resolverClass = conf.getClass(
                HADOOP_SECURITY_SASL_PROPS_RESOLVER_CLASS, SaslPropertiesResolver.class,
                SaslPropertiesResolver.class);
        resolverClass = conf.getClass(DFS_DATA_TRANSFER_SASL_PROPS_RESOLVER_CLASS_KEY, resolverClass,
                SaslPropertiesResolver.class);
        saslPropsResolverConf.setClass(HADOOP_SECURITY_SASL_PROPS_RESOLVER_CLASS, resolverClass,
                SaslPropertiesResolver.class);
        SaslPropertiesResolver resolver = SaslPropertiesResolver.getInstance(saslPropsResolverConf);
        LOG.debug(
                "DataTransferProtocol using SaslPropertiesResolver, configured "
                        + "QOP {} = {}, configured class {} = {}",
                DFS_DATA_TRANSFER_PROTECTION_KEY, qops, DFS_DATA_TRANSFER_SASL_PROPS_RESOLVER_CLASS_KEY,
                resolverClass);
        return resolver;
    }

    /**
     * Reads a SASL negotiation message.
     *
     * @param in stream to read
     * @return bytes of SASL negotiation messsage
     * @throws IOException for any error
     */
    public static byte[] readSaslMessage(InputStream in) throws IOException {
        DataTransferEncryptorMessageProto proto = DataTransferEncryptorMessageProto.parseFrom(vintPrefixed(in));
        if (proto.getStatus() == DataTransferEncryptorStatus.ERROR_UNKNOWN_KEY) {
            throw new InvalidEncryptionKeyException(proto.getMessage());
        } else if (proto.getStatus() == DataTransferEncryptorStatus.ERROR) {
            throw new IOException(proto.getMessage());
        } else {
            return proto.getPayload().toByteArray();
        }
    }

    /**
     * Reads a SASL negotiation message and negotiation cipher options. 
     * 
     * @param in stream to read
     * @param cipherOptions list to store negotiation cipher options
     * @return byte[] SASL negotiation message
     * @throws IOException for any error
     */
    public static byte[] readSaslMessageAndNegotiationCipherOptions(InputStream in,
            List<CipherOption> cipherOptions) throws IOException {
        DataTransferEncryptorMessageProto proto = DataTransferEncryptorMessageProto.parseFrom(vintPrefixed(in));
        if (proto.getStatus() == DataTransferEncryptorStatus.ERROR_UNKNOWN_KEY) {
            throw new InvalidEncryptionKeyException(proto.getMessage());
        } else if (proto.getStatus() == DataTransferEncryptorStatus.ERROR) {
            throw new IOException(proto.getMessage());
        } else {
            List<CipherOptionProto> optionProtos = proto.getCipherOptionList();
            if (optionProtos != null) {
                for (CipherOptionProto optionProto : optionProtos) {
                    cipherOptions.add(PBHelper.convert(optionProto));
                }
            }
            return proto.getPayload().toByteArray();
        }
    }

    /**
     * Negotiate a cipher option which server supports.
     * 
     * @param conf the configuration
     * @param options the cipher options which client supports
     * @return CipherOption negotiated cipher option
     */
    public static CipherOption negotiateCipherOption(Configuration conf, List<CipherOption> options)
            throws IOException {
        // Negotiate cipher suites if configured.  Currently, the only supported
        // cipher suite is AES/CTR/NoPadding, but the protocol allows multiple
        // values for future expansion.
        String cipherSuites = conf.get(DFS_ENCRYPT_DATA_TRANSFER_CIPHER_SUITES_KEY);
        if (cipherSuites == null || cipherSuites.isEmpty()) {
            return null;
        }
        if (!cipherSuites.equals(CipherSuite.AES_CTR_NOPADDING.getName())) {
            throw new IOException(String.format("Invalid cipher suite, %s=%s",
                    DFS_ENCRYPT_DATA_TRANSFER_CIPHER_SUITES_KEY, cipherSuites));
        }
        if (options != null) {
            for (CipherOption option : options) {
                CipherSuite suite = option.getCipherSuite();
                if (suite == CipherSuite.AES_CTR_NOPADDING) {
                    int keyLen = conf.getInt(DFS_ENCRYPT_DATA_TRANSFER_CIPHER_KEY_BITLENGTH_KEY,
                            DFS_ENCRYPT_DATA_TRANSFER_CIPHER_KEY_BITLENGTH_DEFAULT) / 8;
                    CryptoCodec codec = CryptoCodec.getInstance(conf, suite);
                    byte[] inKey = new byte[keyLen];
                    byte[] inIv = new byte[suite.getAlgorithmBlockSize()];
                    byte[] outKey = new byte[keyLen];
                    byte[] outIv = new byte[suite.getAlgorithmBlockSize()];
                    codec.generateSecureRandom(inKey);
                    codec.generateSecureRandom(inIv);
                    codec.generateSecureRandom(outKey);
                    codec.generateSecureRandom(outIv);
                    return new CipherOption(suite, inKey, inIv, outKey, outIv);
                }
            }
        }
        return null;
    }

    /**
     * Send SASL message and negotiated cipher option to client.
     * 
     * @param out stream to receive message
     * @param payload to send
     * @param option negotiated cipher option
     * @throws IOException for any error
     */
    public static void sendSaslMessageAndNegotiatedCipherOption(OutputStream out, byte[] payload,
            CipherOption option) throws IOException {
        DataTransferEncryptorMessageProto.Builder builder = DataTransferEncryptorMessageProto.newBuilder();

        builder.setStatus(DataTransferEncryptorStatus.SUCCESS);
        if (payload != null) {
            builder.setPayload(ByteString.copyFrom(payload));
        }
        if (option != null) {
            builder.addCipherOption(PBHelper.convert(option));
        }

        DataTransferEncryptorMessageProto proto = builder.build();
        proto.writeDelimitedTo(out);
        out.flush();
    }

    /**
     * Create IOStreamPair of {@link org.apache.hadoop.crypto.CryptoInputStream}
     * and {@link org.apache.hadoop.crypto.CryptoOutputStream}
     * 
     * @param conf the configuration
     * @param cipherOption negotiated cipher option
     * @param out underlying output stream
     * @param in underlying input stream
     * @param isServer is server side
     * @return IOStreamPair the stream pair
     * @throws IOException for any error
     */
    public static IOStreamPair createStreamPair(Configuration conf, CipherOption cipherOption, OutputStream out,
            InputStream in, boolean isServer) throws IOException {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Creating IOStreamPair of CryptoInputStream and " + "CryptoOutputStream.");
        }
        CryptoCodec codec = CryptoCodec.getInstance(conf, cipherOption.getCipherSuite());
        byte[] inKey = cipherOption.getInKey();
        byte[] inIv = cipherOption.getInIv();
        byte[] outKey = cipherOption.getOutKey();
        byte[] outIv = cipherOption.getOutIv();
        InputStream cIn = new CryptoInputStream(in, codec, isServer ? inKey : outKey, isServer ? inIv : outIv);
        OutputStream cOut = new CryptoOutputStream(out, codec, isServer ? outKey : inKey, isServer ? outIv : inIv);
        return new IOStreamPair(cIn, cOut);
    }

    /**
     * Sends a SASL negotiation message indicating an error.
     *
     * @param out stream to receive message
     * @param message to send
     * @throws IOException for any error
     */
    public static void sendGenericSaslErrorMessage(OutputStream out, String message) throws IOException {
        sendSaslMessage(out, DataTransferEncryptorStatus.ERROR, null, message);
    }

    /**
     * Sends a SASL negotiation message.
     *
     * @param out stream to receive message
     * @param payload to send
     * @throws IOException for any error
     */
    public static void sendSaslMessage(OutputStream out, byte[] payload) throws IOException {
        sendSaslMessage(out, DataTransferEncryptorStatus.SUCCESS, payload, null);
    }

    /**
     * Send a SASL negotiation message and negotiation cipher options to server.
     * 
     * @param out stream to receive message
     * @param payload to send
     * @param options cipher options to negotiate
     * @throws IOException for any error
     */
    public static void sendSaslMessageAndNegotiationCipherOptions(OutputStream out, byte[] payload,
            List<CipherOption> options) throws IOException {
        DataTransferEncryptorMessageProto.Builder builder = DataTransferEncryptorMessageProto.newBuilder();

        builder.setStatus(DataTransferEncryptorStatus.SUCCESS);
        if (payload != null) {
            builder.setPayload(ByteString.copyFrom(payload));
        }
        if (options != null) {
            builder.addAllCipherOption(PBHelper.convertCipherOptions(options));
        }

        DataTransferEncryptorMessageProto proto = builder.build();
        proto.writeDelimitedTo(out);
        out.flush();
    }

    /**
     * Read SASL message and negotiated cipher option from server.
     * 
     * @param in stream to read
     * @return SaslResponseWithNegotiatedCipherOption SASL message and 
     * negotiated cipher option
     * @throws IOException for any error
     */
    public static SaslResponseWithNegotiatedCipherOption readSaslMessageAndNegotiatedCipherOption(InputStream in)
            throws IOException {
        DataTransferEncryptorMessageProto proto = DataTransferEncryptorMessageProto.parseFrom(vintPrefixed(in));
        if (proto.getStatus() == DataTransferEncryptorStatus.ERROR_UNKNOWN_KEY) {
            throw new InvalidEncryptionKeyException(proto.getMessage());
        } else if (proto.getStatus() == DataTransferEncryptorStatus.ERROR) {
            throw new IOException(proto.getMessage());
        } else {
            byte[] response = proto.getPayload().toByteArray();
            List<CipherOption> options = PBHelper.convertCipherOptionProtos(proto.getCipherOptionList());
            CipherOption option = null;
            if (options != null && !options.isEmpty()) {
                option = options.get(0);
            }
            return new SaslResponseWithNegotiatedCipherOption(response, option);
        }
    }

    /**
     * Encrypt the key and iv of the negotiated cipher option.
     * 
     * @param option negotiated cipher option
     * @param sasl SASL participant representing server
     * @return CipherOption negotiated cipher option which contains the 
     * encrypted key and iv
     * @throws IOException for any error
     */
    public static CipherOption wrap(CipherOption option, SaslParticipant sasl) throws IOException {
        if (option != null) {
            byte[] inKey = option.getInKey();
            if (inKey != null) {
                inKey = sasl.wrap(inKey, 0, inKey.length);
            }
            byte[] outKey = option.getOutKey();
            if (outKey != null) {
                outKey = sasl.wrap(outKey, 0, outKey.length);
            }
            return new CipherOption(option.getCipherSuite(), inKey, option.getInIv(), outKey, option.getOutIv());
        }

        return null;
    }

    /**
     * Decrypt the key and iv of the negotiated cipher option.
     * 
     * @param option negotiated cipher option
     * @param sasl SASL participant representing client
     * @return CipherOption negotiated cipher option which contains the 
     * decrypted key and iv
     * @throws IOException for any error
     */
    public static CipherOption unwrap(CipherOption option, SaslParticipant sasl) throws IOException {
        if (option != null) {
            byte[] inKey = option.getInKey();
            if (inKey != null) {
                inKey = sasl.unwrap(inKey, 0, inKey.length);
            }
            byte[] outKey = option.getOutKey();
            if (outKey != null) {
                outKey = sasl.unwrap(outKey, 0, outKey.length);
            }
            return new CipherOption(option.getCipherSuite(), inKey, option.getInIv(), outKey, option.getOutIv());
        }

        return null;
    }

    /**
     * Sends a SASL negotiation message.
     *
     * @param out stream to receive message
     * @param status negotiation status
     * @param payload to send
     * @param message to send
     * @throws IOException for any error
     */
    public static void sendSaslMessage(OutputStream out, DataTransferEncryptorStatus status, byte[] payload,
            String message) throws IOException {
        DataTransferEncryptorMessageProto.Builder builder = DataTransferEncryptorMessageProto.newBuilder();

        builder.setStatus(status);
        if (payload != null) {
            builder.setPayload(ByteString.copyFrom(payload));
        }
        if (message != null) {
            builder.setMessage(message);
        }

        DataTransferEncryptorMessageProto proto = builder.build();
        proto.writeDelimitedTo(out);
        out.flush();
    }

    /**
     * There is no reason to instantiate this class.
     */
    private DataTransferSaslUtil() {
    }
}