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}