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