import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.function.*;
import java.text.*;
import java.security.*;
import java.security.cert.*;
import java.security.spec.*;
import javax.crypto.*;
import javax.crypto.spec.*;

import javafx.application.*;
import javafx.stage.*;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.text.*;
import javafx.scene.layout.*;
import javafx.scene.input.*;
import javafx.beans.property.*;
import javafx.event.*;
import javafx.geometry.*;
import javafx.collections.*;

public class MKeyStore extends Application {

  Stage stage;
  boolean showChar = false;
  BooleanProperty binary = new SimpleBooleanProperty(false);
  BooleanProperty modifyed = new SimpleBooleanProperty(false);
  ObjectProperty<File> file = new SimpleObjectProperty<>(new File(""));
  String password = null;
  TreeTableView<String> treeTableView;
  KeyStore keyStore;
  Label label;
  DateFormat dateFormatter = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM);
  FileChooser.ExtensionFilter autodtFl = new FileChooser.ExtensionFilter("Autodetect", "*.*");
  FileChooser.ExtensionFilter binaryFl = new FileChooser.ExtensionFilter("Binary", "*.*");
  FileChooser.ExtensionFilter base64Fl = new FileChooser.ExtensionFilter("Base-64", "*.*");
  ContextMenu contextMenu = new ContextMenu();
  String type = "PKCS12";  // PKCS12, JKS

  @Override
  public void start(Stage stage) {
    this.stage = stage;
    label = new Label();
    BorderPane layout = new BorderPane();
    stage.setScene(new Scene(layout, 400, 300));
    MenuBar menuBar = new MenuBar();
    Menu miFile = new Menu("File");
    MenuItem miFileNew = new MenuItem("New");
    miFileNew.setOnAction(ae -> { newKeyStore(null); });
    MenuItem miFileOpen = new MenuItem("Open...");
    miFileOpen.setOnAction(ae -> { openKeyStore(); });
    MenuItem miFileSave = new MenuItem("Save");
    miFileSave.setOnAction(ae -> { saveKeyStore(false); });
    miFileSave.setDisable(true);
    MenuItem miFileSaveAs = new MenuItem("Save as...");
    miFileSaveAs.setOnAction(ae -> { saveKeyStore(true); });
    MenuItem miFileExit = new MenuItem("Exit");
    miFileExit.setOnAction(ae -> { if (confirmSave()) { Platform.exit(); System.exit(0); } });
    miFile.getItems().addAll(miFileNew, miFileOpen, new SeparatorMenuItem(), miFileSave, miFileSaveAs, new SeparatorMenuItem(), miFileExit);
    //Menu miHelp = new Menu("Help");
    menuBar.getMenus().addAll(miFile);
    createTreeTableView();
    treeTableView.setRoot(new EntryTreeItem("", null));
    treeTableView.getRoot().setExpanded(true);
    layout.setTop(menuBar);
    layout.setCenter(treeTableView);
    layout.setBottom(label);
    file.addListener((o, v, n) -> { treeTableView.getRoot().setValue((n != null ? n.getName() : "New") + " (" + keyStore.getType() + ")"); });
    miFileSave.disableProperty().bind(modifyed.not());
    stage.setOnCloseRequest(e -> { if (!confirmSave()) e.consume(); });
    stage.show();
    Platform.runLater(() -> {
      List<String> p = getParameters().getRaw();
      if (p.isEmpty()) { newKeyStore(null); return; }
      File pFile = new File(p.get(0));
      if (pFile.isFile()) {
        loadKeyStore(pFile, autodtFl);
      } else {
        ButtonType bttp = alert(stage, Alert.AlertType.CONFIRMATION, "File " + pFile.getName() + " not found. Create?", ButtonType.YES, ButtonType.NO, ButtonType.CANCEL);
        if (bttp == null || bttp == ButtonType.CANCEL) { miFileExit.getOnAction().handle(null); }
        newKeyStore(bttp == ButtonType.YES ? pFile : null);
      }
    });
  }

  void loadKeyStore(File loadFile, FileChooser.ExtensionFilter flt) {
    try {
      if (loadFile.length() > 500_000L) sizeException();
      byte[] bytes = Files.readAllBytes(loadFile.toPath());
      bytes = detect(bytes, flt, binary);
      keyStore = KeyStore.getInstance(type);
      try {
        keyStore.load(new ByteArrayInputStream(bytes), new char[0]);
        password = "";
      } catch (Exception e) {
        String openPassword = passwordDialog();
        if (openPassword == null) { newKeyStore(null); return; }
        keyStore.load(new ByteArrayInputStream(bytes), openPassword.toCharArray());
        password = openPassword;
      }
      fillTreeTableView();
      file.set(loadFile);
      modifyed.set(false);
    } catch (Exception e) { alert(stage, Alert.AlertType.ERROR, e); newKeyStore(null); }
  }

  void sizeException() throws IOException { throw new IOException("File is too large"); }

  void newKeyStore(File fl) {
    if (!confirmSave()) return;
    try {
      keyStore = KeyStore.getInstance(type);
      keyStore.load(null, null);
      fillTreeTableView();
      file.set(fl);
      modifyed.set(false);
      binary.set(true);
      password = null;
    } catch (Exception e) { e.printStackTrace(); }
  }

  void openKeyStore() {
    if (!confirmSave()) return;
    FileChooser fileChooser = new FileChooser();
    fileChooser.getExtensionFilters().addAll(autodtFl, binaryFl, base64Fl);
    fileChooser.setInitialFileName("KeyStore");
    File openFile = fileChooser.showOpenDialog(stage);
    if (openFile != null) loadKeyStore(openFile, fileChooser.getSelectedExtensionFilter());
  }

  byte[] detect(byte[] bytes, FileChooser.ExtensionFilter flt, BooleanProperty bin) throws IOException {
    if (flt == binaryFl) { bin.set(true); return bytes; }
    boolean chArr = true;
    for (byte b : bytes) if (b > 0 && b < 32 && b != 9 && b != 10 && b != 13) { chArr = false; break; }
    if (flt == base64Fl && !chArr) throw new IOException("Non printable character");
    if (flt == autodtFl && !chArr) { bin.set(true); return bytes; }
    bin.set(false);
    return Base64.getMimeDecoder().decode(norm(new String(bytes)));
  }

  String norm(String s) {
    return s.replaceAll("-----BEGIN [^-]+-----\\s+|-----END [^-]+-----|begin-base64[^\\n]+\\s+|====", "");
  }

  boolean confirmSave() {
    if (!modifyed.get()) return true;
    ButtonType bttp = alert(stage, Alert.AlertType.CONFIRMATION, "Save KeyStore?", ButtonType.YES, ButtonType.NO, ButtonType.CANCEL);
    if (bttp == null || bttp == ButtonType.CANCEL) return false;
    if (bttp == ButtonType.NO) return true;
    return saveKeyStore(false);
  }

  boolean saveKeyStore(boolean saveAs) {
    String savePassword = password;
    File saveFile = file.get();
    boolean bin = binary.get();
    if (saveAs || file.get() == null) {
      FileChooser fileChooser = new FileChooser();
      fileChooser.getExtensionFilters().addAll(binaryFl, base64Fl);
      fileChooser.setInitialFileName(saveFile != null ? (saveFile.isFile() ? "Copy" : "") + saveFile.getName() : "KeyStore");
      saveFile = fileChooser.showSaveDialog(stage);
      if (saveFile == null)  return false;
      savePassword = passwordDialog();
      if (savePassword == null) return false;
      bin = fileChooser.getSelectedExtensionFilter() == binaryFl;
    }
    try (OutputStream os = bin ? Files.newOutputStream(saveFile.toPath()) : Base64.getMimeEncoder().wrap(Files.newOutputStream(saveFile.toPath()))) {
      keyStore.store(os, savePassword.toCharArray());
    } catch (Exception e) { alert(stage, Alert.AlertType.ERROR, e); return false; }
    password = savePassword;
    binary.set(bin);
    file.set(saveFile);
    modifyed.set(false);
    return true;
  }

  void createTreeTableView() {
    treeTableView = new TreeTableView<>();
    TreeTableColumn<String, String> column = new TreeTableColumn<>();
    column.setCellValueFactory(
      (TreeTableColumn.CellDataFeatures<String, String> cellData) -> 
      new ReadOnlyObjectWrapper<String>(cellData.getValue().getValue())
    );
    treeTableView.getColumns().add(column);
    treeTableView.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
    treeTableView.widthProperty().addListener((v, o, n) -> {
      Pane header = (Pane)treeTableView.lookup("TableHeaderRow");
      if (header != null && header.isVisible()) {
        header.setMaxHeight(0); header.setMinHeight(0); header.setPrefHeight(0);
        header.setVisible(false); header.setManaged(false);
      }
    });
    treeTableView.getSelectionModel().selectedItemProperty().addListener((v, o, n) -> {
      Date date = null;
      try {
        if (((EntryTreeItem)n).getEntryClass() != null) date = keyStore.getCreationDate(n.getValue());
      } catch (Exception e) { }
      label.setText(date != null ? dateFormatter.format(date) : "");
    });
    MenuItem miInfo = new MenuItem("Info");
    miInfo.setOnAction(ae -> { info(); });
    MenuItem miView = new MenuItem("View");
    miView.setOnAction(ae -> { viewDialog(true, false); });
    MenuItem miIns = new MenuItem("New");
    miIns.setOnAction(ae -> { viewDialog(false, true); });
    MenuItem miClone = new MenuItem("Clone");
    miClone.setOnAction(ae -> { viewDialog(true, true); });
    MenuItem miDel = new MenuItem("Delete");
    miDel.setOnAction(ae -> { delete(); });
    treeTableView.setRowFactory(tv -> {
      TreeTableRow<String> row = new TreeTableRow<>();
      row.setOnMouseClicked(me -> {
        if (contextMenu.isShowing()) contextMenu.hide();
        EntryTreeItem tiRoot = (EntryTreeItem)treeTableView.getRoot();
        EntryTreeItem eti = (EntryTreeItem)row.getTreeItem();
        if (row.isEmpty() || eti == null) {
        } else if (me.getButton() == MouseButton.PRIMARY && me.getClickCount() == 2) {
          if (eti == tiRoot) {}
          else if (eti.getParent() != tiRoot) miView.getOnAction().handle(null);
        } else if (me.getButton() == MouseButton.SECONDARY && eti != tiRoot) {
          if (eti.getParent() == tiRoot) contextMenu.getItems().setAll(miIns);
          else contextMenu.getItems().setAll(miView, miInfo, new SeparatorMenuItem(), miIns, miClone, new SeparatorMenuItem(), miDel);
          contextMenu.show((Node)me.getSource(), me.getScreenX(), me.getScreenY());
        }
      });
      return row;
    });
    treeTableView.setOnKeyPressed(ke -> {
      EntryTreeItem eti = (EntryTreeItem)treeTableView.getSelectionModel().getSelectedItem();
      EntryTreeItem tiRoot = (EntryTreeItem)treeTableView.getRoot();
      if (eti != null && eti != tiRoot) {
        if (ke.getCode() == KeyCode.ENTER && eti.getParent() != tiRoot) miView.getOnAction().handle(null);
        else if (ke.getCode() == KeyCode.INSERT) miIns.getOnAction().handle(null);
        else if (ke.getCode() == KeyCode.DELETE && eti.getParent() != tiRoot) miDel.getOnAction().handle(null);
      }
    });
  }

  KeyStore.Entry getKeyStoreEntry(EntryTreeItem eti) {
    String alias = eti.getValue();
    KeyStore.Entry kse = null;
    KeyStore.PasswordProtection ppr = null;
    if (isProtected(eti.getEntryClass())) {
      if (eti.getPassword() == null) {
        try {
          kse = keyStore.getEntry(alias, new KeyStore.PasswordProtection(new char[0]));
          eti.setPassword("");
          return kse;
        } catch (Exception e) { }
        String password = passwordDialog();
        if (password == null) return null;
        eti.setPassword(password);
      }
      ppr = new KeyStore.PasswordProtection(eti.getPassword().toCharArray());
    }
    try {
      kse = keyStore.getEntry(alias, ppr);
    } catch (Exception e) {
      alert(stage, Alert.AlertType.ERROR, e);
      eti.setPassword(null);
    }
    return kse;
  }

  void info() {
    EntryTreeItem eti = (EntryTreeItem)treeTableView.getSelectionModel().getSelectedItem();
    KeyStore.Entry kse = getKeyStoreEntry(eti);
    if (kse != null) infoDialog(stage, eti.getValue(), kse.toString());
  }

  void delete() {
    try {
      EntryTreeItem eti = (EntryTreeItem)treeTableView.getSelectionModel().getSelectedItem();
      String alias = eti.getValue();
      if (alert(stage, Alert.AlertType.CONFIRMATION, "Delete " + alias + "?") == ButtonType.OK) {
        keyStore.deleteEntry(alias);
        if (!keyStore.containsAlias(alias)) eti.getParent().getChildren().remove(eti);
        modifyed.set(true);
      }
    } catch (Exception e) { e.printStackTrace(); }
  }

  void fillTreeTableView() {
    treeTableView.getRoot().getChildren().clear();
    treeTableView.getRoot().getChildren().add(new EntryTreeItem("Private key & Certificate", KeyStore.PrivateKeyEntry.class));
    treeTableView.getRoot().getChildren().add(new EntryTreeItem("Secret key", KeyStore.SecretKeyEntry.class));
    treeTableView.getRoot().getChildren().add(new EntryTreeItem("Certificate", KeyStore.TrustedCertificateEntry.class));
    for (TreeItem<String> eti : treeTableView.getRoot().getChildren()) eti.setExpanded(true);
    treeTableView.getSelectionModel().selectFirst();
    try {
      Enumeration<String> aliases = keyStore.aliases();
      while (aliases.hasMoreElements()) {
        String alias = aliases.nextElement();
        for (TreeItem<String> eti : treeTableView.getRoot().getChildren()) {
          Class<? extends KeyStore.Entry> clss = ((EntryTreeItem)eti).getEntryClass();
          if (keyStore.entryInstanceOf(alias, clss)) {
            eti.getChildren().add(new EntryTreeItem(alias, clss));
            break;
          }
        }
      }
    } catch (Exception e) { e.printStackTrace(); }
  }

  String passwordDialog() {
    Dialog<ButtonType> dialog = new Dialog<>();
    dialog.initOwner(stage);
    dialog.initStyle(StageStyle.UTILITY);
    dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
    CheckBox checkBox = new CheckBox();
    checkBox.setSelected(showChar);
    PasswordField passwordField = new PasswordField();
    TextField textField = new TextField();
    passwordField.textProperty().bindBidirectional(textField.textProperty());
    textField.managedProperty().bind(checkBox.selectedProperty());
    textField.visibleProperty().bind(checkBox.selectedProperty());
    passwordField.managedProperty().bind(checkBox.selectedProperty().not());
    passwordField.visibleProperty().bind(checkBox.selectedProperty().not());
    VBox fb = new VBox();
    fb.getChildren().addAll(passwordField, textField);
    HBox cb = new HBox(8);
    cb.getChildren().addAll(new Label("Show characters:"), checkBox);
    VBox pb = new VBox(8);
    pb.getChildren().addAll(new Label("Password:"), fb, cb);
    dialog.getDialogPane().setContent(pb);
    Platform.runLater(() -> (showChar ? textField : passwordField).requestFocus());
    dpBehavior(dialog.getDialogPane());
    Optional<ButtonType> result = dialog.showAndWait();
    showChar = checkBox.isSelected();
    if (result.isPresent() && result.get() == ButtonType.OK) {
      return textField.getText();
    }
    return null;
  }

  ButtonType alert(Window owner, Alert.AlertType type, Object cont, ButtonType... bt) {
    Alert alert = new Alert(type, "", bt);
    alert.initOwner(owner);
    alert.initStyle(StageStyle.UTILITY);
    alert.setHeaderText(null);
    if (cont instanceof String) {
      alert.setContentText((String)cont);
    } else if (cont instanceof Throwable) {
      Throwable t = (Throwable)cont;
      alert.setContentText(t.getMessage() != null && !t.getMessage().isEmpty() ? t.getMessage() : t.toString());
      StringWriter sw = new StringWriter();
      ((Throwable)cont).printStackTrace(new PrintWriter(sw));
      TextArea ta = new TextArea(sw.toString());
      //ta.setStyle("-fx-font-family: monospace; -fx-font-weight: bold;");
      ta.setEditable(false);
      ta.setPadding(new Insets(0));
      alert.getDialogPane().setExpandableContent(ta);
    }
    dpBehavior(alert.getDialogPane());
    Optional<ButtonType> result = alert.showAndWait();
    return (result.isPresent() ? result.get() : null);
  }

  void viewDialog(boolean cnt, boolean edt) {
    EntryTreeItem eti = (EntryTreeItem)treeTableView.getSelectionModel().getSelectedItem();
    KeyStore.Entry kse = null;
    if (cnt) {
      kse = getKeyStoreEntry(eti);
      if (kse == null) return;
    }
    Class<? extends KeyStore.Entry> clss = eti.getEntryClass();
    TextField alias = new TextField(cnt ? eti.getValue() : "newAlias");
    alias.setEditable(edt);
    alias.setFocusTraversable(edt);
    TabPane tabPane = new TabPane();
    if (!tabPane.getStyleClass().contains(TabPane.STYLE_CLASS_FLOATING)) {
      tabPane.getStyleClass().add(TabPane.STYLE_CLASS_FLOATING);
    }
    try {
      if (clss == KeyStore.PrivateKeyEntry.class) {
        byte[] bytes = (kse != null ? ((KeyStore.PrivateKeyEntry)kse).getPrivateKey().getEncoded() : null);
        tabPane.getTabs().add(createTab(PrivateKey.class, bytes, "PrivateKey", edt));
        if (kse != null) {
          java.security.cert.Certificate[] serts = ((KeyStore.PrivateKeyEntry)kse).getCertificateChain();
          for (java.security.cert.Certificate sert : serts) {
            tabPane.getTabs().add(createTab(java.security.cert.Certificate.class, sert.getEncoded(), "Certificate", edt));
          }
        } else tabPane.getTabs().add(createTab(java.security.cert.Certificate.class, null, "Certificate", true));
        if (edt) tabPane.getTabs().add(createTab(null, null, "+", true));
      } else if (clss == KeyStore.SecretKeyEntry.class) {
        byte[] bytes = (kse != null ? ((KeyStore.SecretKeyEntry)kse).getSecretKey().getEncoded() : null);
        tabPane.getTabs().add(createTab(SecretKey.class, bytes, "SecretKey", edt));
      } else if (clss == KeyStore.TrustedCertificateEntry.class) {
        byte[] bytes = (kse != null ? ((KeyStore.TrustedCertificateEntry)kse).getTrustedCertificate().getEncoded() : null);
        tabPane.getTabs().add(createTab(java.security.cert.Certificate.class, bytes, "Certificate", edt));
      }
    } catch (Exception e) { e.printStackTrace(); }
    Dialog<ButtonType> dialog = new Dialog<>();
    Window window = dialog.getDialogPane().getScene().getWindow();
    dialog.initOwner(stage);
    dialog.initStyle(StageStyle.UTILITY);
    dialog.setTitle(clss.getSimpleName());
    dialog.setResizable(true);
    Button bti = new Button("Info"), bte = new Button((edt ? "Load" : "Save"));
    bti.setOnAction(e -> {
      StringBuilder sb = new StringBuilder();
      for (Tab tb : tabPane.getTabs()) {
        if(tb.getUserData() != null && !((TextArea)tb.getContent()).getText().isEmpty()) {
          Object o = getKSObj(window, tb);
          if (o == null) return;
          sb.append(tb.getText()).append(": ").append(getKSObjString(o)).append("\n");
        }
      }
      infoDialog(window, alias.getText(), sb.toString());
    });
    bte.setOnAction(e -> {
      FileChooser fileChooser = new FileChooser();
      if (edt) fileChooser.getExtensionFilters().addAll(autodtFl);
      fileChooser.getExtensionFilters().addAll(binaryFl, base64Fl);
      Tab tb = tabPane.getSelectionModel().getSelectedItem();
      fileChooser.setInitialFileName(tb.getText());
      TextArea ta = (TextArea)tb.getContent();
      try {
        if (edt) {
          File file = fileChooser.showOpenDialog(window);
          if (file != null) {
            if (file.length() > 10_000L) sizeException();
            byte[] bytes = Files.readAllBytes(file.toPath());
            bytes = detect(bytes, fileChooser.getSelectedExtensionFilter(), new SimpleBooleanProperty());
            ta.setText(Base64.getMimeEncoder().encodeToString(bytes));
          }
        } else {
          File file = fileChooser.showSaveDialog(window);
          if (file != null) {
            Files.write(file.toPath(), fileChooser.getSelectedExtensionFilter() == binaryFl ? Base64.getMimeDecoder().decode(ta.getText()) : ta.getText().getBytes());
          }
        }
      } catch (Exception ef) { alert(window, Alert.AlertType.ERROR, ef); }
    });
    tabPane.getSelectionModel().selectedItemProperty().addListener((v, o, n) -> {
      if (clss == KeyStore.PrivateKeyEntry.class && n.getUserData() == null) {
        int sz = tabPane.getTabs().size();
        Tab pnl = tabPane.getTabs().get(sz - 2);
        pnl.setClosable(true);
        Tab pn = tabPane.getTabs().get(sz - 1);
        tabPane.getTabs().add(createTab(null, null, pn.getText(), true));
        pn.setText(pnl.getText());
        pn.setUserData(pnl.getUserData());
        pn.setClosable(true);
      }
    });
    tabPane.getTabs().addListener((ListChangeListener.Change<? extends Tab> c) -> {
      while(c.next()) {
        if (c.wasRemoved() && tabPane.getTabs().size() == 3) {
          tabPane.getTabs().get(1).setClosable(false);
        }
      }
    });
    HBox hb = new HBox(8);
    hb.setAlignment(Pos.CENTER_LEFT);
    hb.getChildren().addAll(new Label("Alias:"), alias,  bti, bte);
    VBox vb = new VBox(8);
    vb.getChildren().addAll(hb, tabPane);
    VBox.setVgrow(tabPane, Priority.ALWAYS);
    dialog.getDialogPane().setContent(vb);
    dialog.getDialogPane().setPrefSize(640, 480);
    BiConsumer<Event, Boolean> addEntry = (event, confirm) -> {
      if (confirm.booleanValue()) {
        ButtonType bttp = alert(window, Alert.AlertType.CONFIRMATION, "Save entry?", ButtonType.YES, ButtonType.NO, ButtonType.CANCEL);
        if (bttp == null || bttp == ButtonType.CANCEL) { event.consume(); return; }
        if (bttp == ButtonType.NO) return;
      }
      ArrayList<java.security.cert.Certificate> sertArr = new ArrayList<>();
      PrivateKey privateKey = null;
      SecretKey secretKey = null;
      String password = null;
      for (Tab tb : tabPane.getTabs()) {
        if(tb.getUserData() != null) {
          Object o = getKSObj(window, tb);
          if (o == null) { event.consume(); return; }
          if (o instanceof java.security.cert.Certificate) sertArr.add((java.security.cert.Certificate)o);
          else if (o instanceof PrivateKey) privateKey = (PrivateKey)o;
          else if (o instanceof SecretKey) secretKey = (SecretKey)o;
        }
      }
      try {
        String al = alias.getText().trim().toLowerCase();
        if (al.isEmpty()) { alert(window, Alert.AlertType.ERROR, "Alias is empty"); event.consume(); return; }
        boolean replace = false;
        if (keyStore.containsAlias(al)) { 
          if (alert(window, Alert.AlertType.CONFIRMATION, "Alias " + al + " already defined. Replace?", ButtonType.YES, ButtonType.NO) != ButtonType.YES) {
            event.consume();
            return;
          }
          replace = true;
        }
        if (isProtected(clss)) {
          password = passwordDialog();
          if (password == null) { event.consume(); return; }
        }
        KeyStore.Entry entry = null;
        if (clss == KeyStore.PrivateKeyEntry.class) {
          entry = new KeyStore.PrivateKeyEntry(privateKey, sertArr.toArray(new java.security.cert.Certificate[0]));
        } else if (clss == KeyStore.SecretKeyEntry.class) {
          entry = new KeyStore.SecretKeyEntry(secretKey);
        } else if (clss == KeyStore.TrustedCertificateEntry.class) {
          entry = new KeyStore.TrustedCertificateEntry(sertArr.get(0));
        }
        if (replace) {
          EntryTreeItem ti = getEntryTreeItem(al);
          keyStore.deleteEntry(al);
          if (!keyStore.containsAlias(al)) ti.getParent().getChildren().remove(ti);
        }
        keyStore.setEntry(al, entry, (password != null ? new KeyStore.PasswordProtection(password.toCharArray()) : null));
        EntryTreeItem newEti = new EntryTreeItem(al, clss);
        newEti.setPassword(password);
        getEntryTreeItem(clss).getChildren().add(newEti);
        treeTableView.getSelectionModel().select(treeTableView.getRow(newEti));
        modifyed.set(true);
      } catch (Exception ef) { alert(window, Alert.AlertType.ERROR, ef); event.consume(); }
    };
    dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK);
    if (edt) {
      dialog.getDialogPane().getButtonTypes().addAll(ButtonType.CANCEL);
      Button btOk = (Button)dialog.getDialogPane().lookupButton(ButtonType.OK);
      btOk.addEventFilter(ActionEvent.ACTION, e -> { addEntry.accept(e, Boolean.FALSE); });
      window.setOnCloseRequest(e -> { addEntry.accept(e, Boolean.TRUE); });
      dialog.getDialogPane().getScene().setOnKeyPressed(e -> {
        if (e.getCode() == KeyCode.ESCAPE) addEntry.accept(e, Boolean.TRUE);
      });
      dpBehavior(dialog.getDialogPane());
    }
    dialog.showAndWait();
  }

  Tab createTab(Class<?> clss, byte[] bytes, String name, boolean edt) {
    String s = (bytes != null ? Base64.getMimeEncoder().encodeToString(bytes) : "");
    TextArea ta = new TextArea(s);
    ta.setEditable(edt);
    ta.setFont(Font.font("Monospaced"));
    Tab tab = new Tab(name, ta);
    tab.setUserData(clss);
    tab.setClosable(false);
    return tab;
  }

  Object getKSObj(Window window, Tab tab) {
    try {
      Class<?> clss = (Class<?>)tab.getUserData();
      String s = ((TextArea)tab.getContent()).getText();
      byte[] bytes  = Base64.getMimeDecoder().decode(norm(s));
      if (clss == java.security.cert.Certificate.class) {
        return /*(java.security.cert.Certificate)*/ CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(bytes));
      } else if (clss == PrivateKey.class) {
        return /*(PrivateKey)*/ KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(bytes));
      } else if (clss == SecretKey.class) {
        return (SecretKey)(new SecretKeySpec(bytes, "AES"));
      }
    } catch (Exception e) {
      tab.getTabPane().getSelectionModel().select(tab);
      alert(window, Alert.AlertType.ERROR, e);
    }
    return null;
  }

  String getKSObjString(Object obj) {
    if (obj instanceof Key) return (obj instanceof SecretKey ? "Size " + (((Key)obj).getEncoded().length * 8) + " bits, " : "") +
      "Algorithm " + ((Key)obj).getAlgorithm() + ", Format " + ((Key)obj).getFormat();
    return obj.toString();
  }

  boolean isProtected(Class<? extends KeyStore.Entry> clss) {
    return (clss == KeyStore.PrivateKeyEntry.class || clss == KeyStore.SecretKeyEntry.class);
  }

  EntryTreeItem getEntryTreeItem(Class<? extends KeyStore.Entry> clss) {
    for (TreeItem<String> eti : treeTableView.getRoot().getChildren())
      if (((EntryTreeItem)eti).getEntryClass() == clss) return (EntryTreeItem)eti;
    return null;
  }

  EntryTreeItem getEntryTreeItem(String alias) {
    for (TreeItem<String> etir : treeTableView.getRoot().getChildren())
      for (TreeItem<String> eti : etir.getChildren())
        if (alias.equals(eti.getValue())) return (EntryTreeItem)eti;
    return null;
  }

  void infoDialog(Window owner, String title, String text) {
    TextArea ta = new TextArea(text);
    ta.setEditable(false);
    ta.setFont(Font.font("Monospaced"));
    ta.setPadding(new Insets(0));
    Dialog<ButtonType> dialog = new Dialog<>();
    dialog.initOwner(owner);
    dialog.initStyle(StageStyle.UTILITY);
    dialog.setResizable(true);
    dialog.setTitle(title);
    dialog.getDialogPane().setContent(ta);
    dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK);
    dialog.getDialogPane().setPrefSize(480, 320);
    dpBehavior(dialog.getDialogPane());
    dialog.showAndWait();
  }

  void dpBehavior(DialogPane dialogPane) {
    for (ButtonType bt : dialogPane.getButtonTypes()) {
      Button b = (Button)dialogPane.lookupButton(bt);
      b.setDefaultButton(false);
      b.addEventHandler(KeyEvent.KEY_PRESSED, ke -> {
        if (ke.getCode() == KeyCode.ENTER) {
          ((Button)ke.getTarget()).fire();
          ke.consume();
        }
      });
    }
  }

  class EntryTreeItem extends TreeItem<String> {

    Class<? extends KeyStore.Entry> entryClass;
    String password = null;
    EntryTreeItem(String alias, Class<? extends KeyStore.Entry> entryClass) {
      super(alias, (Node)null);
      this.entryClass = entryClass;
    }
    String getPassword() { return password; }
    void setPassword(String password) { this.password = password; }
    Class<? extends KeyStore.Entry> getEntryClass() { return entryClass; }

  }  // end class EntryTreeItem

}  // end class MKeyStore