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