mirror of
https://github.com/xpipe-io/xpipe.git
synced 2026-04-22 15:40:31 -04:00
KeePass fixes
This commit is contained in:
@@ -19,8 +19,9 @@ public class KeePassClient {
|
||||
@SneakyThrows
|
||||
public static String receive(String key) {
|
||||
var client = getOrCreate();
|
||||
client.getDatabaseGroups();
|
||||
return client.getLogins("abc");
|
||||
var response = client.getLoginsMessage("https://redirect.pizza");
|
||||
var password = client.getPassword(response);
|
||||
return password;
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package io.xpipe.app.prefs;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.util.JacksonMapper;
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
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;
|
||||
@@ -15,10 +18,6 @@ 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;
|
||||
|
||||
@@ -43,20 +42,33 @@ public class KeePassNativeClient {
|
||||
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;}
|
||||
|
||||
|
||||
/**
|
||||
* Extracts the action from a JSON response.
|
||||
*
|
||||
* @param response The JSON response
|
||||
* @return The action, or null if not found
|
||||
*/
|
||||
private 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;
|
||||
}
|
||||
|
||||
public void useExistingAssociationKey(KeePassAssociationKey key) {
|
||||
this.associationKey = key;
|
||||
}
|
||||
@@ -77,12 +89,7 @@ public class KeePassNativeClient {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -190,12 +197,14 @@ public class KeePassNativeClient {
|
||||
|
||||
// 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("error")) {
|
||||
throw ErrorEvent.expected(new IllegalStateException(responseMap.get("error").toString()));
|
||||
}
|
||||
|
||||
if (responseMap.containsKey("message") && responseMap.containsKey("nonce")) {
|
||||
String encryptedResponse = (String) responseMap.get("message");
|
||||
String responseNonce = (String) responseMap.get("nonce");
|
||||
@@ -205,10 +214,7 @@ public class KeePassNativeClient {
|
||||
Map<String, Object> parsedResponse = jsonToMap(decryptedResponse);
|
||||
boolean success = parsedResponse.containsKey("success") &&
|
||||
"true".equals(parsedResponse.get("success").toString());
|
||||
|
||||
if (success) {
|
||||
associated = true;
|
||||
} else {
|
||||
if (!success) {
|
||||
throw new IllegalStateException("KeePassXC association failed");
|
||||
}
|
||||
}
|
||||
@@ -222,7 +228,7 @@ public class KeePassNativeClient {
|
||||
* @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 {
|
||||
public String getLoginsMessage(String url) throws IOException {
|
||||
// Generate a nonce
|
||||
String nonce = TweetNaClHelper.encodeBase64(TweetNaClHelper.randomBytes(TweetNaClHelper.NONCE_SIZE));
|
||||
|
||||
@@ -240,9 +246,6 @@ public class KeePassNativeClient {
|
||||
|
||||
// Encrypt the message
|
||||
String encryptedMessage = encrypt(messageData, nonce);
|
||||
if (encryptedMessage == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Build the request
|
||||
Map<String, Object> request = new HashMap<>();
|
||||
@@ -252,39 +255,44 @@ public class KeePassNativeClient {
|
||||
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;
|
||||
|
||||
Map<String, Object> responseMap = jsonToMap(responseJson);
|
||||
if (responseMap.containsKey("error")) {
|
||||
throw ErrorEvent.expected(new IllegalStateException(responseMap.get("error").toString()));
|
||||
}
|
||||
|
||||
if (responseMap.containsKey("message") && responseMap.containsKey("nonce")) {
|
||||
String encryptedResponse = (String) responseMap.get("message");
|
||||
String responseNonce = (String) responseMap.get("nonce");
|
||||
return decrypt(encryptedResponse, responseNonce);
|
||||
}
|
||||
|
||||
// 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());
|
||||
throw new IllegalStateException("Login query failed for an unknown reason");
|
||||
}
|
||||
|
||||
public String getPassword(String message) throws IOException {
|
||||
var tree = JacksonMapper.getDefault().readTree(message);
|
||||
var count = tree.required("count").asInt();
|
||||
if (count == 0) {
|
||||
throw ErrorEvent.expected(new IllegalArgumentException("No password was found for specified key"));
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
if (count > 1) {
|
||||
throw ErrorEvent.expected(new IllegalArgumentException("Password key is ambiguous and returned multiple results"));
|
||||
}
|
||||
|
||||
var object = (ObjectNode) tree.required("entries").get(0);
|
||||
var password = object.required("password").asText();
|
||||
return password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects from KeePassXC.
|
||||
*/
|
||||
public void disconnect() {
|
||||
if (responseHandler != null) {
|
||||
responseHandler.interrupt();
|
||||
responseHandler = null;
|
||||
}
|
||||
|
||||
process.destroy();
|
||||
process = null;
|
||||
}
|
||||
@@ -299,43 +307,40 @@ public class KeePassNativeClient {
|
||||
* @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();
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
while (System.currentTimeMillis() - startTime < timeout) {
|
||||
var response = receiveNativeMessage();
|
||||
if (filterResponse(action, response)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
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();
|
||||
throw new IllegalStateException("KeePassXC " + action + " request timed out");
|
||||
}
|
||||
|
||||
private boolean filterResponse(String action, String response) {
|
||||
System.out.println("Received response: " + response);
|
||||
|
||||
// Extract action
|
||||
String extractedAction = extractAction(response);
|
||||
|
||||
// Special handling for action-specific responses
|
||||
if ("database-locked".equals(extractedAction) || "database-unlocked".equals(extractedAction)) {
|
||||
System.out.println("Database state changed: " + extractedAction);
|
||||
// Update state based on the action
|
||||
if ("database-locked".equals(extractedAction)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (action.equals(extractedAction)) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,99 +351,19 @@ public class KeePassNativeClient {
|
||||
* @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();
|
||||
Pattern pattern = Pattern.compile("\"requestId\":\"([^\"]+)\"");
|
||||
Matcher matcher = pattern.matcher(message);
|
||||
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("Error in response handler: " + e.getMessage());
|
||||
System.err.println("Error extracting requestId: " + e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
@@ -490,70 +415,6 @@ public class KeePassNativeClient {
|
||||
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.
|
||||
*
|
||||
@@ -562,33 +423,18 @@ public class KeePassNativeClient {
|
||||
* @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;
|
||||
}
|
||||
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()
|
||||
);
|
||||
|
||||
return TweetNaClHelper.encodeBase64(encrypted);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -599,32 +445,21 @@ public class KeePassNativeClient {
|
||||
* @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;
|
||||
byte[] messageBytes = TweetNaClHelper.decodeBase64(encryptedMessage);
|
||||
byte[] nonceBytes = TweetNaClHelper.decodeBase64(nonce);
|
||||
|
||||
byte[] decrypted = TweetNaClHelper.boxOpen(
|
||||
messageBytes,
|
||||
nonceBytes,
|
||||
serverPublicKey,
|
||||
keyPair.getSecretKey()
|
||||
);
|
||||
|
||||
if (decrypted == null) {
|
||||
throw new IllegalArgumentException("Message decryption failed");
|
||||
}
|
||||
|
||||
return new String(decrypted, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -633,7 +468,7 @@ public class KeePassNativeClient {
|
||||
* @return True if successful, false otherwise
|
||||
* @throws IOException If there's an error communicating with KeePassXC
|
||||
*/
|
||||
public boolean associate() throws IOException {
|
||||
public void associate() throws IOException {
|
||||
// Generate a key pair for identification
|
||||
TweetNaClHelper.KeyPair idKeyPair = TweetNaClHelper.generateKeyPair();
|
||||
|
||||
@@ -648,10 +483,7 @@ public class KeePassNativeClient {
|
||||
|
||||
// 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");
|
||||
@@ -660,177 +492,54 @@ public class KeePassNativeClient {
|
||||
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());
|
||||
Map<String, Object> responseMap = jsonToMap(responseJson);
|
||||
|
||||
if (responseMap.containsKey("error")) {
|
||||
throw ErrorEvent.expected(new IllegalStateException(responseMap.get("error").toString()));
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
if (responseMap.containsKey("message") && responseMap.containsKey("nonce")) {
|
||||
String encryptedResponse = (String) responseMap.get("message");
|
||||
String responseNonce = (String) responseMap.get("nonce");
|
||||
|
||||
String decryptedResponse = decrypt(encryptedResponse, responseNonce);
|
||||
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);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalStateException("KeePassXC association failed");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a map to a JSON string.
|
||||
*/
|
||||
@SneakyThrows
|
||||
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();
|
||||
var mapper = JacksonMapper.getDefault();
|
||||
return mapper.writeValueAsString(map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a JSON string to a map.
|
||||
*/
|
||||
@SneakyThrows
|
||||
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());
|
||||
}
|
||||
|
||||
var mapper = JacksonMapper.getDefault();
|
||||
var type = TypeFactory.defaultInstance().constructType(new TypeReference<>() {});
|
||||
Map<String, Object> map = mapper.readValue(json, type);
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user