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.security;
019
020import java.io.ByteArrayInputStream;
021import java.io.ByteArrayOutputStream;
022import java.io.IOException;
023import java.security.Key;
024import java.security.KeyException;
025import java.security.SecureRandom;
026import java.util.Properties;
027import javax.crypto.spec.SecretKeySpec;
028import org.apache.commons.crypto.cipher.CryptoCipherFactory;
029import org.apache.hadoop.conf.Configuration;
030import org.apache.hadoop.hbase.HConstants;
031import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
032import org.apache.hadoop.hbase.io.crypto.Cipher;
033import org.apache.hadoop.hbase.io.crypto.Encryption;
034import org.apache.hadoop.hbase.io.crypto.aes.CryptoAES;
035import org.apache.hadoop.hbase.util.Bytes;
036import org.apache.yetus.audience.InterfaceAudience;
037import org.apache.yetus.audience.InterfaceStability;
038import org.slf4j.Logger;
039import org.slf4j.LoggerFactory;
040
041import org.apache.hbase.thirdparty.com.google.protobuf.UnsafeByteOperations;
042import org.apache.hadoop.hbase.shaded.protobuf.generated.EncryptionProtos;
043import org.apache.hadoop.hbase.shaded.protobuf.generated.RPCProtos;
044
045/**
046 * Some static utility methods for encryption uses in hbase-client.
047 */
048@InterfaceAudience.Private
049@InterfaceStability.Evolving
050public final class EncryptionUtil {
051  static private final Logger LOG = LoggerFactory.getLogger(EncryptionUtil.class);
052
053  static private final SecureRandom RNG = new SecureRandom();
054
055  /**
056   * Private constructor to keep this class from being instantiated.
057   */
058  private EncryptionUtil() {
059  }
060
061  /**
062   * Protect a key by encrypting it with the secret key of the given subject.
063   * The configuration must be set up correctly for key alias resolution.
064   * @param conf configuration
065   * @param key the raw key bytes
066   * @param algorithm the algorithm to use with this key material
067   * @return the encrypted key bytes
068   * @throws IOException
069   */
070  public static byte[] wrapKey(Configuration conf, byte[] key, String algorithm)
071      throws IOException {
072    return wrapKey(conf,
073      conf.get(HConstants.CRYPTO_MASTERKEY_NAME_CONF_KEY, User.getCurrent().getShortName()),
074      new SecretKeySpec(key, algorithm));
075  }
076
077  /**
078   * Protect a key by encrypting it with the secret key of the given subject.
079   * The configuration must be set up correctly for key alias resolution.
080   * @param conf configuration
081   * @param subject subject key alias
082   * @param key the key
083   * @return the encrypted key bytes
084   */
085  public static byte[] wrapKey(Configuration conf, String subject, Key key)
086      throws IOException {
087    // Wrap the key with the configured encryption algorithm.
088    String algorithm =
089        conf.get(HConstants.CRYPTO_KEY_ALGORITHM_CONF_KEY, HConstants.CIPHER_AES);
090    Cipher cipher = Encryption.getCipher(conf, algorithm);
091    if (cipher == null) {
092      throw new RuntimeException("Cipher '" + algorithm + "' not available");
093    }
094    EncryptionProtos.WrappedKey.Builder builder = EncryptionProtos.WrappedKey.newBuilder();
095    builder.setAlgorithm(key.getAlgorithm());
096    byte[] iv = null;
097    if (cipher.getIvLength() > 0) {
098      iv = new byte[cipher.getIvLength()];
099      RNG.nextBytes(iv);
100      builder.setIv(UnsafeByteOperations.unsafeWrap(iv));
101    }
102    byte[] keyBytes = key.getEncoded();
103    builder.setLength(keyBytes.length);
104    builder.setHashAlgorithm(Encryption.getConfiguredHashAlgorithm(conf));
105    builder.setHash(
106      UnsafeByteOperations.unsafeWrap(Encryption.computeCryptoKeyHash(conf, keyBytes)));
107    ByteArrayOutputStream out = new ByteArrayOutputStream();
108    Encryption.encryptWithSubjectKey(out, new ByteArrayInputStream(keyBytes), subject,
109      conf, cipher, iv);
110    builder.setData(UnsafeByteOperations.unsafeWrap(out.toByteArray()));
111    // Build and return the protobuf message
112    out.reset();
113    builder.build().writeDelimitedTo(out);
114    return out.toByteArray();
115  }
116
117  /**
118   * Unwrap a key by decrypting it with the secret key of the given subject.
119   * The configuration must be set up correctly for key alias resolution.
120   * @param conf configuration
121   * @param subject subject key alias
122   * @param value the encrypted key bytes
123   * @return the raw key bytes
124   * @throws IOException
125   * @throws KeyException
126   */
127  public static Key unwrapKey(Configuration conf, String subject, byte[] value)
128      throws IOException, KeyException {
129    EncryptionProtos.WrappedKey wrappedKey = EncryptionProtos.WrappedKey.PARSER
130        .parseDelimitedFrom(new ByteArrayInputStream(value));
131    String algorithm = conf.get(HConstants.CRYPTO_KEY_ALGORITHM_CONF_KEY,
132      HConstants.CIPHER_AES);
133    Cipher cipher = Encryption.getCipher(conf, algorithm);
134    if (cipher == null) {
135      throw new RuntimeException("Cipher '" + algorithm + "' not available");
136    }
137    return getUnwrapKey(conf, subject, wrappedKey, cipher);
138  }
139
140  private static Key getUnwrapKey(Configuration conf, String subject,
141      EncryptionProtos.WrappedKey wrappedKey, Cipher cipher) throws IOException, KeyException {
142    String configuredHashAlgorithm = Encryption.getConfiguredHashAlgorithm(conf);
143    String wrappedHashAlgorithm = wrappedKey.getHashAlgorithm().trim();
144    if(!configuredHashAlgorithm.equalsIgnoreCase(wrappedHashAlgorithm)) {
145      String msg = String.format("Unexpected encryption key hash algorithm: %s (expecting: %s)",
146        wrappedHashAlgorithm, configuredHashAlgorithm);
147      if(Encryption.failOnHashAlgorithmMismatch(conf)) {
148        throw new KeyException(msg);
149      }
150      LOG.debug(msg);
151    }
152    ByteArrayOutputStream out = new ByteArrayOutputStream();
153    byte[] iv = wrappedKey.hasIv() ? wrappedKey.getIv().toByteArray() : null;
154    Encryption.decryptWithSubjectKey(out, wrappedKey.getData().newInput(),
155      wrappedKey.getLength(), subject, conf, cipher, iv);
156    byte[] keyBytes = out.toByteArray();
157    if (wrappedKey.hasHash()) {
158      if (!Bytes.equals(wrappedKey.getHash().toByteArray(),
159                        Encryption.hashWithAlg(wrappedHashAlgorithm, keyBytes))) {
160        throw new KeyException("Key was not successfully unwrapped");
161      }
162    }
163    return new SecretKeySpec(keyBytes, wrappedKey.getAlgorithm());
164  }
165
166  /**
167   * Unwrap a wal key by decrypting it with the secret key of the given subject. The configuration
168   * must be set up correctly for key alias resolution.
169   * @param conf configuration
170   * @param subject subject key alias
171   * @param value the encrypted key bytes
172   * @return the raw key bytes
173   * @throws IOException if key is not found for the subject, or if some I/O error occurs
174   * @throws KeyException if fail to unwrap the key
175   */
176  public static Key unwrapWALKey(Configuration conf, String subject, byte[] value)
177      throws IOException, KeyException {
178    EncryptionProtos.WrappedKey wrappedKey =
179        EncryptionProtos.WrappedKey.PARSER.parseDelimitedFrom(new ByteArrayInputStream(value));
180    String algorithm = conf.get(HConstants.CRYPTO_WAL_ALGORITHM_CONF_KEY, HConstants.CIPHER_AES);
181    Cipher cipher = Encryption.getCipher(conf, algorithm);
182    if (cipher == null) {
183      throw new RuntimeException("Cipher '" + algorithm + "' not available");
184    }
185    return getUnwrapKey(conf, subject, wrappedKey, cipher);
186  }
187
188  /**
189   * Helper to create an encyption context.
190   *
191   * @param conf The current configuration.
192   * @param family The current column descriptor.
193   * @return The created encryption context.
194   * @throws IOException if an encryption key for the column cannot be unwrapped
195   * @throws IllegalStateException in case of encryption related configuration errors
196   */
197  public static Encryption.Context createEncryptionContext(Configuration conf,
198    ColumnFamilyDescriptor family) throws IOException {
199    Encryption.Context cryptoContext = Encryption.Context.NONE;
200    String cipherName = family.getEncryptionType();
201    if (cipherName != null) {
202      if(!Encryption.isEncryptionEnabled(conf)) {
203        throw new IllegalStateException("Encryption for family '" + family.getNameAsString()
204          + "' configured with type '" + cipherName + "' but the encryption feature is disabled");
205      }
206      Cipher cipher;
207      Key key;
208      byte[] keyBytes = family.getEncryptionKey();
209      if (keyBytes != null) {
210        // Family provides specific key material
211        key = unwrapKey(conf, keyBytes);
212        // Use the algorithm the key wants
213        cipher = Encryption.getCipher(conf, key.getAlgorithm());
214        if (cipher == null) {
215          throw new IllegalStateException("Cipher '" + key.getAlgorithm() + "' is not available");
216        }
217        // Fail if misconfigured
218        // We use the encryption type specified in the column schema as a sanity check on
219        // what the wrapped key is telling us
220        if (!cipher.getName().equalsIgnoreCase(cipherName)) {
221          throw new IllegalStateException("Encryption for family '" + family.getNameAsString()
222            + "' configured with type '" + cipherName + "' but key specifies algorithm '"
223            + cipher.getName() + "'");
224        }
225      } else {
226        // Family does not provide key material, create a random key
227        cipher = Encryption.getCipher(conf, cipherName);
228        if (cipher == null) {
229          throw new IllegalStateException("Cipher '" + cipherName + "' is not available");
230        }
231        key = cipher.getRandomKey();
232      }
233      cryptoContext = Encryption.newContext(conf);
234      cryptoContext.setCipher(cipher);
235      cryptoContext.setKey(key);
236    }
237    return cryptoContext;
238  }
239
240  /**
241   * Helper for {@link #unwrapKey(Configuration, String, byte[])} which automatically uses the
242   * configured master and alternative keys, rather than having to specify a key type to unwrap
243   * with.
244   *
245   * The configuration must be set up correctly for key alias resolution.
246   *
247   * @param conf the current configuration
248   * @param keyBytes the key encrypted by master (or alternative) to unwrap
249   * @return the key bytes, decrypted
250   * @throws IOException if the key cannot be unwrapped
251   */
252  public static Key unwrapKey(Configuration conf, byte[] keyBytes) throws IOException {
253    Key key;
254    String masterKeyName = conf.get(HConstants.CRYPTO_MASTERKEY_NAME_CONF_KEY,
255      User.getCurrent().getShortName());
256    try {
257      // First try the master key
258      key = unwrapKey(conf, masterKeyName, keyBytes);
259    } catch (KeyException e) {
260      // If the current master key fails to unwrap, try the alternate, if
261      // one is configured
262      if (LOG.isDebugEnabled()) {
263        LOG.debug("Unable to unwrap key with current master key '" + masterKeyName + "'");
264      }
265      String alternateKeyName =
266        conf.get(HConstants.CRYPTO_MASTERKEY_ALTERNATE_NAME_CONF_KEY);
267      if (alternateKeyName != null) {
268        try {
269          key = unwrapKey(conf, alternateKeyName, keyBytes);
270        } catch (KeyException ex) {
271          throw new IOException(ex);
272        }
273      } else {
274        throw new IOException(e);
275      }
276    }
277    return key;
278  }
279
280  /**
281   * Helper to create an instance of CryptoAES.
282   *
283   * @param conf The current configuration.
284   * @param cryptoCipherMeta The metadata for create CryptoAES.
285   * @return The instance of CryptoAES.
286   * @throws IOException if create CryptoAES failed
287   */
288  public static CryptoAES createCryptoAES(RPCProtos.CryptoCipherMeta cryptoCipherMeta,
289                               Configuration conf) throws IOException {
290    Properties properties = new Properties();
291    // the property for cipher class
292    properties.setProperty(CryptoCipherFactory.CLASSES_KEY,
293        conf.get("hbase.rpc.crypto.encryption.aes.cipher.class",
294            "org.apache.commons.crypto.cipher.JceCipher"));
295    // create SaslAES for client
296    return new CryptoAES(cryptoCipherMeta.getTransformation(), properties,
297        cryptoCipherMeta.getInKey().toByteArray(),
298        cryptoCipherMeta.getOutKey().toByteArray(),
299        cryptoCipherMeta.getInIv().toByteArray(),
300        cryptoCipherMeta.getOutIv().toByteArray());
301  }
302}