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 }