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