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