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      // creating the HTTP connection using the current UGI at constructor time
413      conn = actualUgi.doAs(new PrivilegedExceptionAction<HttpURLConnection>() {
414        @Override
415        public HttpURLConnection run() throws Exception {
416          DelegationTokenAuthenticatedURL authUrl =
417              new DelegationTokenAuthenticatedURL(configurator);
418          return authUrl.openConnection(url, authToken, doAsUser);
419        }
420      });
421    } catch (IOException ex) {
422      throw ex;
423    } catch (UndeclaredThrowableException ex) {
424      throw new IOException(ex.getUndeclaredThrowable());
425    } catch (Exception ex) {
426      throw new IOException(ex);
427    }
428    conn.setUseCaches(false);
429    conn.setRequestMethod(method);
430    if (method.equals(HTTP_POST) || method.equals(HTTP_PUT)) {
431      conn.setDoOutput(true);
432    }
433    conn = configureConnection(conn);
434    return conn;
435  }
436
437  private <T> T call(HttpURLConnection conn, Map jsonOutput,
438      int expectedResponse, Class<T> klass) throws IOException {
439    return call(conn, jsonOutput, expectedResponse, klass, authRetry);
440  }
441
442  private <T> T call(HttpURLConnection conn, Map jsonOutput,
443      int expectedResponse, Class<T> klass, int authRetryCount)
444      throws IOException {
445    T ret = null;
446    try {
447      if (jsonOutput != null) {
448        writeJson(jsonOutput, conn.getOutputStream());
449      }
450    } catch (IOException ex) {
451      conn.getInputStream().close();
452      throw ex;
453    }
454    if ((conn.getResponseCode() == HttpURLConnection.HTTP_FORBIDDEN
455        && conn.getResponseMessage().equals(ANONYMOUS_REQUESTS_DISALLOWED))
456        || conn.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) {
457      // Ideally, this should happen only when there is an Authentication
458      // failure. Unfortunately, the AuthenticationFilter returns 403 when it
459      // cannot authenticate (Since a 401 requires Server to send
460      // WWW-Authenticate header as well)..
461      KMSClientProvider.this.authToken =
462          new DelegationTokenAuthenticatedURL.Token();
463      if (authRetryCount > 0) {
464        String contentType = conn.getRequestProperty(CONTENT_TYPE);
465        String requestMethod = conn.getRequestMethod();
466        URL url = conn.getURL();
467        conn = createConnection(url, requestMethod);
468        conn.setRequestProperty(CONTENT_TYPE, contentType);
469        return call(conn, jsonOutput, expectedResponse, klass,
470            authRetryCount - 1);
471      }
472    }
473    try {
474      AuthenticatedURL.extractToken(conn, authToken);
475    } catch (AuthenticationException e) {
476      // Ignore the AuthExceptions.. since we are just using the method to
477      // extract and set the authToken.. (Workaround till we actually fix
478      // AuthenticatedURL properly to set authToken post initialization)
479    }
480    HttpExceptionUtils.validateResponse(conn, expectedResponse);
481    if (APPLICATION_JSON_MIME.equalsIgnoreCase(conn.getContentType())
482        && klass != null) {
483      ObjectMapper mapper = new ObjectMapper();
484      InputStream is = null;
485      try {
486        is = conn.getInputStream();
487        ret = mapper.readValue(is, klass);
488      } catch (IOException ex) {
489        if (is != null) {
490          is.close();
491        }
492        throw ex;
493      } finally {
494        if (is != null) {
495          is.close();
496        }
497      }
498    }
499    return ret;
500  }
501
502  public static class KMSKeyVersion extends KeyVersion {
503    public KMSKeyVersion(String keyName, String versionName, byte[] material) {
504      super(keyName, versionName, material);
505    }
506  }
507
508  @Override
509  public KeyVersion getKeyVersion(String versionName) throws IOException {
510    checkNotEmpty(versionName, "versionName");
511    URL url = createURL(KMSRESTConstants.KEY_VERSION_RESOURCE,
512        versionName, null, null);
513    HttpURLConnection conn = createConnection(url, HTTP_GET);
514    Map response = call(conn, null, HttpURLConnection.HTTP_OK, Map.class);
515    return parseJSONKeyVersion(response);
516  }
517
518  @Override
519  public KeyVersion getCurrentKey(String name) throws IOException {
520    checkNotEmpty(name, "name");
521    URL url = createURL(KMSRESTConstants.KEY_RESOURCE, name,
522        KMSRESTConstants.CURRENT_VERSION_SUB_RESOURCE, null);
523    HttpURLConnection conn = createConnection(url, HTTP_GET);
524    Map response = call(conn, null, HttpURLConnection.HTTP_OK, Map.class);
525    return parseJSONKeyVersion(response);
526  }
527
528  @Override
529  @SuppressWarnings("unchecked")
530  public List<String> getKeys() throws IOException {
531    URL url = createURL(KMSRESTConstants.KEYS_NAMES_RESOURCE, null, null,
532        null);
533    HttpURLConnection conn = createConnection(url, HTTP_GET);
534    List response = call(conn, null, HttpURLConnection.HTTP_OK, List.class);
535    return (List<String>) response;
536  }
537
538  public static class KMSMetadata extends Metadata {
539    public KMSMetadata(String cipher, int bitLength, String description,
540        Map<String, String> attributes, Date created, int versions) {
541      super(cipher, bitLength, description, attributes, created, versions);
542    }
543  }
544
545  // breaking keyNames into sets to keep resulting URL undler 2000 chars
546  private List<String[]> createKeySets(String[] keyNames) {
547    List<String[]> list = new ArrayList<String[]>();
548    List<String> batch = new ArrayList<String>();
549    int batchLen = 0;
550    for (String name : keyNames) {
551      int additionalLen = KMSRESTConstants.KEY.length() + 1 + name.length();
552      batchLen += additionalLen;
553      // topping at 1500 to account for initial URL and encoded names
554      if (batchLen > 1500) {
555        list.add(batch.toArray(new String[batch.size()]));
556        batch = new ArrayList<String>();
557        batchLen = additionalLen;
558      }
559      batch.add(name);
560    }
561    if (!batch.isEmpty()) {
562      list.add(batch.toArray(new String[batch.size()]));
563    }
564    return list;
565  }
566
567  @Override
568  @SuppressWarnings("unchecked")
569  public Metadata[] getKeysMetadata(String ... keyNames) throws IOException {
570    List<Metadata> keysMetadata = new ArrayList<Metadata>();
571    List<String[]> keySets = createKeySets(keyNames);
572    for (String[] keySet : keySets) {
573      if (keyNames.length > 0) {
574        Map<String, Object> queryStr = new HashMap<String, Object>();
575        queryStr.put(KMSRESTConstants.KEY, keySet);
576        URL url = createURL(KMSRESTConstants.KEYS_METADATA_RESOURCE, null,
577            null, queryStr);
578        HttpURLConnection conn = createConnection(url, HTTP_GET);
579        List<Map> list = call(conn, null, HttpURLConnection.HTTP_OK, List.class);
580        for (Map map : list) {
581          keysMetadata.add(parseJSONMetadata(map));
582        }
583      }
584    }
585    return keysMetadata.toArray(new Metadata[keysMetadata.size()]);
586  }
587
588  private KeyVersion createKeyInternal(String name, byte[] material,
589      Options options)
590      throws NoSuchAlgorithmException, IOException {
591    checkNotEmpty(name, "name");
592    checkNotNull(options, "options");
593    Map<String, Object> jsonKey = new HashMap<String, Object>();
594    jsonKey.put(KMSRESTConstants.NAME_FIELD, name);
595    jsonKey.put(KMSRESTConstants.CIPHER_FIELD, options.getCipher());
596    jsonKey.put(KMSRESTConstants.LENGTH_FIELD, options.getBitLength());
597    if (material != null) {
598      jsonKey.put(KMSRESTConstants.MATERIAL_FIELD,
599          Base64.encodeBase64String(material));
600    }
601    if (options.getDescription() != null) {
602      jsonKey.put(KMSRESTConstants.DESCRIPTION_FIELD,
603          options.getDescription());
604    }
605    if (options.getAttributes() != null && !options.getAttributes().isEmpty()) {
606      jsonKey.put(KMSRESTConstants.ATTRIBUTES_FIELD, options.getAttributes());
607    }
608    URL url = createURL(KMSRESTConstants.KEYS_RESOURCE, null, null, null);
609    HttpURLConnection conn = createConnection(url, HTTP_POST);
610    conn.setRequestProperty(CONTENT_TYPE, APPLICATION_JSON_MIME);
611    Map response = call(conn, jsonKey, HttpURLConnection.HTTP_CREATED,
612        Map.class);
613    return parseJSONKeyVersion(response);
614  }
615
616  @Override
617  public KeyVersion createKey(String name, Options options)
618      throws NoSuchAlgorithmException, IOException {
619    return createKeyInternal(name, null, options);
620  }
621
622  @Override
623  public KeyVersion createKey(String name, byte[] material, Options options)
624      throws IOException {
625    checkNotNull(material, "material");
626    try {
627      return createKeyInternal(name, material, options);
628    } catch (NoSuchAlgorithmException ex) {
629      throw new RuntimeException("It should not happen", ex);
630    }
631  }
632
633  private KeyVersion rollNewVersionInternal(String name, byte[] material)
634      throws NoSuchAlgorithmException, IOException {
635    checkNotEmpty(name, "name");
636    Map<String, String> jsonMaterial = new HashMap<String, String>();
637    if (material != null) {
638      jsonMaterial.put(KMSRESTConstants.MATERIAL_FIELD,
639          Base64.encodeBase64String(material));
640    }
641    URL url = createURL(KMSRESTConstants.KEY_RESOURCE, name, null, null);
642    HttpURLConnection conn = createConnection(url, HTTP_POST);
643    conn.setRequestProperty(CONTENT_TYPE, APPLICATION_JSON_MIME);
644    Map response = call(conn, jsonMaterial,
645        HttpURLConnection.HTTP_OK, Map.class);
646    KeyVersion keyVersion = parseJSONKeyVersion(response);
647    encKeyVersionQueue.drain(name);
648    return keyVersion;
649  }
650
651
652  @Override
653  public KeyVersion rollNewVersion(String name)
654      throws NoSuchAlgorithmException, IOException {
655    return rollNewVersionInternal(name, null);
656  }
657
658  @Override
659  public KeyVersion rollNewVersion(String name, byte[] material)
660      throws IOException {
661    checkNotNull(material, "material");
662    try {
663      return rollNewVersionInternal(name, material);
664    } catch (NoSuchAlgorithmException ex) {
665      throw new RuntimeException("It should not happen", ex);
666    }
667  }
668
669  @Override
670  public EncryptedKeyVersion generateEncryptedKey(
671      String encryptionKeyName) throws IOException, GeneralSecurityException {
672    try {
673      return encKeyVersionQueue.getNext(encryptionKeyName);
674    } catch (ExecutionException e) {
675      if (e.getCause() instanceof SocketTimeoutException) {
676        throw (SocketTimeoutException)e.getCause();
677      }
678      throw new IOException(e);
679    }
680  }
681
682  @SuppressWarnings("rawtypes")
683  @Override
684  public KeyVersion decryptEncryptedKey(
685      EncryptedKeyVersion encryptedKeyVersion) throws IOException,
686                                                      GeneralSecurityException {
687    checkNotNull(encryptedKeyVersion.getEncryptionKeyVersionName(),
688        "versionName");
689    checkNotNull(encryptedKeyVersion.getEncryptedKeyIv(), "iv");
690    Preconditions.checkArgument(
691        encryptedKeyVersion.getEncryptedKeyVersion().getVersionName()
692            .equals(KeyProviderCryptoExtension.EEK),
693        "encryptedKey version name must be '%s', is '%s'",
694        KeyProviderCryptoExtension.EEK,
695        encryptedKeyVersion.getEncryptedKeyVersion().getVersionName()
696    );
697    checkNotNull(encryptedKeyVersion.getEncryptedKeyVersion(), "encryptedKey");
698    Map<String, String> params = new HashMap<String, String>();
699    params.put(KMSRESTConstants.EEK_OP, KMSRESTConstants.EEK_DECRYPT);
700    Map<String, Object> jsonPayload = new HashMap<String, Object>();
701    jsonPayload.put(KMSRESTConstants.NAME_FIELD,
702        encryptedKeyVersion.getEncryptionKeyName());
703    jsonPayload.put(KMSRESTConstants.IV_FIELD, Base64.encodeBase64String(
704        encryptedKeyVersion.getEncryptedKeyIv()));
705    jsonPayload.put(KMSRESTConstants.MATERIAL_FIELD, Base64.encodeBase64String(
706            encryptedKeyVersion.getEncryptedKeyVersion().getMaterial()));
707    URL url = createURL(KMSRESTConstants.KEY_VERSION_RESOURCE,
708        encryptedKeyVersion.getEncryptionKeyVersionName(),
709        KMSRESTConstants.EEK_SUB_RESOURCE, params);
710    HttpURLConnection conn = createConnection(url, HTTP_POST);
711    conn.setRequestProperty(CONTENT_TYPE, APPLICATION_JSON_MIME);
712    Map response =
713        call(conn, jsonPayload, HttpURLConnection.HTTP_OK, Map.class);
714    return parseJSONKeyVersion(response);
715  }
716
717  @Override
718  public List<KeyVersion> getKeyVersions(String name) throws IOException {
719    checkNotEmpty(name, "name");
720    URL url = createURL(KMSRESTConstants.KEY_RESOURCE, name,
721        KMSRESTConstants.VERSIONS_SUB_RESOURCE, null);
722    HttpURLConnection conn = createConnection(url, HTTP_GET);
723    List response = call(conn, null, HttpURLConnection.HTTP_OK, List.class);
724    List<KeyVersion> versions = null;
725    if (!response.isEmpty()) {
726      versions = new ArrayList<KeyVersion>();
727      for (Object obj : response) {
728        versions.add(parseJSONKeyVersion((Map) obj));
729      }
730    }
731    return versions;
732  }
733
734  @Override
735  public Metadata getMetadata(String name) throws IOException {
736    checkNotEmpty(name, "name");
737    URL url = createURL(KMSRESTConstants.KEY_RESOURCE, name,
738        KMSRESTConstants.METADATA_SUB_RESOURCE, null);
739    HttpURLConnection conn = createConnection(url, HTTP_GET);
740    Map response = call(conn, null, HttpURLConnection.HTTP_OK, Map.class);
741    return parseJSONMetadata(response);
742  }
743
744  @Override
745  public void deleteKey(String name) throws IOException {
746    checkNotEmpty(name, "name");
747    URL url = createURL(KMSRESTConstants.KEY_RESOURCE, name, null, null);
748    HttpURLConnection conn = createConnection(url, HTTP_DELETE);
749    call(conn, null, HttpURLConnection.HTTP_OK, null);
750  }
751
752  @Override
753  public void flush() throws IOException {
754    // NOP
755    // the client does not keep any local state, thus flushing is not required
756    // because of the client.
757    // the server should not keep in memory state on behalf of clients either.
758  }
759
760  @Override
761  public void warmUpEncryptedKeys(String... keyNames)
762      throws IOException {
763    try {
764      encKeyVersionQueue.initializeQueuesForKeys(keyNames);
765    } catch (ExecutionException e) {
766      throw new IOException(e);
767    }
768  }
769
770  @Override
771  public void drain(String keyName) {
772    encKeyVersionQueue.drain(keyName);
773  }
774
775  @Override
776  public Token<?>[] addDelegationTokens(final String renewer,
777      Credentials credentials) throws IOException {
778    Token<?>[] tokens = null;
779    Text dtService = getDelegationTokenService();
780    Token<?> token = credentials.getToken(dtService);
781    if (token == null) {
782      final URL url = createURL(null, null, null, null);
783      final DelegationTokenAuthenticatedURL authUrl =
784          new DelegationTokenAuthenticatedURL(configurator);
785      try {
786        // 'actualUGI' is the UGI of the user creating the client 
787        // It is possible that the creator of the KMSClientProvier
788        // calls this method on behalf of a proxyUser (the doAsUser).
789        // In which case this call has to be made as the proxy user.
790        UserGroupInformation currentUgi = UserGroupInformation.getCurrentUser();
791        final String doAsUser = (currentUgi.getAuthenticationMethod() ==
792            UserGroupInformation.AuthenticationMethod.PROXY)
793                                ? currentUgi.getShortUserName() : null;
794
795        token = actualUgi.doAs(new PrivilegedExceptionAction<Token<?>>() {
796          @Override
797          public Token<?> run() throws Exception {
798            // Not using the cached token here.. Creating a new token here
799            // everytime.
800            return authUrl.getDelegationToken(url,
801                new DelegationTokenAuthenticatedURL.Token(), renewer, doAsUser);
802          }
803        });
804        if (token != null) {
805          credentials.addToken(token.getService(), token);
806          tokens = new Token<?>[] { token };
807        } else {
808          throw new IOException("Got NULL as delegation token");
809        }
810      } catch (InterruptedException e) {
811        Thread.currentThread().interrupt();
812      } catch (Exception e) {
813        throw new IOException(e);
814      }
815    }
816    return tokens;
817  }
818  
819  private Text getDelegationTokenService() throws IOException {
820    URL url = new URL(kmsUrl);
821    InetSocketAddress addr = new InetSocketAddress(url.getHost(),
822        url.getPort());
823    Text dtService = SecurityUtil.buildTokenService(addr);
824    return dtService;
825  }
826
827  /**
828   * Shutdown valueQueue executor threads
829   */
830  @Override
831  public void close() throws IOException {
832    try {
833      encKeyVersionQueue.shutdown();
834    } catch (Exception e) {
835      throw new IOException(e);
836    } finally {
837      if (sslFactory != null) {
838        sslFactory.destroy();
839      }
840    }
841  }
842}