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.crypto.key.kms;
019
020import org.apache.commons.codec.binary.Base64;
021import org.apache.hadoop.classification.InterfaceAudience;
022import org.apache.hadoop.conf.Configuration;
023import org.apache.hadoop.crypto.key.KeyProvider;
024import org.apache.hadoop.crypto.key.KeyProviderCryptoExtension.EncryptedKeyVersion;
025import org.apache.hadoop.crypto.key.KeyProviderDelegationTokenExtension;
026import org.apache.hadoop.crypto.key.KeyProviderFactory;
027import org.apache.hadoop.fs.CommonConfigurationKeysPublic;
028import org.apache.hadoop.fs.Path;
029import org.apache.hadoop.io.Text;
030import org.apache.hadoop.security.Credentials;
031import org.apache.hadoop.security.ProviderUtils;
032import org.apache.hadoop.security.SecurityUtil;
033import org.apache.hadoop.security.UserGroupInformation;
034import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
035import org.apache.hadoop.security.authentication.client.AuthenticationException;
036import org.apache.hadoop.security.authentication.client.ConnectionConfigurator;
037import org.apache.hadoop.security.ssl.SSLFactory;
038import org.apache.hadoop.security.token.Token;
039import org.apache.hadoop.security.token.delegation.web.DelegationTokenAuthenticatedURL;
040import org.apache.hadoop.util.HttpExceptionUtils;
041import org.apache.http.client.utils.URIBuilder;
042import org.codehaus.jackson.map.ObjectMapper;
043
044import javax.net.ssl.HttpsURLConnection;
045
046import java.io.IOException;
047import java.io.InputStream;
048import java.io.OutputStream;
049import java.io.OutputStreamWriter;
050import java.io.Writer;
051import java.lang.reflect.UndeclaredThrowableException;
052import java.net.HttpURLConnection;
053import java.net.InetSocketAddress;
054import java.net.SocketTimeoutException;
055import java.net.URI;
056import java.net.URISyntaxException;
057import java.net.URL;
058import java.net.URLEncoder;
059import java.security.GeneralSecurityException;
060import java.security.NoSuchAlgorithmException;
061import java.security.PrivilegedExceptionAction;
062import java.util.ArrayList;
063import java.util.Date;
064import java.util.HashMap;
065import java.util.LinkedList;
066import java.util.List;
067import java.util.Map;
068import java.util.Queue;
069import java.util.concurrent.ExecutionException;
070
071import org.apache.hadoop.crypto.key.KeyProviderCryptoExtension;
072import org.apache.hadoop.crypto.key.KeyProviderCryptoExtension.CryptoExtension;
073
074import com.google.common.base.Preconditions;
075
076/**
077 * KMS client <code>KeyProvider</code> implementation.
078 */
079@InterfaceAudience.Private
080public class KMSClientProvider extends KeyProvider implements CryptoExtension,
081    KeyProviderDelegationTokenExtension.DelegationTokenExtension {
082
083  private static final String ANONYMOUS_REQUESTS_DISALLOWED = "Anonymous requests are disallowed";
084
085  public static final String TOKEN_KIND = "kms-dt";
086
087  public static final String SCHEME_NAME = "kms";
088
089  private static final String UTF8 = "UTF-8";
090
091  private static final String CONTENT_TYPE = "Content-Type";
092  private static final String APPLICATION_JSON_MIME = "application/json";
093
094  private static final String HTTP_GET = "GET";
095  private static final String HTTP_POST = "POST";
096  private static final String HTTP_PUT = "PUT";
097  private static final String HTTP_DELETE = "DELETE";
098
099
100  private static final String CONFIG_PREFIX = "hadoop.security.kms.client.";
101
102  /* It's possible to specify a timeout, in seconds, in the config file */
103  public static final String TIMEOUT_ATTR = CONFIG_PREFIX + "timeout";
104  public static final int DEFAULT_TIMEOUT = 60;
105
106  /* Number of times to retry authentication in the event of auth failure
107   * (normally happens due to stale authToken) 
108   */
109  public static final String AUTH_RETRY = CONFIG_PREFIX
110      + "authentication.retry-count";
111  public static final int DEFAULT_AUTH_RETRY = 1;
112
113  private final ValueQueue<EncryptedKeyVersion> encKeyVersionQueue;
114
115  private class EncryptedQueueRefiller implements
116    ValueQueue.QueueRefiller<EncryptedKeyVersion> {
117
118    @Override
119    public void fillQueueForKey(String keyName,
120        Queue<EncryptedKeyVersion> keyQueue, int numEKVs) throws IOException {
121      checkNotNull(keyName, "keyName");
122      Map<String, String> params = new HashMap<String, String>();
123      params.put(KMSRESTConstants.EEK_OP, KMSRESTConstants.EEK_GENERATE);
124      params.put(KMSRESTConstants.EEK_NUM_KEYS, "" + numEKVs);
125      URL url = createURL(KMSRESTConstants.KEY_RESOURCE, keyName,
126          KMSRESTConstants.EEK_SUB_RESOURCE, params);
127      HttpURLConnection conn = createConnection(url, HTTP_GET);
128      conn.setRequestProperty(CONTENT_TYPE, APPLICATION_JSON_MIME);
129      List response = call(conn, null,
130          HttpURLConnection.HTTP_OK, List.class);
131      List<EncryptedKeyVersion> ekvs =
132          parseJSONEncKeyVersion(keyName, response);
133      keyQueue.addAll(ekvs);
134    }
135  }
136
137  public static class KMSEncryptedKeyVersion extends EncryptedKeyVersion {
138    public KMSEncryptedKeyVersion(String keyName, String keyVersionName,
139        byte[] iv, String encryptedVersionName, byte[] keyMaterial) {
140      super(keyName, keyVersionName, iv, new KMSKeyVersion(null,
141          encryptedVersionName, keyMaterial));
142    }
143  }
144
145  @SuppressWarnings("rawtypes")
146  private static List<EncryptedKeyVersion>
147      parseJSONEncKeyVersion(String keyName, List valueList) {
148    List<EncryptedKeyVersion> ekvs = new LinkedList<EncryptedKeyVersion>();
149    if (!valueList.isEmpty()) {
150      for (Object values : valueList) {
151        Map valueMap = (Map) values;
152
153        String versionName = checkNotNull(
154                (String) valueMap.get(KMSRESTConstants.VERSION_NAME_FIELD),
155                KMSRESTConstants.VERSION_NAME_FIELD);
156
157        byte[] iv = Base64.decodeBase64(checkNotNull(
158                (String) valueMap.get(KMSRESTConstants.IV_FIELD),
159                KMSRESTConstants.IV_FIELD));
160
161        Map encValueMap = checkNotNull((Map)
162                valueMap.get(KMSRESTConstants.ENCRYPTED_KEY_VERSION_FIELD),
163                KMSRESTConstants.ENCRYPTED_KEY_VERSION_FIELD);
164
165        String encVersionName = checkNotNull((String)
166                encValueMap.get(KMSRESTConstants.VERSION_NAME_FIELD),
167                KMSRESTConstants.VERSION_NAME_FIELD);
168
169        byte[] encKeyMaterial = Base64.decodeBase64(checkNotNull((String)
170                encValueMap.get(KMSRESTConstants.MATERIAL_FIELD),
171                KMSRESTConstants.MATERIAL_FIELD));
172
173        ekvs.add(new KMSEncryptedKeyVersion(keyName, versionName, iv,
174            encVersionName, encKeyMaterial));
175      }
176    }
177    return ekvs;
178  }
179
180  private static KeyVersion parseJSONKeyVersion(Map valueMap) {
181    KeyVersion keyVersion = null;
182    if (!valueMap.isEmpty()) {
183      byte[] material = (valueMap.containsKey(KMSRESTConstants.MATERIAL_FIELD))
184          ? Base64.decodeBase64((String) valueMap.get(KMSRESTConstants.MATERIAL_FIELD))
185          : null;
186      String versionName = (String)valueMap.get(KMSRESTConstants.VERSION_NAME_FIELD);
187      String keyName = (String)valueMap.get(KMSRESTConstants.NAME_FIELD);
188      keyVersion = new KMSKeyVersion(keyName, versionName, material);
189    }
190    return keyVersion;
191  }
192
193  @SuppressWarnings("unchecked")
194  private static Metadata parseJSONMetadata(Map valueMap) {
195    Metadata metadata = null;
196    if (!valueMap.isEmpty()) {
197      metadata = new KMSMetadata(
198          (String) valueMap.get(KMSRESTConstants.CIPHER_FIELD),
199          (Integer) valueMap.get(KMSRESTConstants.LENGTH_FIELD),
200          (String) valueMap.get(KMSRESTConstants.DESCRIPTION_FIELD),
201          (Map<String, String>) valueMap.get(KMSRESTConstants.ATTRIBUTES_FIELD),
202          new Date((Long) valueMap.get(KMSRESTConstants.CREATED_FIELD)),
203          (Integer) valueMap.get(KMSRESTConstants.VERSIONS_FIELD));
204    }
205    return metadata;
206  }
207
208  private static void writeJson(Map map, OutputStream os) throws IOException {
209    Writer writer = new OutputStreamWriter(os);
210    ObjectMapper jsonMapper = new ObjectMapper();
211    jsonMapper.writerWithDefaultPrettyPrinter().writeValue(writer, map);
212  }
213
214  /**
215   * The factory to create KMSClientProvider, which is used by the
216   * ServiceLoader.
217   */
218  public static class Factory extends KeyProviderFactory {
219
220    @Override
221    public KeyProvider createProvider(URI providerName, Configuration conf)
222        throws IOException {
223      if (SCHEME_NAME.equals(providerName.getScheme())) {
224        return new KMSClientProvider(providerName, conf);
225      }
226      return null;
227    }
228  }
229
230  public static <T> T checkNotNull(T o, String name)
231      throws IllegalArgumentException {
232    if (o == null) {
233      throw new IllegalArgumentException("Parameter '" + name +
234          "' cannot be null");
235    }
236    return o;
237  }
238
239  public static String checkNotEmpty(String s, String name)
240      throws IllegalArgumentException {
241    checkNotNull(s, name);
242    if (s.isEmpty()) {
243      throw new IllegalArgumentException("Parameter '" + name +
244          "' cannot be empty");
245    }
246    return s;
247  }
248
249  private String kmsUrl;
250  private SSLFactory sslFactory;
251  private ConnectionConfigurator configurator;
252  private DelegationTokenAuthenticatedURL.Token authToken;
253  private final int authRetry;
254  private final UserGroupInformation actualUgi;
255
256  @Override
257  public String toString() {
258    final StringBuilder sb = new StringBuilder("KMSClientProvider[");
259    sb.append(kmsUrl).append("]");
260    return sb.toString();
261  }
262
263  /**
264   * This small class exists to set the timeout values for a connection
265   */
266  private static class TimeoutConnConfigurator
267          implements ConnectionConfigurator {
268    private ConnectionConfigurator cc;
269    private int timeout;
270
271    /**
272     * Sets the timeout and wraps another connection configurator
273     * @param timeout - will set both connect and read timeouts - in seconds
274     * @param cc - another configurator to wrap - may be null
275     */
276    public TimeoutConnConfigurator(int timeout, ConnectionConfigurator cc) {
277      this.timeout = timeout;
278      this.cc = cc;
279    }
280
281    /**
282     * Calls the wrapped configure() method, then sets timeouts
283     * @param conn the {@link HttpURLConnection} instance to configure.
284     * @return the connection
285     * @throws IOException
286     */
287    @Override
288    public HttpURLConnection configure(HttpURLConnection conn)
289            throws IOException {
290      if (cc != null) {
291        conn = cc.configure(conn);
292      }
293      conn.setConnectTimeout(timeout * 1000);  // conversion to milliseconds
294      conn.setReadTimeout(timeout * 1000);
295      return conn;
296    }
297  }
298
299  public KMSClientProvider(URI uri, Configuration conf) throws IOException {
300    super(conf);
301    Path path = ProviderUtils.unnestUri(uri);
302    URL url = path.toUri().toURL();
303    kmsUrl = createServiceURL(url);
304    if ("https".equalsIgnoreCase(url.getProtocol())) {
305      sslFactory = new SSLFactory(SSLFactory.Mode.CLIENT, conf);
306      try {
307        sslFactory.init();
308      } catch (GeneralSecurityException ex) {
309        throw new IOException(ex);
310      }
311    }
312    int timeout = conf.getInt(TIMEOUT_ATTR, DEFAULT_TIMEOUT);
313    authRetry = conf.getInt(AUTH_RETRY, DEFAULT_AUTH_RETRY);
314    configurator = new TimeoutConnConfigurator(timeout, sslFactory);
315    encKeyVersionQueue =
316        new ValueQueue<KeyProviderCryptoExtension.EncryptedKeyVersion>(
317            conf.getInt(
318                CommonConfigurationKeysPublic.KMS_CLIENT_ENC_KEY_CACHE_SIZE,
319                CommonConfigurationKeysPublic.
320                    KMS_CLIENT_ENC_KEY_CACHE_SIZE_DEFAULT),
321            conf.getFloat(
322                CommonConfigurationKeysPublic.
323                    KMS_CLIENT_ENC_KEY_CACHE_LOW_WATERMARK,
324                CommonConfigurationKeysPublic.
325                    KMS_CLIENT_ENC_KEY_CACHE_LOW_WATERMARK_DEFAULT),
326            conf.getInt(
327                CommonConfigurationKeysPublic.
328                    KMS_CLIENT_ENC_KEY_CACHE_EXPIRY_MS,
329                CommonConfigurationKeysPublic.
330                    KMS_CLIENT_ENC_KEY_CACHE_EXPIRY_DEFAULT),
331            conf.getInt(
332                CommonConfigurationKeysPublic.
333                    KMS_CLIENT_ENC_KEY_CACHE_NUM_REFILL_THREADS,
334                CommonConfigurationKeysPublic.
335                    KMS_CLIENT_ENC_KEY_CACHE_NUM_REFILL_THREADS_DEFAULT),
336            new EncryptedQueueRefiller());
337    authToken = new DelegationTokenAuthenticatedURL.Token();
338    actualUgi =
339        (UserGroupInformation.getCurrentUser().getAuthenticationMethod() ==
340        UserGroupInformation.AuthenticationMethod.PROXY) ? UserGroupInformation
341            .getCurrentUser().getRealUser() : UserGroupInformation
342            .getCurrentUser();
343  }
344
345  private String createServiceURL(URL url) throws IOException {
346    String str = url.toExternalForm();
347    if (str.endsWith("/")) {
348      str = str.substring(0, str.length() - 1);
349    }
350    return new URL(str + KMSRESTConstants.SERVICE_VERSION + "/").
351        toExternalForm();
352  }
353
354  private URL createURL(String collection, String resource, String subResource,
355      Map<String, ?> parameters) throws IOException {
356    try {
357      StringBuilder sb = new StringBuilder();
358      sb.append(kmsUrl);
359      if (collection != null) {
360        sb.append(collection);
361        if (resource != null) {
362          sb.append("/").append(URLEncoder.encode(resource, UTF8));
363          if (subResource != null) {
364            sb.append("/").append(subResource);
365          }
366        }
367      }
368      URIBuilder uriBuilder = new URIBuilder(sb.toString());
369      if (parameters != null) {
370        for (Map.Entry<String, ?> param : parameters.entrySet()) {
371          Object value = param.getValue();
372          if (value instanceof String) {
373            uriBuilder.addParameter(param.getKey(), (String) value);
374          } else {
375            for (String s : (String[]) value) {
376              uriBuilder.addParameter(param.getKey(), s);
377            }
378          }
379        }
380      }
381      return uriBuilder.build().toURL();
382    } catch (URISyntaxException ex) {
383      throw new IOException(ex);
384    }
385  }
386
387  private HttpURLConnection configureConnection(HttpURLConnection conn)
388      throws IOException {
389    if (sslFactory != null) {
390      HttpsURLConnection httpsConn = (HttpsURLConnection) conn;
391      try {
392        httpsConn.setSSLSocketFactory(sslFactory.createSSLSocketFactory());
393      } catch (GeneralSecurityException ex) {
394        throw new IOException(ex);
395      }
396      httpsConn.setHostnameVerifier(sslFactory.getHostnameVerifier());
397    }
398    return conn;
399  }
400
401  private HttpURLConnection createConnection(final URL url, String method)
402      throws IOException {
403    HttpURLConnection conn;
404    try {
405      // if current UGI is different from UGI at constructor time, behave as
406      // proxyuser
407      UserGroupInformation currentUgi = UserGroupInformation.getCurrentUser();
408      final String doAsUser = (currentUgi.getAuthenticationMethod() ==
409          UserGroupInformation.AuthenticationMethod.PROXY)
410                              ? currentUgi.getShortUserName() : null;
411
412      // check and renew TGT to handle potential expiration
413      actualUgi.checkTGTAndReloginFromKeytab();
414      // creating the HTTP connection using the current UGI at constructor time
415      conn = actualUgi.doAs(new PrivilegedExceptionAction<HttpURLConnection>() {
416        @Override
417        public HttpURLConnection run() throws Exception {
418          DelegationTokenAuthenticatedURL authUrl =
419              new DelegationTokenAuthenticatedURL(configurator);
420          return authUrl.openConnection(url, authToken, doAsUser);
421        }
422      });
423    } catch (IOException ex) {
424      throw ex;
425    } catch (UndeclaredThrowableException ex) {
426      throw new IOException(ex.getUndeclaredThrowable());
427    } catch (Exception ex) {
428      throw new IOException(ex);
429    }
430    conn.setUseCaches(false);
431    conn.setRequestMethod(method);
432    if (method.equals(HTTP_POST) || method.equals(HTTP_PUT)) {
433      conn.setDoOutput(true);
434    }
435    conn = configureConnection(conn);
436    return conn;
437  }
438
439  private <T> T call(HttpURLConnection conn, Map jsonOutput,
440      int expectedResponse, Class<T> klass) throws IOException {
441    return call(conn, jsonOutput, expectedResponse, klass, authRetry);
442  }
443
444  private <T> T call(HttpURLConnection conn, Map jsonOutput,
445      int expectedResponse, Class<T> klass, int authRetryCount)
446      throws IOException {
447    T ret = null;
448    try {
449      if (jsonOutput != null) {
450        writeJson(jsonOutput, conn.getOutputStream());
451      }
452    } catch (IOException ex) {
453      conn.getInputStream().close();
454      throw ex;
455    }
456    if ((conn.getResponseCode() == HttpURLConnection.HTTP_FORBIDDEN
457        && conn.getResponseMessage().equals(ANONYMOUS_REQUESTS_DISALLOWED))
458        || conn.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) {
459      // Ideally, this should happen only when there is an Authentication
460      // failure. Unfortunately, the AuthenticationFilter returns 403 when it
461      // cannot authenticate (Since a 401 requires Server to send
462      // WWW-Authenticate header as well)..
463      KMSClientProvider.this.authToken =
464          new DelegationTokenAuthenticatedURL.Token();
465      if (authRetryCount > 0) {
466        String contentType = conn.getRequestProperty(CONTENT_TYPE);
467        String requestMethod = conn.getRequestMethod();
468        URL url = conn.getURL();
469        conn = createConnection(url, requestMethod);
470        conn.setRequestProperty(CONTENT_TYPE, contentType);
471        return call(conn, jsonOutput, expectedResponse, klass,
472            authRetryCount - 1);
473      }
474    }
475    try {
476      AuthenticatedURL.extractToken(conn, authToken);
477    } catch (AuthenticationException e) {
478      // Ignore the AuthExceptions.. since we are just using the method to
479      // extract and set the authToken.. (Workaround till we actually fix
480      // AuthenticatedURL properly to set authToken post initialization)
481    }
482    HttpExceptionUtils.validateResponse(conn, expectedResponse);
483    if (APPLICATION_JSON_MIME.equalsIgnoreCase(conn.getContentType())
484        && klass != null) {
485      ObjectMapper mapper = new ObjectMapper();
486      InputStream is = null;
487      try {
488        is = conn.getInputStream();
489        ret = mapper.readValue(is, klass);
490      } catch (IOException ex) {
491        if (is != null) {
492          is.close();
493        }
494        throw ex;
495      } finally {
496        if (is != null) {
497          is.close();
498        }
499      }
500    }
501    return ret;
502  }
503
504  public static class KMSKeyVersion extends KeyVersion {
505    public KMSKeyVersion(String keyName, String versionName, byte[] material) {
506      super(keyName, versionName, material);
507    }
508  }
509
510  @Override
511  public KeyVersion getKeyVersion(String versionName) throws IOException {
512    checkNotEmpty(versionName, "versionName");
513    URL url = createURL(KMSRESTConstants.KEY_VERSION_RESOURCE,
514        versionName, null, null);
515    HttpURLConnection conn = createConnection(url, HTTP_GET);
516    Map response = call(conn, null, HttpURLConnection.HTTP_OK, Map.class);
517    return parseJSONKeyVersion(response);
518  }
519
520  @Override
521  public KeyVersion getCurrentKey(String name) throws IOException {
522    checkNotEmpty(name, "name");
523    URL url = createURL(KMSRESTConstants.KEY_RESOURCE, name,
524        KMSRESTConstants.CURRENT_VERSION_SUB_RESOURCE, null);
525    HttpURLConnection conn = createConnection(url, HTTP_GET);
526    Map response = call(conn, null, HttpURLConnection.HTTP_OK, Map.class);
527    return parseJSONKeyVersion(response);
528  }
529
530  @Override
531  @SuppressWarnings("unchecked")
532  public List<String> getKeys() throws IOException {
533    URL url = createURL(KMSRESTConstants.KEYS_NAMES_RESOURCE, null, null,
534        null);
535    HttpURLConnection conn = createConnection(url, HTTP_GET);
536    List response = call(conn, null, HttpURLConnection.HTTP_OK, List.class);
537    return (List<String>) response;
538  }
539
540  public static class KMSMetadata extends Metadata {
541    public KMSMetadata(String cipher, int bitLength, String description,
542        Map<String, String> attributes, Date created, int versions) {
543      super(cipher, bitLength, description, attributes, created, versions);
544    }
545  }
546
547  // breaking keyNames into sets to keep resulting URL undler 2000 chars
548  private List<String[]> createKeySets(String[] keyNames) {
549    List<String[]> list = new ArrayList<String[]>();
550    List<String> batch = new ArrayList<String>();
551    int batchLen = 0;
552    for (String name : keyNames) {
553      int additionalLen = KMSRESTConstants.KEY.length() + 1 + name.length();
554      batchLen += additionalLen;
555      // topping at 1500 to account for initial URL and encoded names
556      if (batchLen > 1500) {
557        list.add(batch.toArray(new String[batch.size()]));
558        batch = new ArrayList<String>();
559        batchLen = additionalLen;
560      }
561      batch.add(name);
562    }
563    if (!batch.isEmpty()) {
564      list.add(batch.toArray(new String[batch.size()]));
565    }
566    return list;
567  }
568
569  @Override
570  @SuppressWarnings("unchecked")
571  public Metadata[] getKeysMetadata(String ... keyNames) throws IOException {
572    List<Metadata> keysMetadata = new ArrayList<Metadata>();
573    List<String[]> keySets = createKeySets(keyNames);
574    for (String[] keySet : keySets) {
575      if (keyNames.length > 0) {
576        Map<String, Object> queryStr = new HashMap<String, Object>();
577        queryStr.put(KMSRESTConstants.KEY, keySet);
578        URL url = createURL(KMSRESTConstants.KEYS_METADATA_RESOURCE, null,
579            null, queryStr);
580        HttpURLConnection conn = createConnection(url, HTTP_GET);
581        List<Map> list = call(conn, null, HttpURLConnection.HTTP_OK, List.class);
582        for (Map map : list) {
583          keysMetadata.add(parseJSONMetadata(map));
584        }
585      }
586    }
587    return keysMetadata.toArray(new Metadata[keysMetadata.size()]);
588  }
589
590  private KeyVersion createKeyInternal(String name, byte[] material,
591      Options options)
592      throws NoSuchAlgorithmException, IOException {
593    checkNotEmpty(name, "name");
594    checkNotNull(options, "options");
595    Map<String, Object> jsonKey = new HashMap<String, Object>();
596    jsonKey.put(KMSRESTConstants.NAME_FIELD, name);
597    jsonKey.put(KMSRESTConstants.CIPHER_FIELD, options.getCipher());
598    jsonKey.put(KMSRESTConstants.LENGTH_FIELD, options.getBitLength());
599    if (material != null) {
600      jsonKey.put(KMSRESTConstants.MATERIAL_FIELD,
601          Base64.encodeBase64String(material));
602    }
603    if (options.getDescription() != null) {
604      jsonKey.put(KMSRESTConstants.DESCRIPTION_FIELD,
605          options.getDescription());
606    }
607    if (options.getAttributes() != null && !options.getAttributes().isEmpty()) {
608      jsonKey.put(KMSRESTConstants.ATTRIBUTES_FIELD, options.getAttributes());
609    }
610    URL url = createURL(KMSRESTConstants.KEYS_RESOURCE, null, null, null);
611    HttpURLConnection conn = createConnection(url, HTTP_POST);
612    conn.setRequestProperty(CONTENT_TYPE, APPLICATION_JSON_MIME);
613    Map response = call(conn, jsonKey, HttpURLConnection.HTTP_CREATED,
614        Map.class);
615    return parseJSONKeyVersion(response);
616  }
617
618  @Override
619  public KeyVersion createKey(String name, Options options)
620      throws NoSuchAlgorithmException, IOException {
621    return createKeyInternal(name, null, options);
622  }
623
624  @Override
625  public KeyVersion createKey(String name, byte[] material, Options options)
626      throws IOException {
627    checkNotNull(material, "material");
628    try {
629      return createKeyInternal(name, material, options);
630    } catch (NoSuchAlgorithmException ex) {
631      throw new RuntimeException("It should not happen", ex);
632    }
633  }
634
635  private KeyVersion rollNewVersionInternal(String name, byte[] material)
636      throws NoSuchAlgorithmException, IOException {
637    checkNotEmpty(name, "name");
638    Map<String, String> jsonMaterial = new HashMap<String, String>();
639    if (material != null) {
640      jsonMaterial.put(KMSRESTConstants.MATERIAL_FIELD,
641          Base64.encodeBase64String(material));
642    }
643    URL url = createURL(KMSRESTConstants.KEY_RESOURCE, name, null, null);
644    HttpURLConnection conn = createConnection(url, HTTP_POST);
645    conn.setRequestProperty(CONTENT_TYPE, APPLICATION_JSON_MIME);
646    Map response = call(conn, jsonMaterial,
647        HttpURLConnection.HTTP_OK, Map.class);
648    KeyVersion keyVersion = parseJSONKeyVersion(response);
649    encKeyVersionQueue.drain(name);
650    return keyVersion;
651  }
652
653
654  @Override
655  public KeyVersion rollNewVersion(String name)
656      throws NoSuchAlgorithmException, IOException {
657    return rollNewVersionInternal(name, null);
658  }
659
660  @Override
661  public KeyVersion rollNewVersion(String name, byte[] material)
662      throws IOException {
663    checkNotNull(material, "material");
664    try {
665      return rollNewVersionInternal(name, material);
666    } catch (NoSuchAlgorithmException ex) {
667      throw new RuntimeException("It should not happen", ex);
668    }
669  }
670
671  @Override
672  public EncryptedKeyVersion generateEncryptedKey(
673      String encryptionKeyName) throws IOException, GeneralSecurityException {
674    try {
675      return encKeyVersionQueue.getNext(encryptionKeyName);
676    } catch (ExecutionException e) {
677      if (e.getCause() instanceof SocketTimeoutException) {
678        throw (SocketTimeoutException)e.getCause();
679      }
680      throw new IOException(e);
681    }
682  }
683
684  @SuppressWarnings("rawtypes")
685  @Override
686  public KeyVersion decryptEncryptedKey(
687      EncryptedKeyVersion encryptedKeyVersion) throws IOException,
688                                                      GeneralSecurityException {
689    checkNotNull(encryptedKeyVersion.getEncryptionKeyVersionName(),
690        "versionName");
691    checkNotNull(encryptedKeyVersion.getEncryptedKeyIv(), "iv");
692    Preconditions.checkArgument(
693        encryptedKeyVersion.getEncryptedKeyVersion().getVersionName()
694            .equals(KeyProviderCryptoExtension.EEK),
695        "encryptedKey version name must be '%s', is '%s'",
696        KeyProviderCryptoExtension.EEK,
697        encryptedKeyVersion.getEncryptedKeyVersion().getVersionName()
698    );
699    checkNotNull(encryptedKeyVersion.getEncryptedKeyVersion(), "encryptedKey");
700    Map<String, String> params = new HashMap<String, String>();
701    params.put(KMSRESTConstants.EEK_OP, KMSRESTConstants.EEK_DECRYPT);
702    Map<String, Object> jsonPayload = new HashMap<String, Object>();
703    jsonPayload.put(KMSRESTConstants.NAME_FIELD,
704        encryptedKeyVersion.getEncryptionKeyName());
705    jsonPayload.put(KMSRESTConstants.IV_FIELD, Base64.encodeBase64String(
706        encryptedKeyVersion.getEncryptedKeyIv()));
707    jsonPayload.put(KMSRESTConstants.MATERIAL_FIELD, Base64.encodeBase64String(
708            encryptedKeyVersion.getEncryptedKeyVersion().getMaterial()));
709    URL url = createURL(KMSRESTConstants.KEY_VERSION_RESOURCE,
710        encryptedKeyVersion.getEncryptionKeyVersionName(),
711        KMSRESTConstants.EEK_SUB_RESOURCE, params);
712    HttpURLConnection conn = createConnection(url, HTTP_POST);
713    conn.setRequestProperty(CONTENT_TYPE, APPLICATION_JSON_MIME);
714    Map response =
715        call(conn, jsonPayload, HttpURLConnection.HTTP_OK, Map.class);
716    return parseJSONKeyVersion(response);
717  }
718
719  @Override
720  public List<KeyVersion> getKeyVersions(String name) throws IOException {
721    checkNotEmpty(name, "name");
722    URL url = createURL(KMSRESTConstants.KEY_RESOURCE, name,
723        KMSRESTConstants.VERSIONS_SUB_RESOURCE, null);
724    HttpURLConnection conn = createConnection(url, HTTP_GET);
725    List response = call(conn, null, HttpURLConnection.HTTP_OK, List.class);
726    List<KeyVersion> versions = null;
727    if (!response.isEmpty()) {
728      versions = new ArrayList<KeyVersion>();
729      for (Object obj : response) {
730        versions.add(parseJSONKeyVersion((Map) obj));
731      }
732    }
733    return versions;
734  }
735
736  @Override
737  public Metadata getMetadata(String name) throws IOException {
738    checkNotEmpty(name, "name");
739    URL url = createURL(KMSRESTConstants.KEY_RESOURCE, name,
740        KMSRESTConstants.METADATA_SUB_RESOURCE, null);
741    HttpURLConnection conn = createConnection(url, HTTP_GET);
742    Map response = call(conn, null, HttpURLConnection.HTTP_OK, Map.class);
743    return parseJSONMetadata(response);
744  }
745
746  @Override
747  public void deleteKey(String name) throws IOException {
748    checkNotEmpty(name, "name");
749    URL url = createURL(KMSRESTConstants.KEY_RESOURCE, name, null, null);
750    HttpURLConnection conn = createConnection(url, HTTP_DELETE);
751    call(conn, null, HttpURLConnection.HTTP_OK, null);
752  }
753
754  @Override
755  public void flush() throws IOException {
756    // NOP
757    // the client does not keep any local state, thus flushing is not required
758    // because of the client.
759    // the server should not keep in memory state on behalf of clients either.
760  }
761
762  @Override
763  public void warmUpEncryptedKeys(String... keyNames)
764      throws IOException {
765    try {
766      encKeyVersionQueue.initializeQueuesForKeys(keyNames);
767    } catch (ExecutionException e) {
768      throw new IOException(e);
769    }
770  }
771
772  @Override
773  public void drain(String keyName) {
774    encKeyVersionQueue.drain(keyName);
775  }
776
777  @Override
778  public Token<?>[] addDelegationTokens(final String renewer,
779      Credentials credentials) throws IOException {
780    Token<?>[] tokens = null;
781    Text dtService = getDelegationTokenService();
782    Token<?> token = credentials.getToken(dtService);
783    if (token == null) {
784      final URL url = createURL(null, null, null, null);
785      final DelegationTokenAuthenticatedURL authUrl =
786          new DelegationTokenAuthenticatedURL(configurator);
787      try {
788        // 'actualUGI' is the UGI of the user creating the client 
789        // It is possible that the creator of the KMSClientProvier
790        // calls this method on behalf of a proxyUser (the doAsUser).
791        // In which case this call has to be made as the proxy user.
792        UserGroupInformation currentUgi = UserGroupInformation.getCurrentUser();
793        final String doAsUser = (currentUgi.getAuthenticationMethod() ==
794            UserGroupInformation.AuthenticationMethod.PROXY)
795                                ? currentUgi.getShortUserName() : null;
796
797        token = actualUgi.doAs(new PrivilegedExceptionAction<Token<?>>() {
798          @Override
799          public Token<?> run() throws Exception {
800            // Not using the cached token here.. Creating a new token here
801            // everytime.
802            return authUrl.getDelegationToken(url,
803                new DelegationTokenAuthenticatedURL.Token(), renewer, doAsUser);
804          }
805        });
806        if (token != null) {
807          credentials.addToken(token.getService(), token);
808          tokens = new Token<?>[] { token };
809        } else {
810          throw new IOException("Got NULL as delegation token");
811        }
812      } catch (InterruptedException e) {
813        Thread.currentThread().interrupt();
814      } catch (Exception e) {
815        throw new IOException(e);
816      }
817    }
818    return tokens;
819  }
820  
821  private Text getDelegationTokenService() throws IOException {
822    URL url = new URL(kmsUrl);
823    InetSocketAddress addr = new InetSocketAddress(url.getHost(),
824        url.getPort());
825    Text dtService = SecurityUtil.buildTokenService(addr);
826    return dtService;
827  }
828
829  /**
830   * Shutdown valueQueue executor threads
831   */
832  @Override
833  public void close() throws IOException {
834    try {
835      encKeyVersionQueue.shutdown();
836    } catch (Exception e) {
837      throw new IOException(e);
838    } finally {
839      if (sslFactory != null) {
840        sslFactory.destroy();
841      }
842    }
843  }
844}