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