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}