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 019package org.apache.hadoop.crypto.key; 020 021import java.io.ByteArrayInputStream; 022import java.io.ByteArrayOutputStream; 023import java.io.IOException; 024import java.io.InputStreamReader; 025import java.io.OutputStreamWriter; 026import java.security.NoSuchAlgorithmException; 027import java.util.Collections; 028import java.util.Date; 029import java.util.HashMap; 030import java.util.List; 031import java.util.Map; 032 033import com.google.gson.stream.JsonReader; 034import com.google.gson.stream.JsonWriter; 035import org.apache.hadoop.classification.InterfaceAudience; 036import org.apache.hadoop.classification.InterfaceStability; 037import org.apache.hadoop.conf.Configuration; 038 039import javax.crypto.KeyGenerator; 040 041/** 042 * A provider of secret key material for Hadoop applications. Provides an 043 * abstraction to separate key storage from users of encryption. It 044 * is intended to support getting or storing keys in a variety of ways, 045 * including third party bindings. 046 * <P/> 047 * <code>KeyProvider</code> implementations must be thread safe. 048 */ 049@InterfaceAudience.Public 050@InterfaceStability.Unstable 051public abstract class KeyProvider { 052 public static final String DEFAULT_CIPHER_NAME = 053 "hadoop.security.key.default.cipher"; 054 public static final String DEFAULT_CIPHER = "AES/CTR/NoPadding"; 055 public static final String DEFAULT_BITLENGTH_NAME = 056 "hadoop.security.key.default.bitlength"; 057 public static final int DEFAULT_BITLENGTH = 128; 058 059 private final Configuration conf; 060 061 /** 062 * The combination of both the key version name and the key material. 063 */ 064 public static class KeyVersion { 065 private final String name; 066 private final String versionName; 067 private final byte[] material; 068 069 protected KeyVersion(String name, String versionName, 070 byte[] material) { 071 this.name = name; 072 this.versionName = versionName; 073 this.material = material; 074 } 075 076 public String getName() { 077 return name; 078 } 079 080 public String getVersionName() { 081 return versionName; 082 } 083 084 public byte[] getMaterial() { 085 return material; 086 } 087 088 public String toString() { 089 StringBuilder buf = new StringBuilder(); 090 buf.append("key("); 091 buf.append(versionName); 092 buf.append(")="); 093 if (material == null) { 094 buf.append("null"); 095 } else { 096 for(byte b: material) { 097 buf.append(' '); 098 int right = b & 0xff; 099 if (right < 0x10) { 100 buf.append('0'); 101 } 102 buf.append(Integer.toHexString(right)); 103 } 104 } 105 return buf.toString(); 106 } 107 } 108 109 /** 110 * Key metadata that is associated with the key. 111 */ 112 public static class Metadata { 113 private final static String CIPHER_FIELD = "cipher"; 114 private final static String BIT_LENGTH_FIELD = "bitLength"; 115 private final static String CREATED_FIELD = "created"; 116 private final static String DESCRIPTION_FIELD = "description"; 117 private final static String VERSIONS_FIELD = "versions"; 118 private final static String ATTRIBUTES_FIELD = "attributes"; 119 120 private final String cipher; 121 private final int bitLength; 122 private final String description; 123 private final Date created; 124 private int versions; 125 private Map<String, String> attributes; 126 127 protected Metadata(String cipher, int bitLength, String description, 128 Map<String, String> attributes, Date created, int versions) { 129 this.cipher = cipher; 130 this.bitLength = bitLength; 131 this.description = description; 132 this.attributes = (attributes == null || attributes.isEmpty()) 133 ? null : attributes; 134 this.created = created; 135 this.versions = versions; 136 } 137 138 public String toString() { 139 final StringBuilder metaSB = new StringBuilder(); 140 metaSB.append("cipher: ").append(cipher).append(", "); 141 metaSB.append("length: ").append(bitLength).append(", "); 142 metaSB.append("description: ").append(description).append(", "); 143 metaSB.append("created: ").append(created).append(", "); 144 metaSB.append("version: ").append(versions).append(", "); 145 metaSB.append("attributes: "); 146 if ((attributes != null) && !attributes.isEmpty()) { 147 for (Map.Entry<String, String> attribute : attributes.entrySet()) { 148 metaSB.append("["); 149 metaSB.append(attribute.getKey()); 150 metaSB.append("="); 151 metaSB.append(attribute.getValue()); 152 metaSB.append("], "); 153 } 154 metaSB.deleteCharAt(metaSB.length() - 2); // remove last ', ' 155 } else { 156 metaSB.append("null"); 157 } 158 return metaSB.toString(); 159 } 160 161 public String getDescription() { 162 return description; 163 } 164 165 public Date getCreated() { 166 return created; 167 } 168 169 public String getCipher() { 170 return cipher; 171 } 172 173 @SuppressWarnings("unchecked") 174 public Map<String, String> getAttributes() { 175 return (attributes == null) ? Collections.EMPTY_MAP : attributes; 176 } 177 178 /** 179 * Get the algorithm from the cipher. 180 * @return the algorithm name 181 */ 182 public String getAlgorithm() { 183 int slash = cipher.indexOf('/'); 184 if (slash == - 1) { 185 return cipher; 186 } else { 187 return cipher.substring(0, slash); 188 } 189 } 190 191 public int getBitLength() { 192 return bitLength; 193 } 194 195 public int getVersions() { 196 return versions; 197 } 198 199 protected int addVersion() { 200 return versions++; 201 } 202 203 /** 204 * Serialize the metadata to a set of bytes. 205 * @return the serialized bytes 206 * @throws IOException 207 */ 208 protected byte[] serialize() throws IOException { 209 ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 210 JsonWriter writer = new JsonWriter(new OutputStreamWriter(buffer)); 211 try { 212 writer.beginObject(); 213 if (cipher != null) { 214 writer.name(CIPHER_FIELD).value(cipher); 215 } 216 if (bitLength != 0) { 217 writer.name(BIT_LENGTH_FIELD).value(bitLength); 218 } 219 if (created != null) { 220 writer.name(CREATED_FIELD).value(created.getTime()); 221 } 222 if (description != null) { 223 writer.name(DESCRIPTION_FIELD).value(description); 224 } 225 if (attributes != null && attributes.size() > 0) { 226 writer.name(ATTRIBUTES_FIELD).beginObject(); 227 for (Map.Entry<String, String> attribute : attributes.entrySet()) { 228 writer.name(attribute.getKey()).value(attribute.getValue()); 229 } 230 writer.endObject(); 231 } 232 writer.name(VERSIONS_FIELD).value(versions); 233 writer.endObject(); 234 writer.flush(); 235 } finally { 236 writer.close(); 237 } 238 return buffer.toByteArray(); 239 } 240 241 /** 242 * Deserialize a new metadata object from a set of bytes. 243 * @param bytes the serialized metadata 244 * @throws IOException 245 */ 246 protected Metadata(byte[] bytes) throws IOException { 247 String cipher = null; 248 int bitLength = 0; 249 Date created = null; 250 int versions = 0; 251 String description = null; 252 Map<String, String> attributes = null; 253 JsonReader reader = new JsonReader(new InputStreamReader 254 (new ByteArrayInputStream(bytes))); 255 try { 256 reader.beginObject(); 257 while (reader.hasNext()) { 258 String field = reader.nextName(); 259 if (CIPHER_FIELD.equals(field)) { 260 cipher = reader.nextString(); 261 } else if (BIT_LENGTH_FIELD.equals(field)) { 262 bitLength = reader.nextInt(); 263 } else if (CREATED_FIELD.equals(field)) { 264 created = new Date(reader.nextLong()); 265 } else if (VERSIONS_FIELD.equals(field)) { 266 versions = reader.nextInt(); 267 } else if (DESCRIPTION_FIELD.equals(field)) { 268 description = reader.nextString(); 269 } else if (ATTRIBUTES_FIELD.equalsIgnoreCase(field)) { 270 reader.beginObject(); 271 attributes = new HashMap<String, String>(); 272 while (reader.hasNext()) { 273 attributes.put(reader.nextName(), reader.nextString()); 274 } 275 reader.endObject(); 276 } 277 } 278 reader.endObject(); 279 } finally { 280 reader.close(); 281 } 282 this.cipher = cipher; 283 this.bitLength = bitLength; 284 this.created = created; 285 this.description = description; 286 this.attributes = attributes; 287 this.versions = versions; 288 } 289 } 290 291 /** 292 * Options when creating key objects. 293 */ 294 public static class Options { 295 private String cipher; 296 private int bitLength; 297 private String description; 298 private Map<String, String> attributes; 299 300 public Options(Configuration conf) { 301 cipher = conf.get(DEFAULT_CIPHER_NAME, DEFAULT_CIPHER); 302 bitLength = conf.getInt(DEFAULT_BITLENGTH_NAME, DEFAULT_BITLENGTH); 303 } 304 305 public Options setCipher(String cipher) { 306 this.cipher = cipher; 307 return this; 308 } 309 310 public Options setBitLength(int bitLength) { 311 this.bitLength = bitLength; 312 return this; 313 } 314 315 public Options setDescription(String description) { 316 this.description = description; 317 return this; 318 } 319 320 public Options setAttributes(Map<String, String> attributes) { 321 if (attributes != null) { 322 if (attributes.containsKey(null)) { 323 throw new IllegalArgumentException("attributes cannot have a NULL key"); 324 } 325 this.attributes = new HashMap<String, String>(attributes); 326 } 327 return this; 328 } 329 330 public String getCipher() { 331 return cipher; 332 } 333 334 public int getBitLength() { 335 return bitLength; 336 } 337 338 public String getDescription() { 339 return description; 340 } 341 342 @SuppressWarnings("unchecked") 343 public Map<String, String> getAttributes() { 344 return (attributes == null) ? Collections.EMPTY_MAP : attributes; 345 } 346 347 @Override 348 public String toString() { 349 return "Options{" + 350 "cipher='" + cipher + '\'' + 351 ", bitLength=" + bitLength + 352 ", description='" + description + '\'' + 353 ", attributes=" + attributes + 354 '}'; 355 } 356 } 357 358 /** 359 * Constructor. 360 * 361 * @param conf configuration for the provider 362 */ 363 public KeyProvider(Configuration conf) { 364 this.conf = new Configuration(conf); 365 } 366 367 /** 368 * Return the provider configuration. 369 * 370 * @return the provider configuration 371 */ 372 public Configuration getConf() { 373 return conf; 374 } 375 376 /** 377 * A helper function to create an options object. 378 * @param conf the configuration to use 379 * @return a new options object 380 */ 381 public static Options options(Configuration conf) { 382 return new Options(conf); 383 } 384 385 /** 386 * Indicates whether this provider represents a store 387 * that is intended for transient use - such as the UserProvider 388 * is. These providers are generally used to provide access to 389 * keying material rather than for long term storage. 390 * @return true if transient, false otherwise 391 */ 392 public boolean isTransient() { 393 return false; 394 } 395 396 /** 397 * Get the key material for a specific version of the key. This method is used 398 * when decrypting data. 399 * @param versionName the name of a specific version of the key 400 * @return the key material 401 * @throws IOException 402 */ 403 public abstract KeyVersion getKeyVersion(String versionName 404 ) throws IOException; 405 406 /** 407 * Get the key names for all keys. 408 * @return the list of key names 409 * @throws IOException 410 */ 411 public abstract List<String> getKeys() throws IOException; 412 413 /** 414 * Get key metadata in bulk. 415 * @param names the names of the keys to get 416 * @throws IOException 417 */ 418 public Metadata[] getKeysMetadata(String... names) throws IOException { 419 Metadata[] result = new Metadata[names.length]; 420 for (int i=0; i < names.length; ++i) { 421 result[i] = getMetadata(names[i]); 422 } 423 return result; 424 } 425 426 /** 427 * Get the key material for all versions of a specific key name. 428 * @return the list of key material 429 * @throws IOException 430 */ 431 public abstract List<KeyVersion> getKeyVersions(String name) throws IOException; 432 433 /** 434 * Get the current version of the key, which should be used for encrypting new 435 * data. 436 * @param name the base name of the key 437 * @return the version name of the current version of the key or null if the 438 * key version doesn't exist 439 * @throws IOException 440 */ 441 public KeyVersion getCurrentKey(String name) throws IOException { 442 Metadata meta = getMetadata(name); 443 if (meta == null) { 444 return null; 445 } 446 return getKeyVersion(buildVersionName(name, meta.getVersions() - 1)); 447 } 448 449 /** 450 * Get metadata about the key. 451 * @param name the basename of the key 452 * @return the key's metadata or null if the key doesn't exist 453 * @throws IOException 454 */ 455 public abstract Metadata getMetadata(String name) throws IOException; 456 457 /** 458 * Create a new key. The given key must not already exist. 459 * @param name the base name of the key 460 * @param material the key material for the first version of the key. 461 * @param options the options for the new key. 462 * @return the version name of the first version of the key. 463 * @throws IOException 464 */ 465 public abstract KeyVersion createKey(String name, byte[] material, 466 Options options) throws IOException; 467 468 /** 469 * Get the algorithm from the cipher. 470 * 471 * @return the algorithm name 472 */ 473 private String getAlgorithm(String cipher) { 474 int slash = cipher.indexOf('/'); 475 if (slash == -1) { 476 return cipher; 477 } else { 478 return cipher.substring(0, slash); 479 } 480 } 481 482 /** 483 * Generates a key material. 484 * 485 * @param size length of the key. 486 * @param algorithm algorithm to use for generating the key. 487 * @return the generated key. 488 * @throws NoSuchAlgorithmException 489 */ 490 protected byte[] generateKey(int size, String algorithm) 491 throws NoSuchAlgorithmException { 492 algorithm = getAlgorithm(algorithm); 493 KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm); 494 keyGenerator.init(size); 495 byte[] key = keyGenerator.generateKey().getEncoded(); 496 return key; 497 } 498 499 /** 500 * Create a new key generating the material for it. 501 * The given key must not already exist. 502 * <p/> 503 * This implementation generates the key material and calls the 504 * {@link #createKey(String, byte[], Options)} method. 505 * 506 * @param name the base name of the key 507 * @param options the options for the new key. 508 * @return the version name of the first version of the key. 509 * @throws IOException 510 * @throws NoSuchAlgorithmException 511 */ 512 public KeyVersion createKey(String name, Options options) 513 throws NoSuchAlgorithmException, IOException { 514 byte[] material = generateKey(options.getBitLength(), options.getCipher()); 515 return createKey(name, material, options); 516 } 517 518 /** 519 * Delete the given key. 520 * @param name the name of the key to delete 521 * @throws IOException 522 */ 523 public abstract void deleteKey(String name) throws IOException; 524 525 /** 526 * Roll a new version of the given key. 527 * @param name the basename of the key 528 * @param material the new key material 529 * @return the name of the new version of the key 530 * @throws IOException 531 */ 532 public abstract KeyVersion rollNewVersion(String name, 533 byte[] material 534 ) throws IOException; 535 536 /** 537 * Can be used by implementing classes to close any resources 538 * that require closing 539 */ 540 public void close() throws IOException { 541 // NOP 542 } 543 544 /** 545 * Roll a new version of the given key generating the material for it. 546 * <p/> 547 * This implementation generates the key material and calls the 548 * {@link #rollNewVersion(String, byte[])} method. 549 * 550 * @param name the basename of the key 551 * @return the name of the new version of the key 552 * @throws IOException 553 */ 554 public KeyVersion rollNewVersion(String name) throws NoSuchAlgorithmException, 555 IOException { 556 Metadata meta = getMetadata(name); 557 byte[] material = generateKey(meta.getBitLength(), meta.getCipher()); 558 return rollNewVersion(name, material); 559 } 560 561 /** 562 * Ensures that any changes to the keys are written to persistent store. 563 * @throws IOException 564 */ 565 public abstract void flush() throws IOException; 566 567 /** 568 * Split the versionName in to a base name. Converts "/aaa/bbb/3" to 569 * "/aaa/bbb". 570 * @param versionName the version name to split 571 * @return the base name of the key 572 * @throws IOException 573 */ 574 public static String getBaseName(String versionName) throws IOException { 575 int div = versionName.lastIndexOf('@'); 576 if (div == -1) { 577 throw new IOException("No version in key path " + versionName); 578 } 579 return versionName.substring(0, div); 580 } 581 582 /** 583 * Build a version string from a basename and version number. Converts 584 * "/aaa/bbb" and 3 to "/aaa/bbb@3". 585 * @param name the basename of the key 586 * @param version the version of the key 587 * @return the versionName of the key. 588 */ 589 protected static String buildVersionName(String name, int version) { 590 return name + "@" + version; 591 } 592 593 /** 594 * Find the provider with the given key. 595 * @param providerList the list of providers 596 * @param keyName the key name we are looking for 597 * @return the KeyProvider that has the key 598 */ 599 public static KeyProvider findProvider(List<KeyProvider> providerList, 600 String keyName) throws IOException { 601 for(KeyProvider provider: providerList) { 602 if (provider.getMetadata(keyName) != null) { 603 return provider; 604 } 605 } 606 throw new IOException("Can't find KeyProvider for key " + keyName); 607 } 608}