initial commit

This commit is contained in:
Radek Davidek 2026-02-06 16:48:54 +01:00
commit 46f8ceccbc
9 changed files with 1419 additions and 0 deletions

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
# IDE
.idea/
*.iml
*.iws
*.ipr
.vscode/
.classpath
.project
.settings/
# OS
.DS_Store
Thumbs.db

8
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"java.compile.nullAnalysis.mode": "automatic",
"java.configuration.updateBuildConfiguration": "automatic",
"chat.tools.terminal.autoApprove": {
"/home/kamma/java/apache-maven-3.9.11/bin/mvn": true,
"/home/kamma/java/jdk-21.0.9+10/bin/java": true
}
}

58
pom.xml Normal file
View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jkeepass</groupId>
<artifactId>jkeepass</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>cz.kamma.jkeepass.KeepassApp</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,216 @@
package cz.kamma.jkeepass;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import cz.kamma.jkeepass.model.Database;
import cz.kamma.jkeepass.model.Entry;
import cz.kamma.jkeepass.model.Group;
/**
* Handler for custom JKP format (encrypted JSON database)
*/
public class CustomDatabaseFormat {
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
private static final Gson gson = new GsonBuilder().create();
/**
* Internal JSON structure for serialization
*/
static class JkpDatabase {
public int version = 1;
public String salt; // Base64 encoded
public String iv; // Base64 encoded
public String encryptedData; // Base64 encoded
}
static class JkpGroup {
public String name;
public List<JkpGroup> groups = new ArrayList<>();
public List<JkpEntry> entries = new ArrayList<>();
}
static class JkpEntry {
public String title;
public String username;
public String password;
public String url;
public String notes;
}
/**
* Save database to .jkp file (encrypted JSON)
*/
public static void saveDatabase(File file, Database database, String password) throws Exception {
// Serialize database to JSON
JkpGroup rootGroup = serializeGroup(database.getRootGroup());
String jsonData = gson.toJson(rootGroup);
// Generate salt and derive key
byte[] salt = generateSalt();
SecretKey key = deriveKey(password.toCharArray(), salt);
// Encrypt data
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
javax.crypto.spec.IvParameterSpec iv = new javax.crypto.spec.IvParameterSpec(new byte[16]);
cipher.init(Cipher.ENCRYPT_MODE, key, iv);
byte[] encryptedBytes = cipher.doFinal(jsonData.getBytes(StandardCharsets.UTF_8));
// Create JKP structure
JkpDatabase jkpDb = new JkpDatabase();
jkpDb.salt = Base64.getEncoder().encodeToString(salt);
jkpDb.iv = Base64.getEncoder().encodeToString(iv.getIV());
jkpDb.encryptedData = Base64.getEncoder().encodeToString(encryptedBytes);
// Write to file
String jkpJson = gson.toJson(jkpDb);
Files.write(Path.of(file.getAbsolutePath()), jkpJson.getBytes(StandardCharsets.UTF_8));
}
/**
* Load JKP file and convert to Database object
* Creates an empty database structure and populates it with JKP data
*/
public static Database convertJkpToDatabase(File file, String password) throws Exception {
// Load the JKP structure
JkpGroup jkpData = loadDatabaseStructure(file, password);
// Create a new empty database to hold the JKP data
Database database = new Database();
// Clear template data and populate with JKP data
Group rootGroup = database.getRootGroup();
rootGroup.setName(jkpData.name != null ? jkpData.name : "Root");
// Clear existing entries and groups
for (Object entryObj : new java.util.ArrayList<>(rootGroup.getEntries())) {
rootGroup.removeEntry((Entry) entryObj);
}
for (Object groupObj : new java.util.ArrayList<>(rootGroup.getGroups())) {
rootGroup.removeGroup((Group) groupObj);
}
// Populate with JKP data
deserializeGroup(rootGroup, jkpData, database);
return database;
}
/**
* Load database from .jkp file
*/
public static JkpGroup loadDatabaseStructure(File file, String password) throws Exception {
// Read file
String jkpJson = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
JkpDatabase jkpDb = gson.fromJson(jkpJson, JkpDatabase.class);
if (jkpDb == null || jkpDb.encryptedData == null) {
throw new IOException("Invalid JKP file format");
}
// Decode salt and IV
byte[] salt = Base64.getDecoder().decode(jkpDb.salt);
byte[] ivBytes = Base64.getDecoder().decode(jkpDb.iv);
byte[] encryptedBytes = Base64.getDecoder().decode(jkpDb.encryptedData);
// Derive key
SecretKey key = deriveKey(password.toCharArray(), salt);
// Decrypt data
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
javax.crypto.spec.IvParameterSpec ivSpec = new javax.crypto.spec.IvParameterSpec(ivBytes);
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
String jsonData = new String(decryptedBytes, StandardCharsets.UTF_8);
// Deserialize from JSON
return gson.fromJson(jsonData, JkpGroup.class);
}
/**
* Serialize KeePass Group to JKP Group
*/
private static JkpGroup serializeGroup(Group group) {
JkpGroup jkpGroup = new JkpGroup();
jkpGroup.name = group.getName();
// Serialize subgroups
for (Object subGroupObj : group.getGroups()) {
Group subGroup = (Group) subGroupObj;
jkpGroup.groups.add(serializeGroup(subGroup));
}
// Serialize entries
for (Object entryObj : group.getEntries()) {
Entry entry = (Entry) entryObj;
JkpEntry jkpEntry = new JkpEntry();
jkpEntry.title = entry.getTitle() != null ? entry.getTitle() : "";
jkpEntry.username = entry.getUsername() != null ? entry.getUsername() : "";
jkpEntry.password = entry.getPassword() != null ? entry.getPassword() : "";
jkpEntry.url = entry.getUrl() != null ? entry.getUrl() : "";
jkpEntry.notes = entry.getNotes() != null ? entry.getNotes() : "";
jkpGroup.entries.add(jkpEntry);
}
return jkpGroup;
}
/**
* Deserialize JKP Group and populate KeePass Database
*/
public static void deserializeGroup(Group targetGroup, JkpGroup source, Database database) {
// Add subgroups
for (JkpGroup srcGroup : source.groups) {
Group newGroup = database.newGroup(srcGroup.name);
targetGroup.addGroup(newGroup);
deserializeGroup(newGroup, srcGroup, database);
}
// Add entries
for (JkpEntry srcEntry : source.entries) {
Entry newEntry = targetGroup.addEntry(database.newEntry());
newEntry.setTitle(srcEntry.title != null ? srcEntry.title : "");
newEntry.setUsername(srcEntry.username != null ? srcEntry.username : "");
newEntry.setPassword(srcEntry.password != null ? srcEntry.password : "");
newEntry.setUrl(srcEntry.url != null ? srcEntry.url : "");
newEntry.setNotes(srcEntry.notes != null ? srcEntry.notes : "");
}
}
/**
* Derive encryption key from password
*/
private static SecretKey deriveKey(char[] password, byte[] salt) throws Exception {
String passwordStr = new String(password);
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(salt);
byte[] keyBytes = md.digest(passwordStr.getBytes(StandardCharsets.UTF_8));
// Ensure exactly 32 bytes (256 bits) for AES-256
return new SecretKeySpec(keyBytes, 0, 32, "AES");
}
/**
* Generate random salt
*/
private static byte[] generateSalt() {
SecureRandom random = new SecureRandom();
byte[] salt = new byte[16];
random.nextBytes(salt);
return salt;
}
}

View File

@ -0,0 +1,985 @@
package cz.kamma.jkeepass;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Frame;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.Insets;
import java.awt.MenuItem;
import java.awt.Point;
import java.awt.PopupMenu;
import java.awt.SystemTray;
import java.awt.Toolkit;
import java.awt.TrayIcon;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.prefs.Preferences;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import javax.swing.table.DefaultTableModel;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;
import cz.kamma.jkeepass.model.Database;
import cz.kamma.jkeepass.model.Entry;
import cz.kamma.jkeepass.model.Group;
public class KeepassApp extends JFrame {
private JTable entryTable;
private DefaultTableModel tableModel;
private JTextArea notesArea;
private JLabel statusLabel;
private JMenu recentMenu;
private JTree groupTree;
private DefaultTreeModel treeModel;
private JSplitPane mainSplitPane;
private JSplitPane rightSplitPane;
private Database database;
private Group currentGroup;
private List<Entry> currentEntries = new ArrayList<>();
private final Preferences prefs = Preferences.userNodeForPackage(KeepassApp.class);
private static final String PREF_RECENT_LIST = "recent_database_list";
private static final String PREF_WINDOW_X = "window_x";
private static final String PREF_WINDOW_Y = "window_y";
private static final String PREF_WINDOW_WIDTH = "window_width";
private static final String PREF_WINDOW_HEIGHT = "window_height";
private static final String PREF_MAIN_DIVIDER = "main_divider_location";
private static final String PREF_RIGHT_DIVIDER = "right_divider_location";
private static final int MAX_RECENT_FILES = 5;
private File currentFile;
private String currentPassword;
private TrayIcon trayIcon;
public KeepassApp() {
// Set properties before anything else
System.setProperty("sun.awt.enableExtraMouseButtons", "true");
// Force GTK 2 or 3 for better X11 integration in Cinnamon
System.setProperty("jdk.gtk.version", "3");
// Ensure we don't use the old way of drawing that can conflict with XApp
System.setProperty("sun.java2d.xrender", "true");
setTitle("JKeepass");
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
// Load and apply saved window position and size
int savedX = prefs.getInt(PREF_WINDOW_X, -1);
int savedY = prefs.getInt(PREF_WINDOW_Y, -1);
int savedWidth = prefs.getInt(PREF_WINDOW_WIDTH, 800);
int savedHeight = prefs.getInt(PREF_WINDOW_HEIGHT, 500);
if (savedX != -1 && savedY != -1) {
setLocation(savedX, savedY);
setSize(savedWidth, savedHeight);
} else {
setSize(800, 500);
setLocationRelativeTo(null);
}
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
saveWindowState();
saveComponentState();
if (SystemTray.isSupported() && trayIcon != null) {
setVisible(false);
} else {
System.exit(0);
}
}
});
addComponentListener(new ComponentAdapter() {
@Override
public void componentMoved(ComponentEvent e) {
saveWindowState();
}
@Override
public void componentResized(ComponentEvent e) {
saveWindowState();
}
});
loadAppIcon();
initMenuBar();
initComponents();
// Small delay to let OS/Window Manager settle before checking tray
Timer trayTimer = new Timer(1000, e -> initSystemTray());
trayTimer.setRepeats(false);
trayTimer.start();
}
private void loadAppIcon() {
try {
java.net.URL iconUrl = getClass().getResource("/icon.png");
if (iconUrl != null) {
Image img = Toolkit.getDefaultToolkit().getImage(iconUrl);
setIconImage(img);
System.out.println("Icon loaded successfully via Toolkit.");
} else {
System.err.println("Icon resource not found at /icon.png");
}
} catch (Exception e) {
System.err.println("Could not load app icon: " + e.getMessage());
}
}
private void initSystemTray() {
try {
if (!SystemTray.isSupported()) {
return;
}
SystemTray tray = SystemTray.getSystemTray();
java.net.URL iconUrl = getClass().getResource("/icon.png");
if (iconUrl == null) return;
Image image;
try {
// Read image directly into a BufferedImage for better compatibility with XApp applet
image = javax.imageio.ImageIO.read(iconUrl);
} catch (Exception e) {
image = Toolkit.getDefaultToolkit().getImage(iconUrl);
}
PopupMenu popup = new PopupMenu();
MenuItem showItem = new MenuItem("Show JKeepass");
MenuItem lockItem = new MenuItem("Lock Database");
MenuItem exitItem = new MenuItem("Exit");
showItem.addActionListener(e -> {
setVisible(true);
setExtendedState(JFrame.NORMAL);
toFront();
});
lockItem.addActionListener(e -> lockDatabase());
exitItem.addActionListener(e -> System.exit(0));
popup.add(showItem);
popup.add(lockItem);
popup.addSeparator();
popup.add(exitItem);
trayIcon = new TrayIcon(image, "JKeepass", popup);
trayIcon.setToolTip("JKeepass - Password Manager");
trayIcon.setImageAutoSize(true);
trayIcon.addActionListener(e -> {
setVisible(true);
setExtendedState(JFrame.NORMAL);
toFront();
});
tray.add(trayIcon);
if (statusLabel != null) {
statusLabel.setText(" Ready (SysTray active)");
}
setupWindowListeners();
} catch (Exception e) {
// Silently fail if SystemTray is not available
trayIcon = null;
}
}
private JFrame createRestoreFrame() {
JFrame restoreFrame = new JFrame("JKeepass - Click to Restore");
restoreFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
restoreFrame.setAlwaysOnTop(true);
restoreFrame.setUndecorated(true);
restoreFrame.setOpacity(0.85f);
restoreFrame.setSize(160, 60);
restoreFrame.setLocation(20, 20);
restoreFrame.setType(JFrame.Type.UTILITY);
JButton restoreBtn = new JButton("Restore JKeepass");
restoreBtn.setFont(restoreBtn.getFont().deriveFont(12f));
restoreBtn.addActionListener(e -> {
setVisible(true);
setExtendedState(JFrame.NORMAL);
toFront();
requestFocus();
restoreFrame.dispose();
});
restoreFrame.add(restoreBtn);
return restoreFrame;
}
private void minimizeToCustomTray() {
if (SystemTray.isSupported() && trayIcon != null) {
setVisible(false);
} else {
// Linux Mint doesn't support SystemTray - use floating restore button
lockDatabase();
JFrame restoreFrame = createRestoreFrame();
restoreFrame.setVisible(true);
setVisible(false);
}
}
private void setupWindowListeners() {
for (java.awt.event.WindowListener wl : getWindowListeners()) {
if (!(wl instanceof WindowAdapter) || wl.getClass().getName().contains("KeepassApp")) {
removeWindowListener(wl);
}
}
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
if (trayIcon != null) {
setVisible(false);
} else {
System.exit(0);
}
}
@Override
public void windowIconified(WindowEvent e) {
if (trayIcon != null) {
setVisible(false);
}
}
});
}
private void initMenuBar() {
JMenuBar menuBar = new JMenuBar();
// File Menu
JMenu fileMenu = new JMenu("File");
JMenuItem openItem = new JMenuItem("Open Database...");
recentMenu = new JMenu("Open Recent");
JMenuItem saveItem = new JMenuItem("Save");
JMenuItem minimizeItem = new JMenuItem(SystemTray.isSupported() ? "Minimize to Tray" : "Minimize & Lock");
JMenuItem lockItem = new JMenuItem("Lock Database");
JMenuItem exitItem = new JMenuItem("Exit");
openItem.addActionListener(e -> openDatabase(null));
saveItem.addActionListener(e -> saveDatabase());
minimizeItem.addActionListener(e -> minimizeToCustomTray());
lockItem.addActionListener(e -> lockDatabase());
exitItem.addActionListener(e -> System.exit(0));
fileMenu.add(openItem);
fileMenu.add(recentMenu);
fileMenu.add(saveItem);
fileMenu.addSeparator();
fileMenu.add(lockItem);
fileMenu.add(minimizeItem);
fileMenu.add(exitItem);
updateRecentMenu();
// Entry Menu
JMenu entryMenu = new JMenu("Entry");
JMenuItem addItem = new JMenuItem("Add Entry...");
JMenuItem editItem = new JMenuItem("Edit Entry...");
JMenuItem delItem = new JMenuItem("Delete Entry");
addItem.addActionListener(e -> addEntry());
editItem.addActionListener(e -> editEntry());
delItem.addActionListener(e -> deleteEntry());
entryMenu.add(addItem);
entryMenu.add(editItem);
entryMenu.add(delItem);
// Edit Menu
JMenu editMenu = new JMenu("Edit");
JMenuItem copyUserItem = new JMenuItem("Copy Username");
JMenuItem copyPassItem = new JMenuItem("Copy Password");
copyUserItem.addActionListener(e -> copyToClipboard(true));
copyPassItem.addActionListener(e -> copyToClipboard(false));
editMenu.add(copyUserItem);
editMenu.add(copyPassItem);
// View Menu
JMenu viewMenu = new JMenu("View");
JCheckBoxMenuItem onTopItem = new JCheckBoxMenuItem("Always on Top");
onTopItem.addActionListener(e -> setAlwaysOnTop(onTopItem.isSelected()));
viewMenu.add(onTopItem);
menuBar.add(fileMenu);
menuBar.add(entryMenu);
menuBar.add(editMenu);
menuBar.add(viewMenu);
setJMenuBar(menuBar);
}
private void initComponents() {
// Tree setup
DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode("No database loaded");
treeModel = new DefaultTreeModel(rootNode);
groupTree = new JTree(treeModel);
groupTree.addTreeSelectionListener(e -> {
DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) groupTree.getLastSelectedPathComponent();
if (selectedNode != null && selectedNode.getUserObject() instanceof GroupWrapper) {
displayEntriesInGroup(((GroupWrapper) selectedNode.getUserObject()).getGroup());
}
});
groupTree.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
showTreePopupMenu(e);
}
@Override
public void mouseReleased(MouseEvent e) {
showTreePopupMenu(e);
}
private void showTreePopupMenu(MouseEvent e) {
if (e.isPopupTrigger()) {
TreePath path = groupTree.getPathForLocation(e.getX(), e.getY());
if (path != null) {
groupTree.setSelectionPath(path);
DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) path.getLastPathComponent();
if (selectedNode.getUserObject() instanceof GroupWrapper) {
Group group = ((GroupWrapper) selectedNode.getUserObject()).getGroup();
JPopupMenu popup = new JPopupMenu();
JMenuItem addGroupItem = new JMenuItem("Add Category...");
addGroupItem.addActionListener(al -> addCategory(group));
popup.add(addGroupItem);
// Add Delete Category option (not for root group)
if (selectedNode.getParent() != null) {
popup.addSeparator();
JMenuItem deleteGroupItem = new JMenuItem("Delete Category...");
deleteGroupItem.addActionListener(al -> deleteCategory(group, selectedNode));
popup.add(deleteGroupItem);
}
popup.show(e.getComponent(), e.getX(), e.getY());
}
}
}
}
});
JScrollPane treeScrollPane = new JScrollPane(groupTree);
// Notes area setup
notesArea = new JTextArea(5, 20);
notesArea.setEditable(false);
notesArea.setLineWrap(true);
notesArea.setWrapStyleWord(true);
JScrollPane notesScrollPane = new JScrollPane(notesArea);
notesScrollPane.setBorder(BorderFactory.createTitledBorder("Notes"));
// Table setup
String[] columnNames = {"Title", "Username", "Password", "URL", "Notes"};
tableModel = new DefaultTableModel(columnNames, 0) {
@Override
public boolean isCellEditable(int row, int column) {
return false;
}
};
entryTable = new JTable(tableModel);
entryTable.setAutoCreateRowSorter(true);
entryTable.getSelectionModel().addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) {
int selectedRow = entryTable.getSelectedRow();
if (selectedRow >= 0) {
int modelRow = entryTable.convertRowIndexToModel(selectedRow);
notesArea.setText(currentEntries.get(modelRow).getNotes());
} else {
notesArea.setText("");
}
}
});
entryTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
int row = entryTable.rowAtPoint(e.getPoint());
int col = entryTable.columnAtPoint(e.getPoint());
if (row >= 0 && col >= 0) {
int modelRow = entryTable.convertRowIndexToModel(row);
Entry entry = currentEntries.get(modelRow);
String content = switch (col) {
case 0 -> entry.getTitle();
case 1 -> entry.getUsername();
case 2 -> entry.getPassword();
case 3 -> entry.getUrl();
case 4 -> entry.getNotes();
default -> "";
};
copyTextToClipboard(content, entryTable.getColumnName(col));
}
}
}
@Override
public void mousePressed(MouseEvent e) {
showPopupMenu(e);
}
@Override
public void mouseReleased(MouseEvent e) {
showPopupMenu(e);
}
private void showPopupMenu(MouseEvent e) {
if (e.isPopupTrigger()) {
int row = entryTable.rowAtPoint(e.getPoint());
if (row >= 0) {
entryTable.setRowSelectionInterval(row, row);
}
JPopupMenu popup = new JPopupMenu();
JMenuItem addPop = new JMenuItem("Add Entry");
JMenuItem editPop = new JMenuItem("Edit");
JMenuItem copyPop = new JMenuItem("Copy Username");
JMenuItem copyPassPop = new JMenuItem("Copy Password");
JMenuItem delPop = new JMenuItem("Delete");
addPop.addActionListener(al -> addEntry());
editPop.addActionListener(al -> editEntry());
copyPop.addActionListener(al -> copyToClipboard(true));
copyPassPop.addActionListener(al -> copyToClipboard(false));
delPop.addActionListener(al -> deleteEntry());
popup.add(addPop);
popup.add(editPop);
popup.addSeparator();
popup.add(copyPop);
popup.add(copyPassPop);
popup.addSeparator();
popup.add(delPop);
popup.show(e.getComponent(), e.getX(), e.getY());
}
}
});
JScrollPane tableScrollPane = new JScrollPane(entryTable);
// Right side: Table and Notes
rightSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, tableScrollPane, notesScrollPane);
int rightDivider = prefs.getInt(PREF_RIGHT_DIVIDER, 250);
rightSplitPane.setDividerLocation(rightDivider);
rightSplitPane.addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, e -> saveComponentState());
// Main Split pane
mainSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, treeScrollPane, rightSplitPane);
int mainDivider = prefs.getInt(PREF_MAIN_DIVIDER, 200);
mainSplitPane.setDividerLocation(mainDivider);
mainSplitPane.addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, e -> saveComponentState());
add(mainSplitPane, BorderLayout.CENTER);
// Status bar setup
statusLabel = new JLabel(" Ready");
statusLabel.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5));
JButton lockBtn = new JButton("Lock");
lockBtn.setFocusable(false);
lockBtn.setMargin(new Insets(0, 5, 0, 5));
lockBtn.addActionListener(e -> lockDatabase());
JPanel statusBar = new JPanel(new BorderLayout());
statusBar.setPreferredSize(new Dimension(getWidth(), 30));
statusBar.setBorder(BorderFactory.createEtchedBorder());
statusBar.add(statusLabel, BorderLayout.WEST);
statusBar.add(lockBtn, BorderLayout.EAST);
add(statusBar, BorderLayout.PAGE_END);
// Tray diagnostic
if (!SystemTray.isSupported()) {
statusLabel.setText(" Ready (SysTray not supported - use Lock button)");
} else if (trayIcon == null) {
statusLabel.setText(" Ready (SysTray icon failed)");
} else {
statusLabel.setText(" Ready (SysTray active)");
}
}
/**
* Save current window position, size, and component layout to preferences
*/
private void saveWindowState() {
Point location = getLocation();
Dimension size = getSize();
prefs.putInt(PREF_WINDOW_X, location.x);
prefs.putInt(PREF_WINDOW_Y, location.y);
prefs.putInt(PREF_WINDOW_WIDTH, size.width);
prefs.putInt(PREF_WINDOW_HEIGHT, size.height);
}
/**
* Save component layout (divider positions) to preferences
*/
private void saveComponentState() {
if (mainSplitPane != null) {
prefs.putInt(PREF_MAIN_DIVIDER, mainSplitPane.getDividerLocation());
}
if (rightSplitPane != null) {
prefs.putInt(PREF_RIGHT_DIVIDER, rightSplitPane.getDividerLocation());
}
}
private void updateRecentMenu() {
recentMenu.removeAll();
String recentFiles = prefs.get(PREF_RECENT_LIST, "");
if (recentFiles.isEmpty()) {
JMenuItem noneItem = new JMenuItem("No recent files");
noneItem.setEnabled(false);
recentMenu.add(noneItem);
} else {
String[] paths = recentFiles.split(";");
for (String path : paths) {
if (path.isEmpty()) continue;
JMenuItem item = new JMenuItem(path);
item.addActionListener(e -> {
File file = new File(path);
if (file.exists()) {
openDatabase(file);
} else {
JOptionPane.showMessageDialog(this, "File not found: " + path, "Error", JOptionPane.ERROR_MESSAGE);
removeRecentFile(path);
}
});
recentMenu.add(item);
}
}
}
private void saveRecentFile(String path) {
String recentFiles = prefs.get(PREF_RECENT_LIST, "");
List<String> list = new ArrayList<>(List.of(recentFiles.split(";")));
list.remove(""); // Clean up
list.remove(path); // Avoid duplicates
list.add(0, path); // Add to top
if (list.size() > MAX_RECENT_FILES) {
list = list.subList(0, MAX_RECENT_FILES);
}
prefs.put(PREF_RECENT_LIST, String.join(";", list));
updateRecentMenu();
}
private void removeRecentFile(String path) {
String recentFiles = prefs.get(PREF_RECENT_LIST, "");
List<String> list = new ArrayList<>(List.of(recentFiles.split(";")));
list.remove(path);
prefs.put(PREF_RECENT_LIST, String.join(";", list));
updateRecentMenu();
}
private void openDatabase(File selectedFile) {
if (selectedFile == null) {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("JKeepass Database (*.jkp)", "jkp"));
if (fileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
selectedFile = fileChooser.getSelectedFile();
} else {
return;
}
}
JPasswordField pf = new JPasswordField();
pf.addAncestorListener(new AncestorListener() {
@Override
public void ancestorAdded(AncestorEvent event) {
SwingUtilities.invokeLater(pf::requestFocusInWindow);
}
@Override public void ancestorRemoved(AncestorEvent event) {}
@Override public void ancestorMoved(AncestorEvent event) {}
});
int okCxl = JOptionPane.showConfirmDialog(this, pf, "Enter Master Password:", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
if (okCxl == JOptionPane.OK_OPTION) {
String password = new String(pf.getPassword());
try {
// Load JKP format
this.database = CustomDatabaseFormat.convertJkpToDatabase(selectedFile, password);
this.currentFile = selectedFile;
this.currentPassword = password;
updateTree(this.database.getRootGroup());
saveRecentFile(selectedFile.getAbsolutePath());
statusLabel.setText(" Database loaded: " + selectedFile.getName());
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "Error loading database: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
ex.printStackTrace();
}
}
}
private void saveDatabase() {
if (database == null || currentFile == null || currentPassword == null) {
JOptionPane.showMessageDialog(this, "No database open to save.", "Warning", JOptionPane.WARNING_MESSAGE);
return;
}
try {
// Save as custom JKP format
CustomDatabaseFormat.saveDatabase(currentFile, database, currentPassword);
statusLabel.setText(" Database saved successfully.");
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "Error saving database: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
ex.printStackTrace();
}
}
private void lockDatabase() {
if (database == null) return;
// Clear sensitive data
database = null;
currentGroup = null;
currentEntries.clear();
tableModel.setRowCount(0);
notesArea.setText("");
treeModel.setRoot(new DefaultMutableTreeNode("Database locked"));
statusLabel.setText(" Database locked.");
// Re-open password prompt
if (currentFile != null) {
File toOpen = currentFile;
// Clear current file/pass so we don't think it's still open
currentPassword = null;
currentFile = null;
openDatabase(toOpen);
}
}
private void addEntry() {
if (currentGroup == null) {
JOptionPane.showMessageDialog(this, "Please select a folder first.", "Warning", JOptionPane.WARNING_MESSAGE);
return;
}
EntryDialog dialog = new EntryDialog(this, "Add Entry", null);
if (dialog.showDialog()) {
Entry entry = currentGroup.addEntry(database.newEntry());
updateEntryFromDialog(entry, dialog);
displayEntriesInGroup(currentGroup);
statusLabel.setText(" Entry added. Remember to Save.");
}
}
private void editEntry() {
int selectedRow = entryTable.getSelectedRow();
if (selectedRow >= 0) {
int modelRow = entryTable.convertRowIndexToModel(selectedRow);
Entry entry = currentEntries.get(modelRow);
EntryDialog dialog = new EntryDialog(this, "Edit Entry", entry);
if (dialog.showDialog()) {
updateEntryFromDialog(entry, dialog);
displayEntriesInGroup(currentGroup);
statusLabel.setText(" Entry updated. Remember to Save.");
}
} else {
JOptionPane.showMessageDialog(this, "Please select an entry to edit.", "Warning", JOptionPane.WARNING_MESSAGE);
}
}
private void deleteEntry() {
int selectedRow = entryTable.getSelectedRow();
if (selectedRow >= 0) {
int confirm = JOptionPane.showConfirmDialog(this, "Are you sure you want to delete this entry?", "Confirm Delete", JOptionPane.YES_NO_OPTION);
if (confirm == JOptionPane.YES_OPTION) {
int modelRow = entryTable.convertRowIndexToModel(selectedRow);
Entry entry = currentEntries.get(modelRow);
currentGroup.removeEntry(entry);
displayEntriesInGroup(currentGroup);
statusLabel.setText(" Entry deleted. Remember to Save.");
}
} else {
JOptionPane.showMessageDialog(this, "Please select an entry to delete.", "Warning", JOptionPane.WARNING_MESSAGE);
}
}
private void updateEntryFromDialog(Entry entry, EntryDialog dialog) {
entry.setTitle(dialog.getTitleText());
entry.setUsername(dialog.getUsernameText());
entry.setPassword(dialog.getPasswordText());
entry.setUrl(dialog.getUrlText());
entry.setNotes(dialog.getNotesText());
}
private void addCategory(Group parentGroup) {
String name = JOptionPane.showInputDialog(this, "Enter name for the new category:", "Add Category", JOptionPane.QUESTION_MESSAGE);
if (name != null && !name.trim().isEmpty()) {
parentGroup.addGroup(database.newGroup(name.trim()));
updateTree(database.getRootGroup());
statusLabel.setText(" Category added. Remember to Save.");
}
}
private void deleteCategory(Group group, DefaultMutableTreeNode node) {
// Get parent group from parent node
DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) node.getParent();
if (parentNode == null || !(parentNode.getUserObject() instanceof GroupWrapper)) {
JOptionPane.showMessageDialog(this, "Cannot delete root category.", "Error", JOptionPane.ERROR_MESSAGE);
return;
}
// Check if category has subgroups or entries
int subGroupCount = group.getGroups().size();
int entryCount = group.getEntries().size();
String message = "Delete category \"" + group.getName() + "\"?";
if (subGroupCount > 0) {
message += "\nThis category has " + subGroupCount + " subcategory/categories.";
}
if (entryCount > 0) {
message += "\nThis category has " + entryCount + " entry/entries.";
}
if (subGroupCount > 0 || entryCount > 0) {
message += "\n\nWarning: All contents will be deleted!";
}
message += "\n\nThis action cannot be undone!";
int result = JOptionPane.showConfirmDialog(this, message, "Delete Category",
JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
if (result == JOptionPane.YES_OPTION) {
Group parentGroup = ((GroupWrapper) parentNode.getUserObject()).getGroup();
parentGroup.removeGroup(group);
updateTree(database.getRootGroup());
statusLabel.setText(" Category deleted. Remember to Save.");
}
}
private void displayEntriesInGroup(Group group) {
this.currentGroup = group;
tableModel.setRowCount(0);
currentEntries.clear();
for (Object entryObj : group.getEntries()) {
Entry entry = (Entry) entryObj;
currentEntries.add(entry);
tableModel.addRow(new Object[]{
entry.getTitle(),
entry.getUsername(),
"********",
entry.getUrl(),
entry.getNotes()
});
}
}
/**
* Dialog for adding/editing entries
*/
private static class EntryDialog extends JDialog {
private final JTextField titleField = new JTextField(20);
private final JTextField userField = new JTextField(20);
private final JPasswordField passField = new JPasswordField(20);
private final JTextField urlField = new JTextField(20);
private final JTextArea notesArea = new JTextArea(5, 20);
private boolean confirmed = false;
public EntryDialog(Frame owner, String title, Entry entry) {
super(owner, title, true);
setLayout(new BorderLayout());
titleField.addAncestorListener(new AncestorListener() {
@Override
public void ancestorAdded(AncestorEvent event) {
SwingUtilities.invokeLater(titleField::requestFocusInWindow);
}
@Override public void ancestorRemoved(AncestorEvent event) {}
@Override public void ancestorMoved(AncestorEvent event) {}
});
JPanel panel = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(5, 5, 5, 5);
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.gridx = 0; gbc.gridy = 0;
panel.add(new JLabel(" Title:"), gbc);
gbc.gridx = 1; gbc.gridwidth = 2;
panel.add(titleField, gbc);
gbc.gridx = 0; gbc.gridy = 1; gbc.gridwidth = 1;
panel.add(new JLabel(" Username:"), gbc);
gbc.gridx = 1; gbc.gridwidth = 2;
panel.add(userField, gbc);
gbc.gridx = 0; gbc.gridy = 2; gbc.gridwidth = 1;
panel.add(new JLabel(" Password:"), gbc);
gbc.gridx = 1;
panel.add(passField, gbc);
JPanel passBtnOptionPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
JCheckBox showPassCheck = new JCheckBox("Show");
showPassCheck.addActionListener(e -> {
if (showPassCheck.isSelected()) {
passField.setEchoChar((char) 0);
} else {
passField.setEchoChar('•');
}
});
JButton genBtn = new JButton("Generate");
genBtn.addActionListener(e -> passField.setText(generateRandomPassword(16)));
passBtnOptionPanel.add(showPassCheck);
passBtnOptionPanel.add(genBtn);
gbc.gridx = 2;
panel.add(passBtnOptionPanel, gbc);
gbc.gridx = 0; gbc.gridy = 3; gbc.gridwidth = 1;
panel.add(new JLabel(" URL:"), gbc);
gbc.gridx = 1; gbc.gridwidth = 2;
panel.add(urlField, gbc);
if (entry != null) {
titleField.setText(entry.getTitle());
userField.setText(entry.getUsername());
passField.setText(entry.getPassword());
urlField.setText(entry.getUrl());
notesArea.setText(entry.getNotes());
}
add(panel, BorderLayout.NORTH);
JScrollPane scrollPane = new JScrollPane(notesArea);
scrollPane.setBorder(BorderFactory.createTitledBorder("Notes"));
add(scrollPane, BorderLayout.CENTER);
JPanel btnPanel = new JPanel();
JButton okBtn = new JButton("OK");
JButton cancelBtn = new JButton("Cancel");
okBtn.addActionListener(e -> { confirmed = true; setVisible(false); });
cancelBtn.addActionListener(e -> setVisible(false));
btnPanel.add(okBtn);
btnPanel.add(cancelBtn);
add(btnPanel, BorderLayout.SOUTH);
pack();
setLocationRelativeTo(owner);
}
public boolean showDialog() {
setVisible(true);
return confirmed;
}
public String getTitleText() { return titleField.getText(); }
public String getUsernameText() { return userField.getText(); }
public String getPasswordText() { return new String(passField.getPassword()); }
public String getUrlText() { return urlField.getText(); }
public String getNotesText() { return notesArea.getText(); }
private String generateRandomPassword(int length) {
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+";
SecureRandom random = new SecureRandom();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append(chars.charAt(random.nextInt(chars.length())));
}
return sb.toString();
}
}
private void updateTree(Group rootGroup) {
DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(new GroupWrapper(rootGroup));
populateTreeRecursive(rootNode, rootGroup);
treeModel.setRoot(rootNode);
// Automatically select root and display its entries
groupTree.setSelectionRow(0);
displayEntriesInGroup(rootGroup);
}
private void populateTreeRecursive(DefaultMutableTreeNode node, Group group) {
for (Object subGroupObj : group.getGroups()) {
Group subGroup = (Group) subGroupObj;
DefaultMutableTreeNode childNode = new DefaultMutableTreeNode(new GroupWrapper(subGroup));
node.add(childNode);
populateTreeRecursive(childNode, subGroup);
}
}
/**
* Helper class to display group name in JTree while keeping reference to Group object.
*/
private static class GroupWrapper {
private final Group group;
public GroupWrapper(Group group) {
this.group = group;
}
public Group getGroup() {
return group;
}
@Override
public String toString() {
return group.getName();
}
}
private void copyToClipboard(boolean isUsername) {
int selectedRow = entryTable.getSelectedRow();
if (selectedRow >= 0) {
int modelRow = entryTable.convertRowIndexToModel(selectedRow);
Entry entry = currentEntries.get(modelRow);
String text = isUsername ? entry.getUsername() : entry.getPassword();
String label = isUsername ? "Username" : "Password";
copyTextToClipboard(text, label);
} else {
JOptionPane.showMessageDialog(this, "Please select an entry first.");
}
}
private void copyTextToClipboard(String text, String label) {
if (text != null) {
StringSelection selection = new StringSelection(text);
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, selection);
statusLabel.setText(" " + label + " copied to clipboard.");
Timer timer = new Timer(3000, e -> statusLabel.setText(" Ready"));
timer.setRepeats(false);
timer.start();
}
}
public static void main(String[] args) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception ignored) {}
SwingUtilities.invokeLater(() -> {
new KeepassApp().setVisible(true);
});
}
}

View File

@ -0,0 +1,24 @@
package cz.kamma.jkeepass.model;
/**
* Custom Database implementation (replaces KeePassJava2 dependency)
*/
public class Database {
private Group rootGroup;
public Database() {
this.rootGroup = new Group("Root");
}
public Group getRootGroup() {
return rootGroup;
}
public Entry newEntry() {
return new Entry();
}
public Group newGroup(String name) {
return new Group(name);
}
}

View File

@ -0,0 +1,52 @@
package cz.kamma.jkeepass.model;
/**
* Custom Entry implementation (replaces KeePassJava2 dependency)
*/
public class Entry {
private String title = "";
private String username = "";
private String password = "";
private String url = "";
private String notes = "";
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title != null ? title : "";
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username != null ? username : "";
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password != null ? password : "";
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url != null ? url : "";
}
public String getNotes() {
return notes;
}
public void setNotes(String notes) {
this.notes = notes != null ? notes : "";
}
}

View File

@ -0,0 +1,51 @@
package cz.kamma.jkeepass.model;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Custom Group implementation (replaces KeePassJava2 dependency)
*/
public class Group {
private String name;
private List<Group> groups = new ArrayList<>();
private List<Entry> entries = new ArrayList<>();
public Group(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Collection<Group> getGroups() {
return new ArrayList<>(groups);
}
public Collection<Entry> getEntries() {
return new ArrayList<>(entries);
}
public void addGroup(Group group) {
groups.add(group);
}
public void removeGroup(Group group) {
groups.remove(group);
}
public void removeEntry(Entry entry) {
entries.remove(entry);
}
public Entry addEntry(Entry entry) {
entries.add(entry);
return entry;
}
}

BIN
src/main/resources/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB