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