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 */
018package org.apache.hadoop.hbase.util;
019
020import java.io.ByteArrayInputStream;
021import java.io.ByteArrayOutputStream;
022import java.io.IOException;
023import java.util.Map;
024import java.util.concurrent.ConcurrentHashMap;
025import org.apache.hadoop.conf.Configuration;
026import org.apache.hadoop.hbase.HBaseInterfaceAudience;
027import org.apache.hadoop.hbase.HConstants;
028import org.apache.hadoop.hbase.io.crypto.DefaultCipherProvider;
029import org.apache.hadoop.hbase.io.crypto.Encryption;
030import org.apache.hadoop.hbase.io.crypto.KeyStoreKeyProvider;
031import org.apache.hadoop.hbase.security.EncryptionUtil;
032import org.apache.yetus.audience.InterfaceAudience;
033import org.slf4j.Logger;
034import org.slf4j.LoggerFactory;
035
036@InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.TOOLS)
037public class EncryptionTest {
038  private static final Logger LOG = LoggerFactory.getLogger(EncryptionTest.class);
039
040  static final Map<String, Boolean> keyProviderResults = new ConcurrentHashMap<>();
041  static final Map<String, Boolean> cipherProviderResults = new ConcurrentHashMap<>();
042  static final Map<String, Boolean> cipherResults = new ConcurrentHashMap<>();
043
044  private EncryptionTest() {
045  }
046
047  /**
048   * Check that the configured key provider can be loaded and initialized, or throw an exception. nn
049   */
050  public static void testKeyProvider(final Configuration conf) throws IOException {
051    String providerClassName =
052      conf.get(HConstants.CRYPTO_KEYPROVIDER_CONF_KEY, KeyStoreKeyProvider.class.getName());
053    Boolean result = keyProviderResults.get(providerClassName);
054    if (result == null) {
055      try {
056        Encryption.getKeyProvider(conf);
057        keyProviderResults.put(providerClassName, true);
058      } catch (Exception e) { // most likely a RuntimeException
059        keyProviderResults.put(providerClassName, false);
060        throw new IOException(
061          "Key provider " + providerClassName + " failed test: " + e.getMessage(), e);
062      }
063    } else if (!result) {
064      throw new IOException("Key provider " + providerClassName + " previously failed test");
065    }
066  }
067
068  /**
069   * Check that the configured cipher provider can be loaded and initialized, or throw an exception.
070   * nn
071   */
072  public static void testCipherProvider(final Configuration conf) throws IOException {
073    String providerClassName =
074      conf.get(HConstants.CRYPTO_CIPHERPROVIDER_CONF_KEY, DefaultCipherProvider.class.getName());
075    Boolean result = cipherProviderResults.get(providerClassName);
076    if (result == null) {
077      try {
078        Encryption.getCipherProvider(conf);
079        cipherProviderResults.put(providerClassName, true);
080      } catch (Exception e) { // most likely a RuntimeException
081        cipherProviderResults.put(providerClassName, false);
082        throw new IOException(
083          "Cipher provider " + providerClassName + " failed test: " + e.getMessage(), e);
084      }
085    } else if (!result) {
086      throw new IOException("Cipher provider " + providerClassName + " previously failed test");
087    }
088  }
089
090  /**
091   * Check that the specified cipher can be loaded and initialized, or throw an exception. Verifies
092   * key and cipher provider configuration as a prerequisite for cipher verification. Also verifies
093   * if encryption is enabled globally.
094   * @param conf   HBase configuration
095   * @param cipher chiper algorith to use for the column family
096   * @param key    encryption key
097   * @throws IOException in case of encryption configuration error
098   */
099  public static void testEncryption(final Configuration conf, final String cipher, byte[] key)
100    throws IOException {
101    if (cipher == null) {
102      return;
103    }
104    if (!Encryption.isEncryptionEnabled(conf)) {
105      String message =
106        String.format("Cipher %s failed test: encryption is disabled on the cluster", cipher);
107      throw new IOException(message);
108    }
109    testKeyProvider(conf);
110    testCipherProvider(conf);
111    Boolean result = cipherResults.get(cipher);
112    if (result == null) {
113      try {
114        Encryption.Context context = Encryption.newContext(conf);
115        context.setCipher(Encryption.getCipher(conf, cipher));
116        if (key == null) {
117          // Make a random key since one was not provided
118          context.setKey(context.getCipher().getRandomKey());
119        } else {
120          // This will be a wrapped key from schema
121          context.setKey(EncryptionUtil.unwrapKey(conf,
122            conf.get(HConstants.CRYPTO_MASTERKEY_NAME_CONF_KEY, "hbase"), key));
123        }
124        byte[] iv = null;
125        if (context.getCipher().getIvLength() > 0) {
126          iv = new byte[context.getCipher().getIvLength()];
127          Bytes.secureRandom(iv);
128        }
129        byte[] plaintext = new byte[1024];
130        Bytes.random(plaintext);
131        ByteArrayOutputStream out = new ByteArrayOutputStream();
132        Encryption.encrypt(out, new ByteArrayInputStream(plaintext), context, iv);
133        byte[] ciphertext = out.toByteArray();
134        out.reset();
135        Encryption.decrypt(out, new ByteArrayInputStream(ciphertext), plaintext.length, context,
136          iv);
137        byte[] test = out.toByteArray();
138        if (!Bytes.equals(plaintext, test)) {
139          throw new IOException("Did not pass encrypt/decrypt test");
140        }
141        cipherResults.put(cipher, true);
142      } catch (Exception e) {
143        cipherResults.put(cipher, false);
144        throw new IOException("Cipher " + cipher + " failed test: " + e.getMessage(), e);
145      }
146    } else if (!result) {
147      throw new IOException("Cipher " + cipher + " previously failed test");
148    }
149  }
150}