Some keepass work

This commit is contained in:
crschnick
2025-03-21 14:04:35 +00:00
parent 610cd08842
commit d846b1101b
8 changed files with 1841 additions and 2 deletions
@@ -17,6 +17,7 @@ import io.xpipe.app.icon.SystemIconManager;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.prefs.KeePassClient;
import io.xpipe.app.resources.*;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStorageSyncHandler;
@@ -174,6 +175,7 @@ public class BaseMode extends OperationMode {
ProcessControlProvider.get().reset();
AppPrefs.reset();
AppBeaconServer.reset();
KeePassClient.reset();
StoreViewState.reset();
AppLayoutModel.reset();
AppTheme.reset();
@@ -0,0 +1,14 @@
package io.xpipe.app.prefs;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
@Value
@Jacksonized
@Builder
public class KeePassAssociationKey {
String id;
String key;
String hash;
}
@@ -0,0 +1,107 @@
package io.xpipe.app.prefs;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.LocalShell;
import io.xpipe.app.util.WindowsRegistry;
import io.xpipe.core.process.OsType;
import lombok.SneakyThrows;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
public class KeePassClient {
private static KeePassNativeClient client;
@SneakyThrows
public static String receive(String key) {
var client = getOrCreate();
client.getDatabaseGroups();
return client.getLogins("abc");
}
public static void reset() {
if (client != null) {
client.disconnect();
client = null;
}
}
private static synchronized KeePassNativeClient getOrCreate() throws Exception {
if (client == null) {
var found = findKeePassProxy();
if (found.isEmpty()) {
throw ErrorEvent.expected(new UnsupportedOperationException("No KeePassXC installation was found"));
}
var c = new KeePassNativeClient(found.get());
c.connect();
c.exchangeKeys();
KeePassAssociationKey cached = AppCache.getNonNull("keepassxc-association", KeePassAssociationKey.class, () -> null);
if (cached != null) {
c.useExistingAssociationKey(cached);
try {
c.testAssociation();
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
c.useExistingAssociationKey(null);
cached = null;
}
}
if (cached == null) {
c.associate();
c.testAssociation();
AppCache.update("keepassxc-association", c.getAssociationKey());
}
client = c;
}
return client;
}
private static Optional<Path> findKeePassProxy() {
try (var sc = LocalShell.getShell().start()) {
var found = sc.view().findProgram("keepassxc-proxy");
if (found.isPresent()) {
return found.map(filePath -> filePath.asLocalPath());
}
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
}
return switch (OsType.getLocal()) {
case OsType.Linux linux -> {
var paths = List.of(Path.of("/usr/bin/keepassxc-proxy"), Path.of("/usr/local/bin/keepassxc-proxy"));
yield paths.stream().filter(path -> Files.exists(path)).findFirst();
}
case OsType.MacOs macOs -> {
var paths = List.of(Path.of("/Applications/KeePassXC.app/Contents/MacOS/keepassxc-proxy"));
yield paths.stream().filter(path -> Files.exists(path)).findFirst();
}
case OsType.Windows windows -> {
try {
var foundKey = WindowsRegistry.local()
.findKeyForEqualValueMatchRecursive(
WindowsRegistry.HKEY_LOCAL_MACHINE,
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
"https://keepassxc.org");
if (foundKey.isPresent()) {
var installKey = WindowsRegistry.local()
.readStringValueIfPresent(
foundKey.get().getHkey(), foundKey.get().getKey(), "InstallLocation");
if (installKey.isPresent()) {
yield installKey.map(p -> p + "\\keepassxc-proxy.exe").map(Path::of);
}
}
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
}
yield Optional.empty();
}
};
}
}
@@ -0,0 +1,836 @@
package io.xpipe.app.prefs;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.ThreadHelper;
import lombok.Getter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Client for communicating with KeePassXC using the native messaging protocol.
* This implementation communicates with the actual running KeePassXC-proxy process
* via stdin and stdout.
*
* Native messaging uses length-prefixed JSON messages over stdin/stdout.
*/
public class KeePassNativeClient {
// Default timeouts for different operations (milliseconds)
private static final long TIMEOUT_ASSOCIATE = 30000; // Associate needs user interaction
private static final long TIMEOUT_GET_LOGINS = 5000; // Getting logins is usually fast
private static final long TIMEOUT_TEST_ASSOCIATE = 2000; // Testing association is quick
private static final long TIMEOUT_GET_DATABASE_GROUPS = 3000; // Getting database groups
private final Path proxyExecutable;
private Process process;
private String clientId;
private TweetNaClHelper.KeyPair keyPair;
private byte[] serverPublicKey;
private boolean connected = false;
private boolean associated = false;
private Thread responseHandler;
@Getter
private KeePassAssociationKey associationKey;
// Message buffer for handling requests/responses
private final MessageBuffer messageBuffer = new MessageBuffer();
private final Object responseNotifier = new Object();
// Flag to indicate if key exchange is in progress
private volatile boolean keyExchangeInProgress = false;
public KeePassNativeClient(Path proxyExecutable) {this.proxyExecutable = proxyExecutable;}
public void useExistingAssociationKey(KeePassAssociationKey key) {
this.associationKey = key;
}
/**
* Connects to KeePassXC via the provided input and output streams.
* In a real application, these would be the streams connecting to KeePassXC.
*
* @return True if connection was successful, false otherwise
* @throws IOException If there's an error connecting to KeePassXC
*/
public void connect() throws IOException {
// Generate a random client ID (24 bytes base64 encoded)
this.clientId = TweetNaClHelper.encodeBase64(TweetNaClHelper.randomBytes(TweetNaClHelper.NONCE_SIZE));
// Generate actual cryptographic keys
this.keyPair = TweetNaClHelper.generateKeyPair();
var pb = new ProcessBuilder(List.of(proxyExecutable.toString()));
this.process = pb.start();
// Start a thread to handle responses
responseHandler = new Thread(this::handleResponses);
responseHandler.setDaemon(true);
responseHandler.start();
connected = true;
}
/**
* Performs a key exchange with KeePassXC.
*
* @return True if the key exchange was successful, false otherwise
* @throws IOException If there's an error communicating with KeePassXC
*/
public void exchangeKeys() throws IOException {
// Generate a nonce
byte[] nonceBytes = TweetNaClHelper.randomBytes(TweetNaClHelper.NONCE_SIZE);
String nonce = TweetNaClHelper.encodeBase64(nonceBytes);
// Convert our public key to base64
String publicKeyB64 = TweetNaClHelper.encodeBase64(keyPair.getPublicKey());
// Build the key exchange message - NOTE: This is NOT encrypted
String requestId = UUID.randomUUID().toString();
Map<String, Object> messageMap = new HashMap<>();
messageMap.put("action", "change-public-keys");
messageMap.put("publicKey", publicKeyB64);
messageMap.put("nonce", nonce);
messageMap.put("clientID", clientId);
messageMap.put("requestId", requestId);
// Convert to JSON string
String keyExchangeMessage = mapToJson(messageMap);
// Send the message directly
long startTime = System.currentTimeMillis();
sendNativeMessage(keyExchangeMessage);
// Wait for a direct response rather than using CompletableFuture
// This is a special case because we can't use the encryption yet
long timeout = 3000; // 3 seconds for key exchange
while (System.currentTimeMillis() - startTime < timeout) {
if (process.getInputStream().available() > 0) {
String response = receiveNativeMessage();
if (response.contains("change-public-keys")) {
// Use regex to extract the public key to avoid any JSON parsing issues
Pattern pattern = Pattern.compile("\"publicKey\":\"([^\"]+)\"");
Matcher matcher = pattern.matcher(response);
if (matcher.find()) {
String serverPubKeyB64 = matcher.group(1);
// Store the server's public key
this.serverPublicKey = TweetNaClHelper.decodeBase64(serverPubKeyB64);
// Check for success in the response
boolean success = response.contains("\"success\":\"true\"");
if (!success) {
throw new IllegalStateException("Key exchange failed");
} else {
return;
}
}
}
}
// Small sleep to prevent CPU hogging
ThreadHelper.sleep(50);
}
throw new IllegalStateException("Key exchanged timed out");
}
/**
* Tests the association with KeePassXC.
*
* @return True if associated, false otherwise
* @throws IOException If there's an error communicating with KeePassXC
*/
public void testAssociation() throws IOException {
if (associationKey == null) {
// We need to do an association first
throw ErrorEvent.expected(new IllegalStateException("KeePassXC association failed or was cancelled"));
}
// Generate a nonce
String nonce = TweetNaClHelper.encodeBase64(TweetNaClHelper.randomBytes(TweetNaClHelper.NONCE_SIZE));
// Create the unencrypted message
Map<String, Object> messageData = new HashMap<>();
messageData.put("action", "test-associate");
messageData.put("id", associationKey.getId());
messageData.put("key", associationKey.getKey());
// Encrypt the message
String encryptedMessage = encrypt(messageData, nonce);
if (encryptedMessage == null) {
throw new IllegalStateException("Failed to encrypt test-associate message");
}
// Build the request
Map<String, Object> request = new HashMap<>();
request.put("action", "test-associate");
request.put("message", encryptedMessage);
request.put("nonce", nonce);
request.put("clientID", clientId);
String requestJson = mapToJson(request);
// Send the request
String responseJson = sendRequest("test-associate", requestJson, TIMEOUT_TEST_ASSOCIATE);
if (responseJson == null) {
throw new IllegalStateException("No response received from associated instance");
}
// Parse and decrypt the response
Map<String, Object> responseMap = jsonToMap(responseJson);
if (responseMap.containsKey("message") && responseMap.containsKey("nonce")) {
String encryptedResponse = (String) responseMap.get("message");
String responseNonce = (String) responseMap.get("nonce");
String decryptedResponse = decrypt(encryptedResponse, responseNonce);
if (decryptedResponse != null) {
Map<String, Object> parsedResponse = jsonToMap(decryptedResponse);
boolean success = parsedResponse.containsKey("success") &&
"true".equals(parsedResponse.get("success").toString());
if (success) {
associated = true;
} else {
throw new IllegalStateException("KeePassXC association failed");
}
}
}
}
/**
* Retrieves credentials from KeePassXC.
*
* @param url The URL to get credentials for
* @return The response JSON, or null if failed
* @throws IOException If there's an error communicating with KeePassXC
*/
public String getLogins(String url) throws IOException {
// Generate a nonce
String nonce = TweetNaClHelper.encodeBase64(TweetNaClHelper.randomBytes(TweetNaClHelper.NONCE_SIZE));
// Create the unencrypted message
Map<String, Object> messageData = new HashMap<>();
messageData.put("action", "get-logins");
messageData.put("url", url);
// Add the keys
Map<String, Object> keyData = new HashMap<>();
keyData.put("id", associationKey.getId());
keyData.put("key", associationKey.getKey());
messageData.put("keys", new Map[] { keyData });
// Encrypt the message
String encryptedMessage = encrypt(messageData, nonce);
if (encryptedMessage == null) {
return null;
}
// Build the request
Map<String, Object> request = new HashMap<>();
request.put("action", "get-logins");
request.put("message", encryptedMessage);
request.put("nonce", nonce);
request.put("clientID", clientId);
String requestJson = mapToJson(request);
System.out.println("Sending get-logins message: " + requestJson);
// Send the request
String responseJson = sendRequest("get-logins", requestJson, TIMEOUT_GET_LOGINS);
if (responseJson == null) {
return null;
}
// Parse and decrypt the response
try {
Map<String, Object> responseMap = jsonToMap(responseJson);
if (responseMap.containsKey("message") && responseMap.containsKey("nonce")) {
String encryptedResponse = (String) responseMap.get("message");
String responseNonce = (String) responseMap.get("nonce");
return decrypt(encryptedResponse, responseNonce);
}
} catch (Exception e) {
System.err.println("Error processing get-logins response: " + e.getMessage());
}
return null;
}
/**
* Disconnects from KeePassXC.
*/
public void disconnect() {
if (responseHandler != null) {
responseHandler.interrupt();
responseHandler = null;
}
process.destroy();
process = null;
}
/**
* Sends a request to KeePassXC and waits for a response.
*
* @param action The action being performed (e.g., "associate", "get-logins")
* @param message The JSON message to send
* @param timeout The timeout in milliseconds
* @return The response JSON, or null if timed out
* @throws IOException If there's an error communicating with KeePassXC
*/
private String sendRequest(String action, String message, long timeout) throws IOException {
String requestId = extractRequestId(message);
if (requestId == null) {
// If no requestId in the message, generate one for tracking
requestId = UUID.randomUUID().toString();
}
// Create a completable future for this request
CompletableFuture<String> responseFuture = new CompletableFuture<>();
// Create a pending request and add it to the message buffer
PendingRequest request = new PendingRequest(requestId, action, responseFuture, timeout);
messageBuffer.addRequest(request);
// Send the message
sendNativeMessage(message);
// Notify the response handler that we've sent a message
synchronized (responseNotifier) {
responseNotifier.notify();
}
try {
// Wait for the response with the specified timeout
return responseFuture.get(timeout, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("Request interrupted: " + e.getMessage());
return null;
} catch (ExecutionException e) {
System.err.println("Error in request execution: " + e.getMessage());
return null;
} catch (TimeoutException e) {
System.err.println("Request timed out after " + timeout + "ms: " + action);
return null;
} finally {
// Clean up timed-out requests
messageBuffer.cleanupTimedOutRequests();
}
}
/**
* Extracts the requestId from a JSON message.
*
* @param message The JSON message
* @return The requestId, or null if not found
*/
private String extractRequestId(String message) {
return MessageBuffer.extractRequestId(message);
}
/**
* Continuously reads and processes responses from KeePassXC.
*/
private void handleResponses() {
try {
while (!Thread.currentThread().isInterrupted()) {
// If key exchange is in progress, skip normal message handling
if (keyExchangeInProgress) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
continue;
}
// Check if there's anything to read
boolean hasData = false;
try {
hasData = process.getInputStream().available() > 0;
} catch (IOException e) {
System.err.println("Error checking input stream: " + e.getMessage());
continue;
}
if (hasData) {
try {
String response = receiveNativeMessage();
if (response != null) {
processResponse(response);
}
} catch (IOException e) {
System.err.println("Error reading response: " + e.getMessage());
}
} else {
// If nothing to read, wait efficiently
try {
synchronized (responseNotifier) {
responseNotifier.wait(100); // Wait up to 100ms for notification
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
// Periodically check for timed-out requests
messageBuffer.cleanupTimedOutRequests();
}
} catch (Exception e) {
System.err.println("Error in response handler: " + e.getMessage());
}
}
/**
* Process a response from KeePassXC.
*
* @param response The JSON response
*/
private void processResponse(String response) {
System.out.println("Received response: " + response);
try {
// Extract action
String action = MessageBuffer.extractAction(response);
// Special handling for action-specific responses
if ("database-locked".equals(action) || "database-unlocked".equals(action)) {
System.out.println("Database state changed: " + action);
// Update state based on the action
if ("database-locked".equals(action)) {
associated = false;
}
// Notify any waiting requests
messageBuffer.handleResponse(response);
return;
}
// Standard response handling - use the message buffer to complete the appropriate request
int completedCount = messageBuffer.handleResponse(response);
if (completedCount == 0) {
System.out.println("Warning: Response did not match any pending request: " + response);
}
} catch (Exception e) {
System.err.println("Error processing response: " + e.getMessage());
}
}
/**
* Sends a message to KeePassXC using the native messaging protocol.
* The message is prefixed with a 32-bit length (little-endian).
*
* @param message The JSON message to send
* @throws IOException If there's an error writing to the stream
*/
private void sendNativeMessage(String message) throws IOException {
byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8);
int length = messageBytes.length;
// Create a ByteBuffer with length in little-endian format
ByteBuffer lengthBuffer = ByteBuffer.allocate(4);
lengthBuffer.order(ByteOrder.LITTLE_ENDIAN);
lengthBuffer.putInt(length);
process.getOutputStream().write(lengthBuffer.array());
process.getOutputStream().write(messageBytes);
process.getOutputStream().flush();
}
/**
* Receives a message from KeePassXC using the native messaging protocol.
* The message is prefixed with a 32-bit length (little-endian).
*
* @return The received JSON message as a string, or null if reading failed
* @throws IOException If there's an error reading from the stream
*/
private String receiveNativeMessage() throws IOException {
// Read the length prefix (4 bytes, little-endian)
byte[] lengthBytes = new byte[4];
int bytesRead = process.getInputStream().read(lengthBytes);
if (bytesRead != 4) {
throw new IOException("Error reading received message");
}
// Convert bytes to integer (little-endian)
ByteBuffer lengthBuffer = ByteBuffer.wrap(lengthBytes);
lengthBuffer.order(ByteOrder.LITTLE_ENDIAN);
int messageLength = lengthBuffer.getInt();
// Read the actual message
byte[] messageBytes = new byte[messageLength];
var read = process.getInputStream().read(messageBytes);
if (read != messageLength) {
throw new IOException("Received message with " + read + " bytes but expected " + messageBytes.length);
}
return new String(messageBytes, StandardCharsets.UTF_8);
}
/**
* Gets the database groups from KeePassXC.
*
* @return The JSON string containing the groups structure, or null if failed
* @throws IOException If there's an error communicating with KeePassXC
*/
public String getDatabaseGroups() throws IOException {
if (!connected) {
throw new IllegalStateException("Not connected to KeePassXC");
}
// Generate a nonce
String nonce = TweetNaClHelper.encodeBase64(TweetNaClHelper.randomBytes(TweetNaClHelper.NONCE_SIZE));
// Create the unencrypted message
Map<String, Object> messageData = new HashMap<>();
messageData.put("action", "get-database-groups");
// Encrypt the message
String encryptedMessage = encrypt(messageData, nonce);
if (encryptedMessage == null) {
System.err.println("Failed to encrypt get-database-groups message");
return null;
}
// Build the request
Map<String, Object> request = new HashMap<>();
request.put("action", "get-database-groups");
request.put("message", encryptedMessage);
request.put("nonce", nonce);
request.put("clientID", clientId);
String requestJson = mapToJson(request);
System.out.println("Sending get-database-groups message: " + requestJson);
// Send the request
String responseJson = sendRequest("get-database-groups", requestJson, TIMEOUT_GET_DATABASE_GROUPS);
if (responseJson == null) {
System.err.println("No response received from get-database-groups");
return null;
}
// Parse and decrypt the response
try {
Map<String, Object> responseMap = jsonToMap(responseJson);
if (responseMap.containsKey("message") && responseMap.containsKey("nonce")) {
String encryptedResponse = (String) responseMap.get("message");
String responseNonce = (String) responseMap.get("nonce");
String decryptedResponse = decrypt(encryptedResponse, responseNonce);
if (decryptedResponse != null) {
System.out.println("Received decrypted get-database-groups response: " + decryptedResponse);
return decryptedResponse;
} else {
System.err.println("Failed to decrypt get-database-groups response");
}
}
} catch (Exception e) {
System.err.println("Error processing get-database-groups response: " + e.getMessage());
}
return null;
}
/**
* Encrypts a message for sending to KeePassXC.
*
* @param message The message to encrypt
* @param nonce The nonce to use for encryption
* @return The encrypted message, or null if encryption failed
*/
private String encrypt(Map<String, Object> message, String nonce) {
if (serverPublicKey == null) {
System.err.println("Server public key not available for encryption");
return null;
}
try {
String messageJson = mapToJson(message);
byte[] messageBytes = messageJson.getBytes(StandardCharsets.UTF_8);
byte[] nonceBytes = TweetNaClHelper.decodeBase64(nonce);
byte[] encrypted = TweetNaClHelper.box(
messageBytes,
nonceBytes,
serverPublicKey,
keyPair.getSecretKey()
);
if (encrypted == null) {
System.err.println("Encryption failed");
return null;
}
return TweetNaClHelper.encodeBase64(encrypted);
} catch (Exception e) {
System.err.println("Error during encryption: " + e.getMessage());
return null;
}
}
/**
* Decrypts a message received from KeePassXC.
*
* @param encryptedMessage The encrypted message
* @param nonce The nonce used for encryption
* @return The decrypted message, or null if decryption failed
*/
private String decrypt(String encryptedMessage, String nonce) {
if (serverPublicKey == null) {
System.err.println("Server public key not available for decryption");
return null;
}
try {
byte[] messageBytes = TweetNaClHelper.decodeBase64(encryptedMessage);
byte[] nonceBytes = TweetNaClHelper.decodeBase64(nonce);
byte[] decrypted = TweetNaClHelper.boxOpen(
messageBytes,
nonceBytes,
serverPublicKey,
keyPair.getSecretKey()
);
if (decrypted == null) {
System.err.println("Decryption failed");
return null;
}
return new String(decrypted, StandardCharsets.UTF_8);
} catch (Exception e) {
System.err.println("Error during decryption: " + e.getMessage());
return null;
}
}
/**
* Associate with KeePassXC.
*
* @return True if successful, false otherwise
* @throws IOException If there's an error communicating with KeePassXC
*/
public boolean associate() throws IOException {
// Generate a key pair for identification
TweetNaClHelper.KeyPair idKeyPair = TweetNaClHelper.generateKeyPair();
// Generate a nonce
String nonce = TweetNaClHelper.encodeBase64(TweetNaClHelper.randomBytes(TweetNaClHelper.NONCE_SIZE));
// Create the unencrypted message
Map<String, Object> messageData = new HashMap<>();
messageData.put("action", "associate");
messageData.put("key", TweetNaClHelper.encodeBase64(keyPair.getPublicKey()));
messageData.put("idKey", TweetNaClHelper.encodeBase64(idKeyPair.getPublicKey()));
// Encrypt the message
String encryptedMessage = encrypt(messageData, nonce);
if (encryptedMessage == null) {
return false;
}
// Build the request
Map<String, Object> request = new HashMap<>();
request.put("action", "associate");
request.put("message", encryptedMessage);
request.put("nonce", nonce);
request.put("clientID", clientId);
String requestJson = mapToJson(request);
System.out.println("Sending associate message: " + requestJson);
// Send the request using longer timeout as it requires user interaction
String responseJson = sendRequest("associate", requestJson, TIMEOUT_ASSOCIATE);
if (responseJson == null) {
return false;
}
// Parse and decrypt the response
try {
Map<String, Object> responseMap = jsonToMap(responseJson);
if (responseMap.containsKey("message") && responseMap.containsKey("nonce")) {
String encryptedResponse = (String) responseMap.get("message");
String responseNonce = (String) responseMap.get("nonce");
String decryptedResponse = decrypt(encryptedResponse, responseNonce);
if (decryptedResponse != null) {
Map<String, Object> parsedResponse = jsonToMap(decryptedResponse);
boolean success = parsedResponse.containsKey("success") &&
"true".equals(parsedResponse.get("success").toString());
if (success && parsedResponse.containsKey("id") && parsedResponse.containsKey("hash")) {
String id = (String) parsedResponse.get("id");
String hash = (String) parsedResponse.get("hash");
associationKey = new KeePassAssociationKey(id, TweetNaClHelper.encodeBase64(idKeyPair.getPublicKey()), hash);
associated = true;
System.out.println("Association successful");
System.out.println("Database ID: " + id);
System.out.println("Database hash: " + hash);
return true;
}
}
}
} catch (Exception e) {
System.err.println("Error processing associate response: " + e.getMessage());
}
return false;
}
/**
* Convert a map to a JSON string.
*/
private String mapToJson(Map<String, Object> map) {
StringBuilder sb = new StringBuilder();
sb.append("{");
boolean first = true;
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (!first) {
sb.append(",");
}
first = false;
sb.append("\"").append(entry.getKey()).append("\":");
Object value = entry.getValue();
if (value instanceof String) {
sb.append("\"").append(escapeJsonString((String) value)).append("\"");
} else if (value instanceof Number || value instanceof Boolean) {
sb.append(value);
} else if (value instanceof Map[]) {
sb.append("[");
Map[] maps = (Map[]) value;
for (int i = 0; i < maps.length; i++) {
if (i > 0) {
sb.append(",");
}
sb.append(mapToJson(maps[i]));
}
sb.append("]");
} else if (value == null) {
sb.append("null");
} else {
sb.append("\"").append(escapeJsonString(value.toString())).append("\"");
}
}
sb.append("}");
return sb.toString();
}
/**
* Escape special characters in a JSON string.
*/
private String escapeJsonString(String s) {
if (s == null) {
return "";
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
switch (ch) {
case '"':
sb.append("\\\"");
break;
case '\\':
sb.append("\\\\");
break;
case '\b':
sb.append("\\b");
break;
case '\f':
sb.append("\\f");
break;
case '\n':
sb.append("\\n");
break;
case '\r':
sb.append("\\r");
break;
case '\t':
sb.append("\\t");
break;
default:
sb.append(ch);
}
}
return sb.toString();
}
/**
* Convert a JSON string to a map.
*/
private Map<String, Object> jsonToMap(String json) {
Map<String, Object> map = new HashMap<>();
try {
// Use regex to extract key-value pairs
Pattern pattern = Pattern.compile("\"([^\"]+)\"\\s*:\\s*(\"[^\"]*\"|\\d+|true|false|null|\\{[^}]*\\}|\\[[^\\]]*\\])");
Matcher matcher = pattern.matcher(json);
while (matcher.find()) {
String key = matcher.group(1);
String valueStr = matcher.group(2);
// Parse the value based on its format
Object value;
if (valueStr.startsWith("\"") && valueStr.endsWith("\"")) {
// String value
value = valueStr.substring(1, valueStr.length() - 1);
} else if ("true".equals(valueStr) || "false".equals(valueStr)) {
// Boolean value
value = Boolean.parseBoolean(valueStr);
} else if ("null".equals(valueStr)) {
// Null value
value = null;
} else {
try {
// Number value
value = Integer.parseInt(valueStr);
} catch (NumberFormatException e1) {
try {
value = Double.parseDouble(valueStr);
} catch (NumberFormatException e2) {
// Just use the string as is
value = valueStr;
}
}
}
map.put(key, value);
}
} catch (Exception e) {
System.err.println("Error parsing JSON: " + e.getMessage());
}
return map;
}
}
@@ -0,0 +1,312 @@
package io.xpipe.app.prefs;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Manages pending requests to KeePassXC.
* This class tracks all pending requests and provides methods to add, complete, and cancel requests.
*/
public class MessageBuffer {
private final Map<String, PendingRequest> requestsById;
private final Map<String, List<PendingRequest>> requestsByAction;
private final Object lock = new Object();
/**
* Creates a new message buffer.
*/
public MessageBuffer() {
this.requestsById = new ConcurrentHashMap<>();
this.requestsByAction = new ConcurrentHashMap<>();
}
/**
* Adds a new pending request to the buffer.
*
* @param request The request to add
*/
public void addRequest(PendingRequest request) {
synchronized (lock) {
requestsById.put(request.getRequestId(), request);
requestsByAction.computeIfAbsent(request.getAction(), k -> new ArrayList<>())
.add(request);
}
}
/**
* Gets a request by its ID.
*
* @param requestId The request ID
* @return The request, or null if not found
*/
public PendingRequest getRequestById(String requestId) {
synchronized (lock) {
return requestsById.get(requestId);
}
}
/**
* Gets all pending requests for a specific action.
*
* @param action The action
* @return A list of pending requests, or an empty list if none found
*/
public List<PendingRequest> getRequestsByAction(String action) {
synchronized (lock) {
List<PendingRequest> requests = requestsByAction.get(action);
if (requests == null) {
return new ArrayList<>();
}
return new ArrayList<>(requests); // Return a copy to avoid concurrent modification
}
}
/**
* Completes a request with the given response.
*
* @param requestId The request ID
* @param response The response from KeePassXC
* @return True if the request was completed, false if not found or already completed
*/
public boolean completeRequest(String requestId, String response) {
synchronized (lock) {
PendingRequest request = requestsById.get(requestId);
if (request == null) {
return false;
}
boolean completed = request.complete(response);
if (completed) {
removeRequest(request);
}
return completed;
}
}
/**
* Completes all pending requests for a specific action.
* This is useful for action-specific responses that don't include a request ID.
*
* @param action The action
* @param response The response from KeePassXC
* @return The number of requests that were completed
*/
public int completeRequestsByAction(String action, String response) {
synchronized (lock) {
List<PendingRequest> requests = requestsByAction.get(action);
if (requests == null || requests.isEmpty()) {
return 0;
}
int count = 0;
List<PendingRequest> completedRequests = new ArrayList<>();
for (PendingRequest request : requests) {
if (request.complete(response)) {
completedRequests.add(request);
count++;
}
}
// Remove completed requests
for (PendingRequest request : completedRequests) {
removeRequest(request);
}
return count;
}
}
/**
* Times out a request.
*
* @param requestId The request ID
* @return True if the request was timed out, false if not found or already completed
*/
public boolean timeoutRequest(String requestId) {
synchronized (lock) {
PendingRequest request = requestsById.get(requestId);
if (request == null) {
return false;
}
boolean timedOut = request.timeout();
if (timedOut) {
removeRequest(request);
}
return timedOut;
}
}
/**
* Cancels a request.
*
* @param requestId The request ID
* @param reason The reason for cancellation
* @return True if the request was cancelled, false if not found or already completed
*/
public boolean cancelRequest(String requestId, String reason) {
synchronized (lock) {
PendingRequest request = requestsById.get(requestId);
if (request == null) {
return false;
}
boolean cancelled = request.cancel(reason);
if (cancelled) {
removeRequest(request);
}
return cancelled;
}
}
/**
* Removes a request from the buffer.
*
* @param request The request to remove
*/
private void removeRequest(PendingRequest request) {
requestsById.remove(request.getRequestId());
List<PendingRequest> actionRequests = requestsByAction.get(request.getAction());
if (actionRequests != null) {
actionRequests.remove(request);
if (actionRequests.isEmpty()) {
requestsByAction.remove(request.getAction());
}
}
}
/**
* Cleans up timed-out requests.
*
* @return The number of requests that were timed out
*/
public int cleanupTimedOutRequests() {
synchronized (lock) {
List<PendingRequest> timedOutRequests = new ArrayList<>();
for (PendingRequest request : requestsById.values()) {
if (request.isTimedOut()) {
request.timeout();
timedOutRequests.add(request);
}
}
// Remove timed-out requests
for (PendingRequest request : timedOutRequests) {
removeRequest(request);
}
return timedOutRequests.size();
}
}
/**
* Gets the number of pending requests.
*
* @return The number of pending requests
*/
public int getPendingRequestCount() {
synchronized (lock) {
return requestsById.size();
}
}
/**
* Extracts the request ID from a JSON response.
*
* @param response The JSON response
* @return The request ID, or null if not found
*/
public static String extractRequestId(String response) {
try {
Pattern pattern = Pattern.compile("\"requestId\":\"([^\"]+)\"");
Matcher matcher = pattern.matcher(response);
if (matcher.find()) {
return matcher.group(1);
}
} catch (Exception e) {
System.err.println("Error extracting requestId: " + e.getMessage());
}
return null;
}
/**
* Extracts the action from a JSON response.
*
* @param response The JSON response
* @return The action, or null if not found
*/
public static String extractAction(String response) {
try {
Pattern pattern = Pattern.compile("\"action\":\"([^\"]+)\"");
Matcher matcher = pattern.matcher(response);
if (matcher.find()) {
return matcher.group(1);
}
} catch (Exception e) {
System.err.println("Error extracting action: " + e.getMessage());
}
return null;
}
/**
* Extracts the nonce from a JSON response.
*
* @param response The JSON response
* @return The nonce, or null if not found
*/
public static String extractNonce(String response) {
try {
Pattern pattern = Pattern.compile("\"nonce\":\"([^\"]+)\"");
Matcher matcher = pattern.matcher(response);
if (matcher.find()) {
return matcher.group(1);
}
} catch (Exception e) {
System.err.println("Error extracting nonce: " + e.getMessage());
}
return null;
}
/**
* Handles an incoming response from KeePassXC.
* This method attempts to match the response to a pending request and complete it.
*
* @param response The JSON response from KeePassXC
* @return The number of requests that were completed
*/
public int handleResponse(String response) {
if (response == null || response.isEmpty()) {
return 0;
}
synchronized (lock) {
String requestId = extractRequestId(response);
String action = extractAction(response);
if (requestId != null) {
// Try to complete by request ID first
if (completeRequest(requestId, response)) {
return 1;
}
}
if (action != null) {
// Then try to complete by action
return completeRequestsByAction(action, response);
}
return 0;
}
}
}
@@ -0,0 +1,152 @@
package io.xpipe.app.prefs;
import java.util.concurrent.CompletableFuture;
/**
* Represents a pending request to KeePassXC.
* This class tracks the request details and provides methods to complete or cancel the request.
*/
public class PendingRequest {
private final String requestId;
private final String action;
private final CompletableFuture<String> future;
private final long timestamp;
private final long timeout;
private boolean completed;
/**
* Creates a new pending request.
*
* @param requestId The unique ID of the request
* @param action The action being performed (e.g., "associate", "get-logins")
* @param future The CompletableFuture that will be completed when the response is received
* @param timeout The timeout in milliseconds
*/
public PendingRequest(String requestId, String action, CompletableFuture<String> future, long timeout) {
this.requestId = requestId;
this.action = action;
this.future = future;
this.timeout = timeout;
this.timestamp = System.currentTimeMillis();
this.completed = false;
}
/**
* Gets the request ID.
*
* @return The request ID
*/
public String getRequestId() {
return requestId;
}
/**
* Gets the action.
*
* @return The action
*/
public String getAction() {
return action;
}
/**
* Gets the completable future.
*
* @return The future
*/
public CompletableFuture<String> getFuture() {
return future;
}
/**
* Gets the timestamp when the request was created.
*
* @return The timestamp in milliseconds
*/
public long getTimestamp() {
return timestamp;
}
/**
* Gets the timeout duration.
*
* @return The timeout in milliseconds
*/
public long getTimeout() {
return timeout;
}
/**
* Checks if the request is completed.
*
* @return True if the request is completed, false otherwise
*/
public boolean isCompleted() {
return completed;
}
/**
* Completes the request with the given response.
*
* @param response The response from KeePassXC
* @return True if the request was completed, false if it was already completed
*/
public boolean complete(String response) {
if (completed) {
return false;
}
completed = true;
future.complete(response);
return true;
}
/**
* Completes the request exceptionally with a timeout.
*
* @return True if the request was completed, false if it was already completed
*/
public boolean timeout() {
if (completed) {
return false;
}
completed = true;
future.completeExceptionally(new TimeoutException("Request timed out after " + timeout + "ms"));
return true;
}
/**
* Cancels the request.
*
* @param reason The reason for cancellation
* @return True if the request was cancelled, false if it was already completed
*/
public boolean cancel(String reason) {
if (completed) {
return false;
}
completed = true;
future.completeExceptionally(new RuntimeException("Request cancelled: " + reason));
return true;
}
/**
* Checks if the request has timed out.
*
* @return True if the request has timed out, false otherwise
*/
public boolean isTimedOut() {
return !completed && System.currentTimeMillis() - timestamp > timeout;
}
/**
* Exception class for request timeouts.
*/
public static class TimeoutException extends Exception {
public TimeoutException(String message) {
super(message);
}
}
}
@@ -0,0 +1,416 @@
package io.xpipe.app.prefs;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.KeyGenerationParameters;
import org.bouncycastle.crypto.agreement.X25519Agreement;
import org.bouncycastle.crypto.generators.X25519KeyPairGenerator;
import org.bouncycastle.crypto.params.X25519PrivateKeyParameters;
import org.bouncycastle.crypto.params.X25519PublicKeyParameters;
import org.bouncycastle.util.Arrays;
import java.security.SecureRandom;
import java.util.Base64;
/**
* Cryptographic helper for KeePassXC communication.
*
* This implementation properly mimics TweetNaCl.js behavior using BouncyCastle,
* implementing X25519 key exchange and XSalsa20-Poly1305 authenticated encryption
* which is what KeePassXC expects.
*/
public class TweetNaClHelper {
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
public static final int KEY_SIZE = 32;
public static final int NONCE_SIZE = 24;
// Sigma constant ("expand 32-byte k")
private static final byte[] SIGMA = {
101, 120, 112, 97, 110, 100, 32, 51, 50, 45, 98, 121, 116, 101, 32, 107
};
public static class KeyPair {
private final byte[] publicKey;
private final byte[] secretKey;
public KeyPair(byte[] publicKey, byte[] secretKey) {
this.publicKey = publicKey;
this.secretKey = secretKey;
}
public byte[] getPublicKey() {
return publicKey;
}
public byte[] getSecretKey() {
return secretKey;
}
}
/**
* Generate a new key pair.
*/
public static KeyPair generateKeyPair() {
X25519KeyPairGenerator keyGen = new X25519KeyPairGenerator();
keyGen.init(new KeyGenerationParameters(SECURE_RANDOM, 0));
AsymmetricCipherKeyPair keyPair = keyGen.generateKeyPair();
X25519PrivateKeyParameters privateKey = (X25519PrivateKeyParameters) keyPair.getPrivate();
X25519PublicKeyParameters publicKey = (X25519PublicKeyParameters) keyPair.getPublic();
return new KeyPair(publicKey.getEncoded(), privateKey.getEncoded());
}
/**
* Generate random bytes.
*/
public static byte[] randomBytes(int size) {
byte[] bytes = new byte[size];
SECURE_RANDOM.nextBytes(bytes);
return bytes;
}
/**
* Encrypt a message using NaCl box.
*
* This uses X25519 for key exchange and XSalsa20-Poly1305 for authenticated encryption.
* Follows the TweetNaCl.js implementation exactly.
*/
public static byte[] box(byte[] message, byte[] nonce, byte[] theirPublicKey, byte[] ourSecretKey) {
// Create a shared secret key for encryption - this is the 'beforenm' step
byte[] k = boxBeforeNm(theirPublicKey, ourSecretKey);
// Now use this key with secretbox (the 'afternm' step)
return secretbox(message, nonce, k);
}
/**
* Compute the shared key for box encryption (equivalent to nacl.box.before)
*/
private static byte[] boxBeforeNm(byte[] theirPublicKey, byte[] ourSecretKey) {
// First compute the X25519 shared secret
byte[] sharedSecret = computeSharedSecret(theirPublicKey, ourSecretKey);
// Then use hsalsa20 to derive the key for XSalsa20
byte[] k = new byte[32];
hsalsa20(k, new byte[16], sharedSecret, SIGMA);
return k;
}
/**
* Decrypt a message using NaCl box open.
* Follows the TweetNaCl.js implementation exactly.
*/
public static byte[] boxOpen(byte[] encryptedMessage, byte[] nonce, byte[] theirPublicKey, byte[] ourSecretKey) {
// Create a shared secret key for decryption - this is the 'beforenm' step
byte[] k = boxBeforeNm(theirPublicKey, ourSecretKey);
// Now use this key with secretbox_open (the 'afternm' step)
return secretboxOpen(encryptedMessage, nonce, k);
}
/**
* Compute a shared secret using X25519.
*/
private static byte[] computeSharedSecret(byte[] publicKey, byte[] secretKey) {
try {
X25519PublicKeyParameters publicParams = new X25519PublicKeyParameters(publicKey, 0);
X25519PrivateKeyParameters privateParams = new X25519PrivateKeyParameters(secretKey, 0);
X25519Agreement agreement = new X25519Agreement();
agreement.init(privateParams);
byte[] sharedSecret = new byte[agreement.getAgreementSize()];
agreement.calculateAgreement(publicParams, sharedSecret, 0);
return sharedSecret;
} catch (Exception e) {
throw new RuntimeException("Error computing shared secret: " + e.getMessage(), e);
}
}
/**
* Proper implementation of HSalsa20 function from NaCl, used to derive the subkey.
* This matches the TweetNaCl.js implementation.
*/
private static void hsalsa20(byte[] out, byte[] nonce, byte[] key, byte[] constants) {
int[] x = new int[16]; // Working state
// Load constants (sigma)
x[0] = load32(constants, 0);
x[5] = load32(constants, 4);
x[10] = load32(constants, 8);
x[15] = load32(constants, 12);
// Load key
x[1] = load32(key, 0);
x[2] = load32(key, 4);
x[3] = load32(key, 8);
x[4] = load32(key, 12);
x[11] = load32(key, 16);
x[12] = load32(key, 20);
x[13] = load32(key, 24);
x[14] = load32(key, 28);
// Load nonce
x[6] = load32(nonce, 0);
x[7] = load32(nonce, 4);
x[8] = load32(nonce, 8);
x[9] = load32(nonce, 12);
// Perform 20 rounds of the Salsa20 core
for (int i = 0; i < 20; i += 2) {
// Column round
x[4] ^= rotl32(x[0] + x[12], 7);
x[8] ^= rotl32(x[4] + x[0], 9);
x[12] ^= rotl32(x[8] + x[4], 13);
x[0] ^= rotl32(x[12] + x[8], 18);
x[9] ^= rotl32(x[5] + x[1], 7);
x[13] ^= rotl32(x[9] + x[5], 9);
x[1] ^= rotl32(x[13] + x[9], 13);
x[5] ^= rotl32(x[1] + x[13], 18);
x[14] ^= rotl32(x[10] + x[6], 7);
x[2] ^= rotl32(x[14] + x[10], 9);
x[6] ^= rotl32(x[2] + x[14], 13);
x[10] ^= rotl32(x[6] + x[2], 18);
x[3] ^= rotl32(x[15] + x[11], 7);
x[7] ^= rotl32(x[3] + x[15], 9);
x[11] ^= rotl32(x[7] + x[3], 13);
x[15] ^= rotl32(x[11] + x[7], 18);
// Diagonal round
x[1] ^= rotl32(x[0] + x[3], 7);
x[2] ^= rotl32(x[1] + x[0], 9);
x[3] ^= rotl32(x[2] + x[1], 13);
x[0] ^= rotl32(x[3] + x[2], 18);
x[6] ^= rotl32(x[5] + x[4], 7);
x[7] ^= rotl32(x[6] + x[5], 9);
x[4] ^= rotl32(x[7] + x[6], 13);
x[5] ^= rotl32(x[4] + x[7], 18);
x[11] ^= rotl32(x[10] + x[9], 7);
x[8] ^= rotl32(x[11] + x[10], 9);
x[9] ^= rotl32(x[8] + x[11], 13);
x[10] ^= rotl32(x[9] + x[8], 18);
x[12] ^= rotl32(x[15] + x[14], 7);
x[13] ^= rotl32(x[12] + x[15], 9);
x[14] ^= rotl32(x[13] + x[12], 13);
x[15] ^= rotl32(x[14] + x[13], 18);
}
// Extract the output
store32(out, 0, x[0]);
store32(out, 4, x[5]);
store32(out, 8, x[10]);
store32(out, 12, x[15]);
store32(out, 16, x[6]);
store32(out, 20, x[7]);
store32(out, 24, x[8]);
store32(out, 28, x[9]);
}
/**
* Implementation of secretbox from NaCl.
*/
private static byte[] secretbox(byte[] message, byte[] nonce, byte[] key) {
// For compatibility with TweetNaCl, we implement the same logic
// Our secretbox will combine XSalsa20 encryption with Poly1305 MAC
try {
// In TweetNaCl.js, secretbox adds 32 zero bytes before the message
byte[] paddedMessage = new byte[32 + message.length];
System.arraycopy(message, 0, paddedMessage, 32, message.length);
// Apply XSalsa20 encryption
byte[] c = new byte[paddedMessage.length];
streamXorXSalsa20(c, paddedMessage, paddedMessage.length, nonce, key);
// The first 16 bytes are used for the Poly1305 tag (MAC)
byte[] tag = new byte[16];
crypto_onetimeauth(tag, c, 32, c.length - 32, Arrays.copyOf(c, 32));
// Copy tag into the first 16 bytes of c
System.arraycopy(tag, 0, c, 16, 16);
// Clear the first 16 bytes (not used in the result)
for (int i = 0; i < 16; i++) {
c[i] = 0;
}
// Return result skipping the first 16 bytes (boxzerobytes)
return Arrays.copyOfRange(c, 16, c.length);
} catch (Exception e) {
throw new RuntimeException("Encryption failed: " + e.getMessage(), e);
}
}
/**
* Implementation of secretbox_open from NaCl.
*/
private static byte[] secretboxOpen(byte[] encryptedMessage, byte[] nonce, byte[] key) {
// Check if the message is long enough
if (encryptedMessage.length < 16) {
return null; // Not enough data
}
try {
// Reconstruct the ciphertext with boxzerobytes prefix
byte[] c = new byte[16 + encryptedMessage.length];
System.arraycopy(encryptedMessage, 0, c, 16, encryptedMessage.length);
// Verify the Poly1305 authentication tag
byte[] subkey = Arrays.copyOf(new byte[32], 32); // First 32 bytes of the keystream
streamXSalsa20(subkey, 32, nonce, key);
if (crypto_onetimeauth_verify(c, 16, c, 32, c.length - 32, subkey) != 0) {
return null; // MAC verification failed
}
// Decrypt the message
byte[] m = new byte[c.length];
streamXorXSalsa20(m, c, c.length, nonce, key);
// Return the actual message (skipping the 32 zero bytes prefix)
return Arrays.copyOfRange(m, 32, m.length);
} catch (Exception e) {
return null; // Return null on decryption failure (as in NaCl)
}
}
/**
* Core XSalsa20 stream cipher function.
*/
private static void streamXSalsa20(byte[] out, int outLength, byte[] nonce, byte[] key) {
// First, derive a subkey using HSalsa20
byte[] subkey = new byte[32];
hsalsa20(subkey, Arrays.copyOf(nonce, 16), key, SIGMA);
// Then use the subkey with the remaining bytes of the nonce
streamSalsa20(out, outLength, Arrays.copyOfRange(nonce, 16, 24), subkey);
}
/**
* XSalsa20 stream XOR function
*/
private static void streamXorXSalsa20(byte[] c, byte[] m, int mlen, byte[] nonce, byte[] key) {
// First, derive a subkey using HSalsa20
byte[] subkey = new byte[32];
hsalsa20(subkey, Arrays.copyOf(nonce, 16), key, SIGMA);
// Then use the subkey with the remaining bytes of the nonce
streamXorSalsa20(c, m, mlen, Arrays.copyOfRange(nonce, 16, 24), subkey);
}
/**
* Core Salsa20 stream cipher function.
*/
private static void streamSalsa20(byte[] out, int outLength, byte[] nonce, byte[] key) {
byte[] zeros = new byte[outLength];
streamXorSalsa20(out, zeros, outLength, nonce, key);
}
/**
* Salsa20 stream XOR function
*/
private static void streamXorSalsa20(byte[] c, byte[] m, int mlen, byte[] nonce, byte[] key) {
// Use BouncyCastle's Salsa20 implementation
org.bouncycastle.crypto.engines.Salsa20Engine salsa20 = new org.bouncycastle.crypto.engines.Salsa20Engine();
org.bouncycastle.crypto.params.ParametersWithIV params =
new org.bouncycastle.crypto.params.ParametersWithIV(
new org.bouncycastle.crypto.params.KeyParameter(key), nonce);
salsa20.init(true, params);
salsa20.processBytes(m, 0, mlen, c, 0);
}
/**
* Poly1305 one-time authentication.
*/
private static void crypto_onetimeauth(byte[] out, byte[] m, int mpos, int mlen, byte[] key) {
org.bouncycastle.crypto.macs.Poly1305 poly1305 = new org.bouncycastle.crypto.macs.Poly1305();
poly1305.init(new org.bouncycastle.crypto.params.KeyParameter(key));
poly1305.update(m, mpos, mlen);
poly1305.doFinal(out, 0);
}
/**
* Verify a Poly1305 one-time authentication tag.
*/
private static int crypto_onetimeauth_verify(byte[] h, int hpos, byte[] m, int mpos, int mlen, byte[] key) {
byte[] correct = new byte[16];
crypto_onetimeauth(correct, m, mpos, mlen, key);
return crypto_verify_16(h, hpos, correct, 0);
}
/**
* Verify 16 bytes in constant time.
*/
private static int crypto_verify_16(byte[] x, int xpos, byte[] y, int ypos) {
return constantTimeEquals(Arrays.copyOfRange(x, xpos, xpos + 16),
Arrays.copyOfRange(y, ypos, ypos + 16)) ? 0 : -1;
}
/**
* Helper for loading 32-bit integers (little-endian).
*/
private static int load32(byte[] src, int offset) {
int u = src[offset] & 0xff;
u |= (src[offset + 1] & 0xff) << 8;
u |= (src[offset + 2] & 0xff) << 16;
u |= (src[offset + 3] & 0xff) << 24;
return u;
}
/**
* Helper for storing 32-bit integers (little-endian).
*/
private static void store32(byte[] dst, int offset, int u) {
dst[offset] = (byte) (u & 0xff);
dst[offset + 1] = (byte) ((u >>> 8) & 0xff);
dst[offset + 2] = (byte) ((u >>> 16) & 0xff);
dst[offset + 3] = (byte) ((u >>> 24) & 0xff);
}
/**
* Rotate a 32-bit integer left by the specified number of bits.
*/
private static int rotl32(int x, int b) {
return ((x << b) | (x >>> (32 - b)));
}
/**
* Compare two byte arrays in constant time to prevent timing attacks.
*/
private static boolean constantTimeEquals(byte[] a, byte[] b) {
if (a.length != b.length) {
return false;
}
int result = 0;
for (int i = 0; i < a.length; i++) {
result |= a[i] ^ b[i];
}
return result == 0;
}
/**
* Encode bytes as Base64.
*/
public static String encodeBase64(byte[] data) {
return Base64.getEncoder().encodeToString(data);
}
/**
* Decode Base64 to bytes.
*/
public static byte[] decodeBase64(String data) {
return Base64.getDecoder().decode(data);
}
}
@@ -76,11 +76,11 @@ public class ShellView {
return isRoot;
}
public Optional<String> findProgram(String name) throws Exception {
public Optional<FilePath> findProgram(String name) throws Exception {
var out = shellControl
.command(shellControl.getShellDialect().getWhichCommand(name))
.readStdoutIfPossible();
return out.flatMap(s -> s.lines().findFirst()).map(String::trim);
return out.flatMap(s -> s.lines().findFirst()).map(String::trim).map(s -> FilePath.of(s));
}
public boolean isInPath(String executable) throws Exception {