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.IOException; 022import java.io.PrintStream; 023import java.security.InvalidParameterException; 024import java.security.NoSuchAlgorithmException; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028 029import com.google.common.annotations.VisibleForTesting; 030import org.apache.hadoop.conf.Configuration; 031import org.apache.hadoop.conf.Configured; 032import org.apache.hadoop.crypto.key.KeyProvider.Metadata; 033import org.apache.hadoop.crypto.key.KeyProvider.Options; 034import org.apache.hadoop.util.Tool; 035import org.apache.hadoop.util.ToolRunner; 036 037/** 038 * This program is the CLI utility for the KeyProvider facilities in Hadoop. 039 */ 040public class KeyShell extends Configured implements Tool { 041 final static private String USAGE_PREFIX = "Usage: hadoop key " + 042 "[generic options]\n"; 043 final static private String COMMANDS = 044 " [-help]\n" + 045 " [" + CreateCommand.USAGE + "]\n" + 046 " [" + RollCommand.USAGE + "]\n" + 047 " [" + DeleteCommand.USAGE + "]\n" + 048 " [" + ListCommand.USAGE + "]\n"; 049 private static final String LIST_METADATA = "keyShell.list.metadata"; 050 @VisibleForTesting 051 public static final String NO_VALID_PROVIDERS = 052 "There are no valid (non-transient) providers configured.\n" + 053 "No action has been taken. Use the -provider option to specify\n" + 054 "a provider. If you want to use a transient provider then you\n" + 055 "MUST use the -provider argument."; 056 057 private boolean interactive = true; 058 private Command command = null; 059 060 /** If true, fail if the provider requires a password and none is given. */ 061 private boolean strict = false; 062 063 /** allows stdout to be captured if necessary. */ 064 @VisibleForTesting 065 public PrintStream out = System.out; 066 /** allows stderr to be captured if necessary. */ 067 @VisibleForTesting 068 public PrintStream err = System.err; 069 070 private boolean userSuppliedProvider = false; 071 072 /** 073 * Primary entry point for the KeyShell; called via main(). 074 * 075 * @param args Command line arguments. 076 * @return 0 on success and 1 on failure. This value is passed back to 077 * the unix shell, so we must follow shell return code conventions: 078 * the return code is an unsigned character, and 0 means success, and 079 * small positive integers mean failure. 080 * @throws Exception 081 */ 082 @Override 083 public int run(String[] args) throws Exception { 084 int exitCode = 0; 085 try { 086 exitCode = init(args); 087 if (exitCode != 0) { 088 return exitCode; 089 } 090 if (command.validate()) { 091 command.execute(); 092 } else { 093 exitCode = 1; 094 } 095 } catch (Exception e) { 096 e.printStackTrace(err); 097 return 1; 098 } 099 return exitCode; 100 } 101 102 /** 103 * Parse the command line arguments and initialize the data. 104 * <pre> 105 * % hadoop key create keyName [-size size] [-cipher algorithm] 106 * [-provider providerPath] 107 * % hadoop key roll keyName [-provider providerPath] 108 * % hadoop key list [-provider providerPath] 109 * % hadoop key delete keyName [-provider providerPath] [-i] 110 * </pre> 111 * @param args Command line arguments. 112 * @return 0 on success, 1 on failure. 113 * @throws IOException 114 */ 115 private int init(String[] args) throws IOException { 116 final Options options = KeyProvider.options(getConf()); 117 final Map<String, String> attributes = new HashMap<String, String>(); 118 119 for (int i = 0; i < args.length; i++) { // parse command line 120 boolean moreTokens = (i < args.length - 1); 121 if (args[i].equals("create")) { 122 String keyName = "-help"; 123 if (moreTokens) { 124 keyName = args[++i]; 125 } 126 127 command = new CreateCommand(keyName, options); 128 if ("-help".equals(keyName)) { 129 printKeyShellUsage(); 130 return 1; 131 } 132 } else if (args[i].equals("delete")) { 133 String keyName = "-help"; 134 if (moreTokens) { 135 keyName = args[++i]; 136 } 137 138 command = new DeleteCommand(keyName); 139 if ("-help".equals(keyName)) { 140 printKeyShellUsage(); 141 return 1; 142 } 143 } else if (args[i].equals("roll")) { 144 String keyName = "-help"; 145 if (moreTokens) { 146 keyName = args[++i]; 147 } 148 149 command = new RollCommand(keyName); 150 if ("-help".equals(keyName)) { 151 printKeyShellUsage(); 152 return 1; 153 } 154 } else if ("list".equals(args[i])) { 155 command = new ListCommand(); 156 } else if ("-size".equals(args[i]) && moreTokens) { 157 options.setBitLength(Integer.parseInt(args[++i])); 158 } else if ("-cipher".equals(args[i]) && moreTokens) { 159 options.setCipher(args[++i]); 160 } else if ("-description".equals(args[i]) && moreTokens) { 161 options.setDescription(args[++i]); 162 } else if ("-attr".equals(args[i]) && moreTokens) { 163 final String attrval[] = args[++i].split("=", 2); 164 final String attr = attrval[0].trim(); 165 final String val = attrval[1].trim(); 166 if (attr.isEmpty() || val.isEmpty()) { 167 out.println("\nAttributes must be in attribute=value form, " + 168 "or quoted\nlike \"attribute = value\"\n"); 169 printKeyShellUsage(); 170 return 1; 171 } 172 if (attributes.containsKey(attr)) { 173 out.println("\nEach attribute must correspond to only one value:\n" + 174 "atttribute \"" + attr + "\" was repeated\n" ); 175 printKeyShellUsage(); 176 return 1; 177 } 178 attributes.put(attr, val); 179 } else if ("-provider".equals(args[i]) && moreTokens) { 180 userSuppliedProvider = true; 181 getConf().set(KeyProviderFactory.KEY_PROVIDER_PATH, args[++i]); 182 } else if ("-metadata".equals(args[i])) { 183 getConf().setBoolean(LIST_METADATA, true); 184 } else if ("-f".equals(args[i]) || ("-force".equals(args[i]))) { 185 interactive = false; 186 } else if (args[i].equals("-strict")) { 187 strict = true; 188 } else if ("-help".equals(args[i])) { 189 printKeyShellUsage(); 190 return 1; 191 } else { 192 printKeyShellUsage(); 193 ToolRunner.printGenericCommandUsage(System.err); 194 return 1; 195 } 196 } 197 198 if (command == null) { 199 printKeyShellUsage(); 200 return 1; 201 } 202 203 if (!attributes.isEmpty()) { 204 options.setAttributes(attributes); 205 } 206 207 return 0; 208 } 209 210 private void printKeyShellUsage() { 211 out.println(USAGE_PREFIX + COMMANDS); 212 if (command != null) { 213 out.println(command.getUsage()); 214 } else { 215 out.println("=========================================================" + 216 "======"); 217 out.println(CreateCommand.USAGE + ":\n\n" + CreateCommand.DESC); 218 out.println("=========================================================" + 219 "======"); 220 out.println(RollCommand.USAGE + ":\n\n" + RollCommand.DESC); 221 out.println("=========================================================" + 222 "======"); 223 out.println(DeleteCommand.USAGE + ":\n\n" + DeleteCommand.DESC); 224 out.println("=========================================================" + 225 "======"); 226 out.println(ListCommand.USAGE + ":\n\n" + ListCommand.DESC); 227 } 228 } 229 230 private abstract class Command { 231 protected KeyProvider provider = null; 232 233 public boolean validate() { 234 return true; 235 } 236 237 protected KeyProvider getKeyProvider() { 238 KeyProvider prov = null; 239 List<KeyProvider> providers; 240 try { 241 providers = KeyProviderFactory.getProviders(getConf()); 242 if (userSuppliedProvider) { 243 prov = providers.get(0); 244 } else { 245 for (KeyProvider p : providers) { 246 if (!p.isTransient()) { 247 prov = p; 248 break; 249 } 250 } 251 } 252 } catch (IOException e) { 253 e.printStackTrace(err); 254 } 255 if (prov == null) { 256 out.println(NO_VALID_PROVIDERS); 257 } 258 return prov; 259 } 260 261 protected void printProviderWritten() { 262 out.println(provider + " has been updated."); 263 } 264 265 protected void warnIfTransientProvider() { 266 if (provider.isTransient()) { 267 out.println("WARNING: you are modifying a transient provider."); 268 } 269 } 270 271 public abstract void execute() throws Exception; 272 273 public abstract String getUsage(); 274 } 275 276 private class ListCommand extends Command { 277 public static final String USAGE = 278 "list [-provider <provider>] [-strict] [-metadata] [-help]"; 279 public static final String DESC = 280 "The list subcommand displays the keynames contained within\n" + 281 "a particular provider as configured in core-site.xml or\n" + 282 "specified with the -provider argument. -metadata displays\n" + 283 "the metadata. If -strict is supplied, fail immediately if\n" + 284 "the provider requires a password and none is given."; 285 286 private boolean metadata = false; 287 288 public boolean validate() { 289 boolean rc = true; 290 provider = getKeyProvider(); 291 if (provider == null) { 292 rc = false; 293 } 294 metadata = getConf().getBoolean(LIST_METADATA, false); 295 return rc; 296 } 297 298 public void execute() throws IOException { 299 try { 300 final List<String> keys = provider.getKeys(); 301 out.println("Listing keys for KeyProvider: " + provider); 302 if (metadata) { 303 final Metadata[] meta = 304 provider.getKeysMetadata(keys.toArray(new String[keys.size()])); 305 for (int i = 0; i < meta.length; ++i) { 306 out.println(keys.get(i) + " : " + meta[i]); 307 } 308 } else { 309 for (String keyName : keys) { 310 out.println(keyName); 311 } 312 } 313 } catch (IOException e) { 314 out.println("Cannot list keys for KeyProvider: " + provider 315 + ": " + e.toString()); 316 throw e; 317 } 318 } 319 320 @Override 321 public String getUsage() { 322 return USAGE + ":\n\n" + DESC; 323 } 324 } 325 326 private class RollCommand extends Command { 327 public static final String USAGE = 328 "roll <keyname> [-provider <provider>] [-strict] [-help]"; 329 public static final String DESC = 330 "The roll subcommand creates a new version for the specified key\n" + 331 "within the provider indicated using the -provider argument.\n" + 332 "If -strict is supplied, fail immediately if the provider requires\n" + 333 "a password and none is given."; 334 335 private String keyName = null; 336 337 public RollCommand(String keyName) { 338 this.keyName = keyName; 339 } 340 341 public boolean validate() { 342 boolean rc = true; 343 provider = getKeyProvider(); 344 if (provider == null) { 345 rc = false; 346 } 347 if (keyName == null) { 348 out.println("Please provide a <keyname>.\n" + 349 "See the usage description by using -help."); 350 rc = false; 351 } 352 return rc; 353 } 354 355 public void execute() throws NoSuchAlgorithmException, IOException { 356 try { 357 warnIfTransientProvider(); 358 out.println("Rolling key version from KeyProvider: " 359 + provider + "\n for key name: " + keyName); 360 try { 361 provider.rollNewVersion(keyName); 362 provider.flush(); 363 out.println(keyName + " has been successfully rolled."); 364 printProviderWritten(); 365 } catch (NoSuchAlgorithmException e) { 366 out.println("Cannot roll key: " + keyName + " within KeyProvider: " 367 + provider + ". " + e.toString()); 368 throw e; 369 } 370 } catch (IOException e1) { 371 out.println("Cannot roll key: " + keyName + " within KeyProvider: " 372 + provider + ". " + e1.toString()); 373 throw e1; 374 } 375 } 376 377 @Override 378 public String getUsage() { 379 return USAGE + ":\n\n" + DESC; 380 } 381 } 382 383 private class DeleteCommand extends Command { 384 public static final String USAGE = 385 "delete <keyname> [-provider <provider>] [-strict] [-f] [-help]"; 386 public static final String DESC = 387 "The delete subcommand deletes all versions of the key\n" + 388 "specified by the <keyname> argument from within the\n" + 389 "provider specified by -provider. The command asks for\n" + 390 "user confirmation unless -f is specified. If -strict is\n" + 391 "supplied, fail immediately if the provider requires a\n" + 392 "password and none is given."; 393 394 private String keyName = null; 395 private boolean cont = true; 396 397 public DeleteCommand(String keyName) { 398 this.keyName = keyName; 399 } 400 401 @Override 402 public boolean validate() { 403 provider = getKeyProvider(); 404 if (provider == null) { 405 return false; 406 } 407 if (keyName == null) { 408 out.println("There is no keyName specified. Please specify a " + 409 "<keyname>. See the usage description with -help."); 410 return false; 411 } 412 if (interactive) { 413 try { 414 cont = ToolRunner 415 .confirmPrompt("You are about to DELETE all versions of " 416 + " key " + keyName + " from KeyProvider " 417 + provider + ". Continue? "); 418 if (!cont) { 419 out.println(keyName + " has not been deleted."); 420 } 421 return cont; 422 } catch (IOException e) { 423 out.println(keyName + " will not be deleted."); 424 e.printStackTrace(err); 425 } 426 } 427 return true; 428 } 429 430 public void execute() throws IOException { 431 warnIfTransientProvider(); 432 out.println("Deleting key: " + keyName + " from KeyProvider: " 433 + provider); 434 if (cont) { 435 try { 436 provider.deleteKey(keyName); 437 provider.flush(); 438 out.println(keyName + " has been successfully deleted."); 439 printProviderWritten(); 440 } catch (IOException e) { 441 out.println(keyName + " has not been deleted. " + e.toString()); 442 throw e; 443 } 444 } 445 } 446 447 @Override 448 public String getUsage() { 449 return USAGE + ":\n\n" + DESC; 450 } 451 } 452 453 private class CreateCommand extends Command { 454 public static final String USAGE = 455 "create <keyname> [-cipher <cipher>] [-size <size>]\n" + 456 " [-description <description>]\n" + 457 " [-attr <attribute=value>]\n" + 458 " [-provider <provider>] [-strict]\n" + 459 " [-help]"; 460 public static final String DESC = 461 "The create subcommand creates a new key for the name specified\n" + 462 "by the <keyname> argument within the provider specified by the\n" + 463 "-provider argument. You may specify a cipher with the -cipher\n" + 464 "argument. The default cipher is currently \"AES/CTR/NoPadding\".\n" + 465 "The default keysize is 128. You may specify the requested key\n" + 466 "length using the -size argument. Arbitrary attribute=value\n" + 467 "style attributes may be specified using the -attr argument.\n" + 468 "-attr may be specified multiple times, once per attribute.\n"; 469 470 private final String keyName; 471 private final Options options; 472 473 public CreateCommand(String keyName, Options options) { 474 this.keyName = keyName; 475 this.options = options; 476 } 477 478 public boolean validate() { 479 boolean rc = true; 480 try { 481 provider = getKeyProvider(); 482 if (provider == null) { 483 rc = false; 484 } else if (provider.needsPassword()) { 485 if (strict) { 486 out.println(provider.noPasswordError()); 487 rc = false; 488 } else { 489 out.println(provider.noPasswordWarning()); 490 } 491 } 492 } catch (IOException e) { 493 e.printStackTrace(err); 494 } 495 if (keyName == null) { 496 out.println("Please provide a <keyname>. See the usage description" + 497 " with -help."); 498 rc = false; 499 } 500 return rc; 501 } 502 503 public void execute() throws IOException, NoSuchAlgorithmException { 504 warnIfTransientProvider(); 505 try { 506 provider.createKey(keyName, options); 507 provider.flush(); 508 out.println(keyName + " has been successfully created with options " 509 + options.toString() + "."); 510 printProviderWritten(); 511 } catch (InvalidParameterException e) { 512 out.println(keyName + " has not been created. " + e.toString()); 513 throw e; 514 } catch (IOException e) { 515 out.println(keyName + " has not been created. " + e.toString()); 516 throw e; 517 } catch (NoSuchAlgorithmException e) { 518 out.println(keyName + " has not been created. " + e.toString()); 519 throw e; 520 } 521 } 522 523 @Override 524 public String getUsage() { 525 return USAGE + ":\n\n" + DESC; 526 } 527 } 528 529 /** 530 * main() entry point for the KeyShell. While strictly speaking the 531 * return is void, it will System.exit() with a return code: 0 is for 532 * success and 1 for failure. 533 * 534 * @param args Command line arguments. 535 * @throws Exception 536 */ 537 public static void main(String[] args) throws Exception { 538 int res = ToolRunner.run(new Configuration(), new KeyShell(), args); 539 System.exit(res); 540 } 541}