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     */
018    package org.apache.hadoop.crypto.key.kms;
019    
020    import org.apache.commons.codec.binary.Base64;
021    import org.apache.hadoop.classification.InterfaceAudience;
022    import org.apache.hadoop.conf.Configuration;
023    import org.apache.hadoop.crypto.key.KeyProvider;
024    import org.apache.hadoop.crypto.key.KeyProviderCryptoExtension.EncryptedKeyVersion;
025    import org.apache.hadoop.crypto.key.KeyProviderDelegationTokenExtension;
026    import org.apache.hadoop.crypto.key.KeyProviderFactory;
027    import org.apache.hadoop.fs.CommonConfigurationKeysPublic;
028    import org.apache.hadoop.fs.Path;
029    import org.apache.hadoop.io.Text;
030    import org.apache.hadoop.security.Credentials;
031    import org.apache.hadoop.security.ProviderUtils;
032    import org.apache.hadoop.security.SecurityUtil;
033    import org.apache.hadoop.security.UserGroupInformation;
034    import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
035    import org.apache.hadoop.security.authentication.client.AuthenticationException;
036    import org.apache.hadoop.security.authentication.client.ConnectionConfigurator;
037    import org.apache.hadoop.security.ssl.SSLFactory;
038    import org.apache.hadoop.security.token.Token;
039    import org.apache.hadoop.security.token.delegation.web.DelegationTokenAuthenticatedURL;
040    import org.apache.hadoop.util.HttpExceptionUtils;
041    import org.apache.http.client.utils.URIBuilder;
042    import org.codehaus.jackson.map.ObjectMapper;
043    
044    import javax.net.ssl.HttpsURLConnection;
045    
046    import java.io.IOException;
047    import java.io.InputStream;
048    import java.io.OutputStream;
049    import java.io.OutputStreamWriter;
050    import java.io.Writer;
051    import java.lang.reflect.UndeclaredThrowableException;
052    import java.net.HttpURLConnection;
053    import java.net.InetSocketAddress;
054    import java.net.SocketTimeoutException;
055    import java.net.URI;
056    import java.net.URISyntaxException;
057    import java.net.URL;
058    import java.net.URLEncoder;
059    import java.security.GeneralSecurityException;
060    import java.security.NoSuchAlgorithmException;
061    import java.security.PrivilegedExceptionAction;
062    import java.util.ArrayList;
063    import java.util.Date;
064    import java.util.HashMap;
065    import java.util.LinkedList;
066    import java.util.List;
067    import java.util.Map;
068    import java.util.Queue;
069    import java.util.concurrent.ExecutionException;
070    
071    import org.apache.hadoop.crypto.key.KeyProviderCryptoExtension;
072    import org.apache.hadoop.crypto.key.KeyProviderCryptoExtension.CryptoExtension;
073    
074    import com.google.common.base.Preconditions;
075    
076    /**
077     * KMS client <code>KeyProvider</code> implementation.
078     */
079    @InterfaceAudience.Private
080    public 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(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          URL url = createURL(null, null, null, null);
783          DelegationTokenAuthenticatedURL authUrl =
784              new DelegationTokenAuthenticatedURL(configurator);
785          try {
786            token = authUrl.getDelegationToken(url, authToken, renewer);
787            if (token != null) {
788              credentials.addToken(token.getService(), token);
789              tokens = new Token<?>[] { token };
790            } else {
791              throw new IOException("Got NULL as delegation token");
792            }
793          } catch (AuthenticationException ex) {
794            throw new IOException(ex);
795          }
796        }
797        return tokens;
798      }
799      
800      private Text getDelegationTokenService() throws IOException {
801        URL url = new URL(kmsUrl);
802        InetSocketAddress addr = new InetSocketAddress(url.getHost(),
803            url.getPort());
804        Text dtService = SecurityUtil.buildTokenService(addr);
805        return dtService;
806      }
807    
808      /**
809       * Shutdown valueQueue executor threads
810       */
811      @Override
812      public void close() throws IOException {
813        try {
814          encKeyVersionQueue.shutdown();
815        } catch (Exception e) {
816          throw new IOException(e);
817        }
818      }
819    }