/*
 * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

/*
 *  (C) Copyright IBM Corp. 1999 All Rights Reserved.
 *  Copyright 1997 The Open Group Research Institute.  All rights reserved.
 */

package sun.security.krb5.internal.tools;

import sun.security.krb5.*;
import sun.security.krb5.internal.ktab.*;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Serial;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Locale;
import sun.security.krb5.internal.crypto.EType;
/**
 * This class can execute as a command-line tool to help the user manage
 * entries in the key table.
 * Available functions include list/add/update/delete service key(s).
 *
 * @author Yanni Zhang
 * @author Ram Marti
 */

public class Ktab {
    // KeyTabAdmin admin;
    KeyTab table;
    char action;
    String name;   // name and directory of key table
    String principal;
    boolean showEType;
    boolean showTime;
    int etype = -1;
    char[] password = null;

    boolean fopt = false;   // true if delete without prompt or
                            // add by contacting KDC. Default false
    boolean append = false; // true if new keys are appended. Default false
    int vDel = -1;          // kvno to delete, -1 all, -2 old. Default -1
    int vAdd = -1;          // kvno to add. Default -1, means auto incremented
    String salt = null;     // salt to use. Default null, means default salt

    /**
     * The main program that can be invoked at command line.
     * See {@link #printHelp} for usages.
     */
    public static void main(String[] args) {
        Ktab ktab = new Ktab();
        int exitCode = ktab.run(args);
        if (exitCode != 0) {
            System.exit(exitCode);
        }
    }

    private static class ExitException extends RuntimeException {
        @Serial
        private static final long serialVersionUID = 0L;
        private final int errorCode;
        public ExitException(int errorCode) {
            this.errorCode = errorCode;
        }
    }

    public int run(String[] args) {
        try {
            run0(args);
            return 0;
        } catch (ExitException ee) {
            return ee.errorCode;
        }
    }

    private void run0(String[] args) throws ExitException {
        if ((args.length == 1) &&
            ((args[0].equalsIgnoreCase("-?")) ||
             (args[0].equalsIgnoreCase("-h")) ||
             (args[0].equalsIgnoreCase("--help")) ||
             // -help: legacy.
             (args[0].equalsIgnoreCase("-help")))) {
            printHelp();
            return;
        } else if ((args == null) || (args.length == 0)) {
            action = 'l';
        } else {
            processArgs(args);
        }
        table = KeyTab.getInstance(name);
        if (table.isMissing() && action != 'a') {
            if (name == null) {
                System.out.println("No default key table exists.");
            } else {
                System.out.println("Key table " +
                        name + " does not exist.");
            }
            throw new ExitException(-1);
        }
        if (!table.isValid()) {
            if (name == null) {
                System.out.println("The format of the default key table " +
                        " is incorrect.");
            } else {
                System.out.println("The format of key table " +
                        name + " is incorrect.");
            }
            throw new ExitException(-1);
        }
        switch (action) {
        case 'l':
            listKt();
            break;
        case 'a':
            addEntry();
            break;
        case 'd':
            deleteEntry();
            break;
        default:
            error("A command must be provided");
        }
    }

    /**
     * Parses the command line arguments.
     */
    void processArgs(String[] args) {

        // Commands (should appear before options):
        //   -l
        //   -a <princ>
        //   -d <princ>
        // Options:
        //   -e <etype> (for -d)
        //   -e (for -l)
        //   -n <kvno>
        //   -k <keytab>
        //   -t
        //   -f
        //   -append
        // Optional extra arguments:
        //   password for -a
        //   [kvno|all|old] for -d

        boolean argAlreadyAppeared = false;
        for (int i = 0; i < args.length; i++) {
            if (args[i].startsWith("-")) {
                switch (args[i].toLowerCase(Locale.US)) {

                    // Commands
                    case "-l":   // list
                        action = 'l';
                        break;
                    case "-a":   // add a new entry to keytab.
                        action = 'a';
                        if (++i >= args.length || args[i].startsWith("-")) {
                            error("A principal name must be specified after -a");
                        }
                        principal = args[i];
                        break;
                    case "-d":   // delete entries
                        action = 'd';
                        if (++i >= args.length || args[i].startsWith("-")) {
                            error("A principal name must be specified after -d");
                        }
                        principal = args[i];
                        break;

                        // Options
                    case "-e":
                        if (action == 'l') {    // list etypes
                            showEType = true;
                        } else if (action == 'd') { // delete etypes
                            if (++i >= args.length || args[i].startsWith("-")) {
                                error("An etype must be specified after -e");
                            }
                            try {
                                etype = Integer.parseInt(args[i]);
                                if (etype <= 0) {
                                    throw new NumberFormatException();
                                }
                            } catch (NumberFormatException nfe) {
                                error(args[i] + " is not a valid etype");
                            }
                        } else {
                            error(args[i] + " is not valid after -" + action);
                        }
                        break;
                    case "-s":   // salt for -a
                        if (++i >= args.length || args[i].startsWith("-")) {
                            error("A salt string must be specified after -s");
                        }
                        salt = args[i];
                        break;
                    case "-n":   // kvno for -a
                        if (++i >= args.length || args[i].startsWith("-")) {
                            error("A KVNO must be specified after -n");
                        }
                        try {
                            vAdd = Integer.parseInt(args[i]);
                            if (vAdd < 0) {
                                throw new NumberFormatException();
                            }
                        } catch (NumberFormatException nfe) {
                            error(args[i] + " is not a valid KVNO");
                        }
                        break;
                    case "-k":  // specify keytab to use
                        if (++i >= args.length || args[i].startsWith("-")) {
                            error("A keytab name must be specified after -k");
                        }
                        if (args[i].length() >= 5 &&
                            args[i].substring(0, 5).equalsIgnoreCase("FILE:")) {
                            name = args[i].substring(5);
                        } else {
                            name = args[i];
                        }
                        break;
                    case "-t":   // list timestamps
                        showTime = true;
                        break;
                    case "-f":   // force delete or get salt from KDC
                        fopt = true;
                        break;
                    case "-append": // -a, new keys append to file
                        append = true;
                        break;
                    default:
                        error("Unknown command: " + args[i]);
                        break;
                }
            } else {    // optional standalone arguments
                if (argAlreadyAppeared) {
                    error("Useless extra argument " + args[i]);
                }
                if (action == 'a') {
                    password = args[i].toCharArray();
                } else if (action == 'd') {
                    switch (args[i]) {
                        case "all": vDel = -1; break;
                        case "old": vDel = -2; break;
                        default: {
                            try {
                                vDel = Integer.parseInt(args[i]);
                                if (vDel < 0) {
                                    throw new NumberFormatException();
                                }
                            } catch (NumberFormatException nfe) {
                                error(args[i] + " is not a valid KVNO");
                            }
                        }
                    }
                } else {
                    error("Useless extra argument " + args[i]);
                }
                argAlreadyAppeared = true;
            }
        }
    }

    /**
     * Adds a service key to key table. If the specified key table does not
     * exist, the program will automatically generate
     * a new key table.
     */
    void addEntry() {
        if (salt != null && fopt) {
            System.err.println("-s and -f cannot coexist when adding a keytab entry.");
            throw new ExitException(-1);
        }
        PrincipalName pname = null;
        try {
            pname = new PrincipalName(principal);
        } catch (KrbException e) {
            System.err.println("Failed to add " + principal +
                               " to keytab.");
            e.printStackTrace();
            throw new ExitException(-1);
        }
        if (password == null) {
            try {
                BufferedReader cis = stdinReader();
                System.out.print("Password for " + pname.toString() + ":");
                System.out.flush();
                password = cis.readLine().toCharArray();
            } catch (IOException e) {
                System.err.println("Failed to read the password.");
                e.printStackTrace();
                throw new ExitException(-1);
            }

        }
        try {
            // admin.addEntry(pname, password);
            if (fopt) {
                KrbAsReqBuilder builder = new KrbAsReqBuilder(pname, password);
                builder.action();
                table.addEntry(pname, builder.getKeys(true), vAdd, append);
            } else if (salt != null) {
                table.addEntry(pname, salt, password, vAdd, append);
            } else {
                table.addEntry(pname, password, vAdd, append);
            }
            Arrays.fill(password, '0');  // clear password
            // admin.save();
            table.save();
            System.out.println("Done!");
            System.out.println("Service key for " + principal +
                               " is saved in " + table.tabName());

        } catch (KrbException e) {
            System.err.println("Failed to add " + principal + " to keytab.");
            e.printStackTrace();
            throw new ExitException(-1);
        } catch (IOException e) {
            System.err.println("Failed to save new entry.");
            e.printStackTrace();
            throw new ExitException(-1);
        }
    }

    /**
     * Lists key table name and entries in it.
     */
    void listKt() {
        System.out.println("Keytab name: " + table.tabName());
        KeyTabEntry[] entries = table.getEntries();
        if ((entries != null) && (entries.length > 0)) {
            String[][] output = new String[entries.length+1][showTime?3:2];
            int column = 0;
            output[0][column++] = "KVNO";
            if (showTime) output[0][column++] = "Timestamp";
            output[0][column++] = "Principal";
            for (int i = 0; i < entries.length; i++) {
                column = 0;
                output[i+1][column++] = entries[i].getKey().
                        getKeyVersionNumber().toString();
                if (showTime) output[i+1][column++] =
                        DateFormat.getDateTimeInstance(
                        DateFormat.SHORT, DateFormat.SHORT).format(
                        new Date(entries[i].getTimeStamp().getTime()));
                String princ = entries[i].getService().toString();
                if (showEType) {
                    int e = entries[i].getKey().getEType();
                    output[i+1][column++] = princ + " (" + e + ":" +
                            EType.toString(e) + ")";
                } else {
                    output[i+1][column++] = princ;
                }
            }
            int[] width = new int[column];
            for (int j=0; j<column; j++) {
                for (int i=0; i <= entries.length; i++) {
                    if (output[i][j].length() > width[j]) {
                        width[j] = output[i][j].length();
                    }
                }
                if (j != 0) width[j] = -width[j];
            }
            for (int j=0; j<column; j++) {
                System.out.printf("%" + width[j] + "s ", output[0][j]);
            }
            System.out.println();
            for (int j=0; j<column; j++) {
                for (int k=0; k<Math.abs(width[j]); k++) System.out.print("-");
                System.out.print(" ");
            }
            System.out.println();
            for (int i=0; i<entries.length; i++) {
                for (int j=0; j<column; j++) {
                    System.out.printf("%" + width[j] + "s ", output[i+1][j]);
                }
                System.out.println();
            }
        } else {
            System.out.println("0 entry.");
        }
    }

    private static BufferedReader stdinReader() {
        Charset charset = Charset.forName(System.getProperty("stdin.encoding"), Charset.defaultCharset());
        Reader reader = new InputStreamReader(System.in, charset);
        return new BufferedReader(reader);
    }

    /**
     * Deletes an entry from the key table.
     */
    void deleteEntry() {
        PrincipalName pname = null;
        try {
            pname = new PrincipalName(principal);
            if (!fopt) {
                String answer;
                BufferedReader cis = stdinReader();
                System.out.print("Are you sure you want to delete "+
                        "service key(s) for " + pname.toString() +
                        " (" + (etype==-1?"all etypes":("etype="+etype)) + ", " +
                        (vDel==-1?"all kvno":(vDel==-2?"old kvno":("kvno=" + vDel))) +
                        ") in " + table.tabName() + "? (Y/[N]): ");

                System.out.flush();
                answer = cis.readLine();
                if (answer.equalsIgnoreCase("Y") ||
                    answer.equalsIgnoreCase("Yes")) {
                    ;
                } else {
                    // no error, the user did not want to delete the entry
                    return;
                }
            }
        } catch (KrbException e) {
            System.err.println("Error occurred while deleting the entry. "+
                               "Deletion failed.");
            e.printStackTrace();
            throw new ExitException(-1);
        } catch (IOException e) {
            System.err.println("Error occurred while deleting the entry. "+
                               " Deletion failed.");
            e.printStackTrace();
            throw new ExitException(-1);
        }

        int count = table.deleteEntries(pname, etype, vDel);

        if (count == 0) {
            System.err.println("No matched entry in the keytab. " +
                               "Deletion fails.");
            throw new ExitException(-1);
        } else {
            try {
                table.save();
            } catch (IOException e) {
                System.err.println("Error occurs while saving the keytab. " +
                                   "Deletion fails.");
                e.printStackTrace();
                throw new ExitException(-1);
            }
            System.out.println("Done! " + count + " entries removed.");
        }
    }

    void error(String... errors) {
        for (String error: errors) {
            System.out.println("Error: " + error + ".");
        }
        printHelp();
        throw new ExitException(-1);
    }

    /**
     * Prints out the help information.
     */
    void printHelp() {
        System.out.println("\nUsage: ktab <commands> <options>");
        System.out.println();
        System.out.println("Available commands:");
        System.out.println();
        System.out.println("-l [-e] [-t]\n"
                + "    list the keytab name and entries. -e with etype, -t with timestamp.");
        System.out.println("-a <principal name> [<password>] [-n <kvno>] [-f | -s <salt>] [-append]\n"
                + "    add new key entries to the keytab for the given principal name with\n"
                + "    optional <password>. If a <kvno> is specified, new keys' Key Version\n"
                + "    Numbers equal to the value, otherwise, automatically incrementing\n"
                + "    the Key Version Numbers. If <salt> is specified, it will be used\n"
                + "    instead of the default salt. If -f is specified, the KDC will be\n"
                + "    contacted to fetch the salt. If -append is specified, new keys are\n"
                + "    appended to the keytab, otherwise, old keys for the\n"
                + "    same principal are removed.");
        System.out.println("-d <principal name> [-f] [-e <etype>] [<kvno> | all | old]\n"
                + "    delete key entries from the keytab for the specified principal. If\n"
                + "    <kvno> is specified, delete keys whose Key Version Numbers match\n"
                + "    kvno. If \"all\" is specified, delete all keys. If \"old\" is specified,\n"
                + "    delete all keys except those with the highest kvno. Default action\n"
                + "    is \"all\". If <etype> is specified, only keys of this encryption type\n"
                + "    are deleted. <etype> should be specified as the numeric value etype\n"
                + "    defined in RFC 3961, section 8. A prompt to confirm the deletion is\n"
                + "    displayed unless -f is specified.");
        System.out.println();
        System.out.println("Common option(s):");
        System.out.println();
        System.out.println("-k <keytab name>\n"
                + "    specify keytab name and path with prefix FILE:");
    }
}
