diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..92322c4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.idea/
+target/
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..971ad2d
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2021-2024 Ralph Plawetzki
+Copyright (c) 2024-2024 Hatter Jiang
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 1e8211f..0e6ae47 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,58 @@
-# card-cryptomator
+# tinyencrypt-cryptomator
+> This project is fork from https://github.com/purejava/keepassxc-cryptomator , add GnuPG support
+
+[](https://github.com/jht5945/gnupg-cryptomator/releases)
+[](https://github.com/jht5945/gnupg-cryptomator/blob/master/LICENSE)
+
+Plug-in for Cryptomator to store vault passwords with card-cli encryption.
+
+# Build Project
+
+Requirement:
+
+* JDK 17 or later
+* Maven 3.8.4 or later
+
+```shell
+mvn package
+```
+
+Copy packaged plugin from `target/card-cryptomator-$VERSION.jar` to Cryptomator plugins directory.
+> Usage reference [Wiki](https://github.com/purejava/keepassxc-cryptomator/wiki)
+
+# Configuration
+
+Configure file location:
+
+* `/etc/cryptomator/card_config.json`
+* `~/.config/cryptomator/card_config.json`
+
+```json
+{
+}
+```
+
+# Documentation
+
+For documentation please take a look at the [Wiki](https://github.com/purejava/keepassxc-cryptomator/wiki).
+
+Plugin location:
+
+| OS | Default Dir |
+| ---- | ---- |
+| Mac | `~/Library/Application Support/Cryptomator/Plugins` |
+| Linux | `~/.local/share/Cryptomator/plugins` |
+| Windows | `%homepath%\AppData\Roaming\Cryptomator\Plugins` |
+
+# How it works?
+
+This plugin use card-cli encrypt and decrypt passwords.
+
+# Copyright
+
+Copyright (C) 2021-2024 Ralph Plawetzki
+Copyright (C) 2024-2024 Hatter Jiang
+
+The Cryptomator logo is Copyright (C) of https://cryptomator.org/
+The KeePassXC logo is Copyright (C) of https://keepassxc.org/
diff --git a/build.json b/build.json
new file mode 100644
index 0000000..e07661d
--- /dev/null
+++ b/build.json
@@ -0,0 +1,7 @@
+{
+ "java": "17",
+ "builder": {
+ "name": "maven",
+ "version": "3.8.4"
+ }
+}
diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml
new file mode 100644
index 0000000..8443f3a
--- /dev/null
+++ b/dependency-reduced-pom.xml
@@ -0,0 +1,275 @@
+
+
+ 4.0.0
+ me.hatter
+ card-cryptomator
+ card-cryptomator
+ 1.0.0
+ Plug-in for Cryptomator to store vault passwords with card-cli encryption.
+ https://git.hatter.ink/hatter/card-cryptomator
+
+
+ Ralph Plawetzki
+ ralph@purejava.org
+ https://github.com/purejava
+ +1
+
+
+ Hatter Jiang
+ jht5945@gmail.com
+ https://github.com/jht5945
+ +8
+
+
+
+
+ MIT License
+ https://opensource.org/licenses/MIT
+
+
+
+ scm:git:git@github.com:jht5945/tinyencrypt-cryptomator.git
+ scm:git:git@github.com:jht5945/tinyencrypt-cryptomator.git
+ git@github.com:jht5945/tinyencrypt-cryptomator.git
+
+
+
+
+
+ maven-clean-plugin
+ 3.3.2
+
+
+ maven-resources-plugin
+ 3.3.1
+
+ false
+
+
+
+ maven-compiler-plugin
+ 3.13.0
+
+
+ maven-surefire-plugin
+ 3.3.0
+
+
+ maven-jar-plugin
+ 3.4.1
+
+ true
+
+
+
+ maven-install-plugin
+ 3.1.2
+
+
+ maven-deploy-plugin
+ 3.1.2
+
+
+ maven-site-plugin
+ 4.0.0-M15
+
+
+ maven-project-info-reports-plugin
+ 3.6.0
+
+
+
+
+
+ maven-compiler-plugin
+ 3.13.0
+
+ 17
+ 17
+
+
+
+ maven-source-plugin
+ 3.3.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+ maven-javadoc-plugin
+ 3.7.0
+
+
+ attach-javadocs
+
+ jar
+
+
+ false
+
+
+
+
+
+ maven-shade-plugin
+ 3.6.0
+
+
+ package
+
+ shade
+
+
+
+
+
+ maven-gpg-plugin
+ 3.2.4
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+ --pinentry-mode
+ loopback
+
+
+
+
+
+
+ org.sonatype.plugins
+ nexus-staging-maven-plugin
+ 1.7.0
+ true
+
+ ossrh
+ https://oss.sonatype.org/
+ false
+
+
+
+ maven-surefire-plugin
+ 3.3.0
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.10.2
+
+
+
+
+
+
+
+ sign
+
+
+
+ maven-gpg-plugin
+ 3.2.4
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+ --pinentry-mode
+ loopback
+
+
+
+
+
+
+
+
+
+
+
+ org.slf4j
+ slf4j-simple
+ 2.0.13
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.10.2
+ test
+
+
+ opentest4j
+ org.opentest4j
+
+
+ junit-platform-commons
+ org.junit.platform
+
+
+ apiguardian-api
+ org.apiguardian
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.10.2
+ test
+
+
+ junit-platform-engine
+ org.junit.platform
+
+
+ apiguardian-api
+ org.apiguardian
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.10.2
+ test
+
+
+ junit-jupiter-params
+ org.junit.jupiter
+
+
+
+
+
+
+ ossrh
+ https://oss.sonatype.org/service/local/staging/deploy/maven2/
+
+
+ ossrh
+ https://oss.sonatype.org/content/repositories/snapshots
+
+
+
+ 1.3.1
+ 2.0.13
+ 2.11.0
+ UTF-8
+ 5.10.2
+ 1.2.5
+
+
diff --git a/keepassxc-cryptomator.png b/keepassxc-cryptomator.png
new file mode 100644
index 0000000..03b1e69
Binary files /dev/null and b/keepassxc-cryptomator.png differ
diff --git a/keepassxc-cryptomator.svg b/keepassxc-cryptomator.svg
new file mode 100644
index 0000000..78e6796
--- /dev/null
+++ b/keepassxc-cryptomator.svg
@@ -0,0 +1,258 @@
+
+
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..6260d78
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,286 @@
+
+
+ 4.0.0
+ me.hatter
+ card-cryptomator
+ 1.0.0
+
+ card-cryptomator
+ Plug-in for Cryptomator to store vault passwords with card-cli encryption.
+ https://git.hatter.ink/hatter/card-cryptomator
+
+
+
+ MIT License
+ https://opensource.org/licenses/MIT
+
+
+
+
+
+ Ralph Plawetzki
+ ralph@purejava.org
+ +1
+
+ https://github.com/purejava
+
+
+ Hatter Jiang
+ jht5945@gmail.com
+ +8
+
+ https://github.com/jht5945
+
+
+
+
+ scm:git:git@github.com:jht5945/tinyencrypt-cryptomator.git
+ scm:git:git@github.com:jht5945/tinyencrypt-cryptomator.git
+ git@github.com:jht5945/tinyencrypt-cryptomator.git
+
+
+
+
+ ossrh
+ https://oss.sonatype.org/content/repositories/snapshots
+
+
+ ossrh
+ https://oss.sonatype.org/service/local/staging/deploy/maven2/
+
+
+
+
+ UTF-8
+
+
+ 1.3.1
+ 1.2.5
+ 2.11.0
+ 2.0.13
+ 5.10.2
+
+
+
+
+ org.cryptomator
+ integrations-api
+ ${api.version}
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+
+
+ org.slf4j
+ slf4j-simple
+ ${slf4j.version}
+ test
+
+
+ com.google.code.gson
+ gson
+ ${gson.version}
+
+
+ org.purejava
+ keepassxc-proxy-access
+ ${keepassxc-proxy.version}
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ ${junit.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${junit.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit.version}
+ test
+
+
+
+
+
+
+
+ maven-clean-plugin
+ 3.3.2
+
+
+ maven-resources-plugin
+ 3.3.1
+
+ false
+
+
+
+ maven-compiler-plugin
+ 3.13.0
+
+
+ maven-surefire-plugin
+ 3.3.0
+
+
+ maven-jar-plugin
+ 3.4.1
+
+ true
+
+
+
+ maven-install-plugin
+ 3.1.2
+
+
+ maven-deploy-plugin
+ 3.1.2
+
+
+ maven-site-plugin
+ 4.0.0-M15
+
+
+ maven-project-info-reports-plugin
+ 3.6.0
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+ 17
+ 17
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.3.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.7.0
+
+
+ attach-javadocs
+
+ jar
+
+
+ false
+
+
+
+
+
+ maven-shade-plugin
+ 3.6.0
+
+
+ package
+
+ shade
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.2.4
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+ --pinentry-mode
+ loopback
+
+
+
+
+
+
+ org.sonatype.plugins
+ nexus-staging-maven-plugin
+ 1.7.0
+ true
+
+ ossrh
+ https://oss.sonatype.org/
+ false
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.3.0
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.10.2
+
+
+
+
+
+
+
+ sign
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.2.4
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+ --pinentry-mode
+ loopback
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/me/hatter/integrations/card/CardAccessProvider.java b/src/main/java/me/hatter/integrations/card/CardAccessProvider.java
new file mode 100644
index 0000000..55e22ab
--- /dev/null
+++ b/src/main/java/me/hatter/integrations/card/CardAccessProvider.java
@@ -0,0 +1,80 @@
+package me.hatter.integrations.card;
+
+import org.cryptomator.integrations.keychain.KeychainAccessException;
+import org.cryptomator.integrations.keychain.KeychainAccessProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author hatterjiang
+ */
+public class CardAccessProvider implements KeychainAccessProvider {
+
+ private static final Logger LOG = LoggerFactory.getLogger(CardAccessProvider.class);
+
+ private CardConfig cardConfig;
+
+ public CardAccessProvider() {
+ try {
+ cardConfig = Utils.loadCardConfig();
+ if (!Utils.checkCardCliReady(cardConfig)) {
+ LOG.error("Check card-cli command failed");
+ cardConfig = null;
+ }
+ } catch (KeychainAccessException e) {
+ cardConfig = null;
+ LOG.error("Load card-cli config failed", e);
+ }
+ }
+
+ @Override
+ public String displayName() {
+ return "CardCli";
+ }
+
+ @Override
+ public boolean isSupported() {
+ return cardConfig != null;
+ }
+
+ @Override
+ public boolean isLocked() {
+ // No lock status
+ return false;
+ }
+
+ @Override
+ public void storePassphrase(String vault, CharSequence password) throws KeychainAccessException {
+ storePassphrase(vault, "Vault", password);
+ }
+
+ @Override
+ public void storePassphrase(String vault, String name, CharSequence password) throws KeychainAccessException {
+ LOG.info("Store password for: " + vault + " / " + name);
+ Utils.storePassword(cardConfig, vault, name, password);
+ }
+
+ @Override
+ public char[] loadPassphrase(String vault) throws KeychainAccessException {
+ LOG.info("Load password for: " + vault);
+ final String password = Utils.loadPassword(cardConfig, vault);
+ return password.toCharArray();
+ }
+
+ @Override
+ public void deletePassphrase(String vault) throws KeychainAccessException {
+ LOG.info("Delete password for: " + vault);
+ Utils.deletePassword(cardConfig, vault);
+ }
+
+ @Override
+ public void changePassphrase(String vault, CharSequence password) throws KeychainAccessException {
+ changePassphrase(vault, "Vault", password);
+ }
+
+ @Override
+ public void changePassphrase(String vault, String name, CharSequence password) throws KeychainAccessException {
+ LOG.info("Change password for: " + vault + " / " + name);
+ Utils.storePassword(cardConfig, vault, name, password);
+ }
+}
diff --git a/src/main/java/me/hatter/integrations/card/CardConfig.java b/src/main/java/me/hatter/integrations/card/CardConfig.java
new file mode 100644
index 0000000..c8e1e29
--- /dev/null
+++ b/src/main/java/me/hatter/integrations/card/CardConfig.java
@@ -0,0 +1,21 @@
+package me.hatter.integrations.card;
+
+/**
+ * card-cli config
+ *
+ * @author hatterjiang
+ */
+public class CardConfig {
+ /**
+ * OPTIONAL, Encrypt key base path, default "~/.config/cryptomator/card_keys/"
+ */
+ private String encryptKeyBasePath;
+
+ public String getEncryptKeyBasePath() {
+ return encryptKeyBasePath;
+ }
+
+ public void setEncryptKeyBasePath(String encryptKeyBasePath) {
+ this.encryptKeyBasePath = encryptKeyBasePath;
+ }
+}
diff --git a/src/main/java/me/hatter/integrations/card/CardHmacDecryptResult.java b/src/main/java/me/hatter/integrations/card/CardHmacDecryptResult.java
new file mode 100644
index 0000000..8bed4cf
--- /dev/null
+++ b/src/main/java/me/hatter/integrations/card/CardHmacDecryptResult.java
@@ -0,0 +1,13 @@
+package me.hatter.integrations.card;
+
+public class CardHmacDecryptResult {
+ private String plaintext;
+
+ public String getPlaintext() {
+ return plaintext;
+ }
+
+ public void setPlaintext(String plaintext) {
+ this.plaintext = plaintext;
+ }
+}
diff --git a/src/main/java/me/hatter/integrations/card/CardHmacEncryptResult.java b/src/main/java/me/hatter/integrations/card/CardHmacEncryptResult.java
new file mode 100644
index 0000000..e0be3ed
--- /dev/null
+++ b/src/main/java/me/hatter/integrations/card/CardHmacEncryptResult.java
@@ -0,0 +1,13 @@
+package me.hatter.integrations.card;
+
+public class CardHmacEncryptResult {
+ private String ciphertext;
+
+ public String getCiphertext() {
+ return ciphertext;
+ }
+
+ public void setCiphertext(String ciphertext) {
+ this.ciphertext = ciphertext;
+ }
+}
diff --git a/src/main/java/me/hatter/integrations/card/Utils.java b/src/main/java/me/hatter/integrations/card/Utils.java
new file mode 100644
index 0000000..97efd04
--- /dev/null
+++ b/src/main/java/me/hatter/integrations/card/Utils.java
@@ -0,0 +1,285 @@
+package me.hatter.integrations.card;
+
+import com.google.gson.Gson;
+import org.apache.commons.lang3.StringUtils;
+import org.cryptomator.integrations.keychain.KeychainAccessException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * @author hatterjiang
+ */
+public class Utils {
+ private static final Logger LOG = LoggerFactory.getLogger(Utils.class);
+ private static final String USER_HOME = System.getProperty("user.home");
+ private static final String DEFAULT_CARD_COMMAND = new File(USER_HOME, "bin/card-cli").getAbsolutePath();
+ private static final File CARD_CONFIG_FILE1 = new File("/etc/cryptomator/card_config.json");
+ private static final File CARD_CONFIG_FILE2 = new File(USER_HOME, ".config/cryptomator/card_config.json");
+ private static final File DEFAULT_ENCRYPTION_KEY_BASE_PATH = new File(USER_HOME, ".config/cryptomator/card_keys/");
+
+ public static boolean isCheckPassphraseStored() {
+ final StackTraceElement stack = getCallerStackTrace();
+ if (stack != null) {
+ return "isPassphraseStored".equals(stack.getMethodName());
+ }
+ return false;
+ }
+
+ public static StackTraceElement getCallerStackTrace() {
+ // org.cryptomator.common.keychain.KeychainManager :: isPassphraseStored
+ final StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
+ for (int i = 0; i < stackTraceElements.length; i++) {
+ final StackTraceElement stack = stackTraceElements[i];
+ if ("org.cryptomator.common.keychain.KeychainManager".equals(stack.getClassName())) {
+ return stack;
+ }
+ }
+ return null;
+ }
+
+ public static boolean checkCardCliReady(CardConfig cardConfig) {
+ if (cardConfig == null) {
+ return false;
+ }
+ try {
+ final UtilsCommandResult versionResult = runCardCli(cardConfig, null, "-V");
+ if (versionResult.getExitValue() == 0) {
+ return true;
+ }
+ LOG.warn("Check card-cli not success: " + versionResult);
+ return false;
+ } catch (KeychainAccessException e) {
+ LOG.warn("Check card-cli failed", e);
+ return false;
+ }
+ }
+
+ public static CardConfig loadCardConfig() throws KeychainAccessException {
+ final File configFile = getCardConfigFile();
+ if (configFile == null) {
+ return new CardConfig();
+ }
+ final String configJson = readFile(configFile);
+ final CardConfig cardConfig;
+ try {
+ cardConfig = new Gson().fromJson(configJson, CardConfig.class);
+ } catch (Exception e) {
+ throw new KeychainAccessException("Parse card-cli config file: " + configFile + " failed", e);
+ }
+ return cardConfig;
+ }
+
+ public static void deletePassword(CardConfig cardConfig, String vault) {
+ final File keyFile = getKeyFile(cardConfig, vault);
+ if (keyFile.exists() && keyFile.isFile()) {
+ keyFile.delete();
+ }
+ }
+
+ public static String loadPassword(CardConfig cardConfig, String vault) throws KeychainAccessException {
+ final File keyFile = getKeyFile(cardConfig, vault);
+ if (isCheckPassphraseStored()) {
+ LOG.info("Check passphrase stored: " + vault + ", exists: " + keyFile.exists());
+ if (keyFile.exists()) {
+ // this is only for check passphrase stored
+ return "123456";
+ }
+ }
+ if (!keyFile.isFile()) {
+ throw new KeychainAccessException("Password key file: " + keyFile + " not found");
+ }
+ final String encryptedKey = readFile(keyFile);
+ final byte[] password = decrypt(cardConfig, encryptedKey);
+ return new String(password, StandardCharsets.UTF_8);
+ }
+
+ public static void storePassword(CardConfig cardConfig, String vault, String name, CharSequence password) throws KeychainAccessException {
+ final String encryptedPassword = encrypt(cardConfig, password.toString().getBytes(StandardCharsets.UTF_8), name);
+ final File keyFile = getKeyFile(cardConfig, vault);
+ writeFile(keyFile, encryptedPassword);
+ }
+
+ private static File getKeyFile(CardConfig cardConfig, String vault) {
+ final StringBuilder sb = new StringBuilder(vault.length());
+ for (char c : vault.toCharArray()) {
+ if ((c >= 'a' && c <= 'z')
+ || (c >= 'A' && c <= 'Z')
+ || (c >= '0' && c <= '9')
+ || (c == '-' || c == '.')) {
+ sb.append(c);
+ } else if (c == '_') {
+ sb.append("__");
+ } else {
+ sb.append('_');
+ final String hex = Integer.toHexString(c);
+ if (hex.length() % 2 != 0) {
+ sb.append('0');
+ }
+ sb.append(hex);
+ }
+ }
+ return new File(getEncryptKeyBasePath(cardConfig), sb.toString());
+ }
+
+ private static String readFile(File file) throws KeychainAccessException {
+ final StringBuilder sb = new StringBuilder((int) file.length());
+ try (final BufferedReader reader = new BufferedReader(new FileReader(file, StandardCharsets.UTF_8))) {
+ for (int b; ((b = reader.read()) != -1); ) {
+ sb.append((char) b);
+ }
+ return sb.toString();
+ } catch (IOException e) {
+ throw new KeychainAccessException("Read file: " + file + " failed", e);
+ }
+ }
+
+ private static void writeFile(File file, String content) throws KeychainAccessException {
+ try (FileOutputStream fos = new FileOutputStream(file)) {
+ fos.write(content.getBytes(StandardCharsets.UTF_8));
+ } catch (Exception e) {
+ throw new KeychainAccessException("Write file: " + file + " failed", e);
+ }
+ }
+
+ private static byte[] decrypt(CardConfig cardConfig, String input) throws KeychainAccessException {
+ final UtilsCommandResult decryptResult = runCardCli(
+ cardConfig,
+ null,
+ "hmac-decrypt",
+ "--ciphertext", input,
+ "--auto-pbe",
+ "--json"
+ );
+ if (decryptResult.getExitValue() != 0) {
+ throw new KeychainAccessException("card-cli decrypt failed: " + decryptResult);
+ }
+ final String resultString = new String(decryptResult.getStdout(), StandardCharsets.UTF_8);
+ final CardHmacDecryptResult result = new Gson().fromJson(resultString, CardHmacDecryptResult.class);
+ return Base64.getDecoder().decode(result.getPlaintext());
+ }
+
+ private static String encrypt(CardConfig cardConfig, byte[] input, String name) throws KeychainAccessException {
+ final UtilsCommandResult encryptResult = runCardCli(
+ cardConfig,
+ null,
+ "hmac-encrypt",
+ "--plaintext", Base64.getEncoder().encodeToString(input),
+ "--with-pbe-encrypt",
+ "--pbe-iteration", "1000000",
+ "--json"
+ );
+ if (encryptResult.getExitValue() != 0) {
+ throw new KeychainAccessException("card-cli encrypt failed: " + encryptResult);
+ }
+ final String resultString = new String(encryptResult.getStdout(), StandardCharsets.UTF_8);
+ final CardHmacEncryptResult result = new Gson().fromJson(resultString, CardHmacEncryptResult.class);
+ return result.getCiphertext();
+ }
+
+ private static UtilsCommandResult runCardCli(CardConfig cardConfig, byte[] input, String... arguments) throws KeychainAccessException {
+ final List commands = new ArrayList<>();
+ commands.add(DEFAULT_CARD_COMMAND);
+ if ((arguments == null) || (arguments.length == 0)) {
+ throw new KeychainAccessException("card-cli not arguments");
+ }
+ commands.addAll(Arrays.asList(arguments));
+ try {
+ final ProcessBuilder processBuilder = new ProcessBuilder(commands);
+ final Process process = processBuilder.start();
+
+ // ----- STD IN -----
+ final AtomicReference inThreadException = new AtomicReference<>();
+ final Thread inThread = new Thread(() -> {
+ if ((input != null) && (input.length > 0)) {
+ try (OutputStream processIn = process.getOutputStream()) {
+ processIn.write(input);
+ } catch (IOException e) {
+ inThreadException.set(e);
+ }
+ }
+ });
+ inThread.setDaemon(true);
+ inThread.setName("card-cli-stdin");
+
+ // ----- STD OUT -----
+ final AtomicReference outThreadException = new AtomicReference<>();
+ final ByteArrayOutputStream outBaos = new ByteArrayOutputStream();
+ final Thread outThread = getThread(process.getInputStream(), outBaos, outThreadException, "card-cli-stdout");
+ // ----- STD ERR -----
+ final AtomicReference errThreadException = new AtomicReference<>();
+ final ByteArrayOutputStream errBaos = new ByteArrayOutputStream();
+ final Thread errThread = getThread(process.getErrorStream(), errBaos, errThreadException, "card-cli-stderr");
+
+ inThread.start();
+ outThread.start();
+ errThread.start();
+
+ inThread.join();
+ if (inThreadException.get() != null) {
+ throw inThreadException.get();
+ }
+ outThread.join();
+ if (outThreadException.get() != null) {
+ throw outThreadException.get();
+ }
+ errThread.join();
+ if (errThreadException.get() != null) {
+ throw errThreadException.get();
+ }
+ final int exitValue = process.waitFor();
+
+ return new UtilsCommandResult(exitValue, outBaos.toByteArray(), errBaos.toByteArray());
+ } catch (Exception e) {
+ throw new KeychainAccessException("Run card-cli command failed: " + commands, e);
+ }
+ }
+
+ private static Thread getThread(InputStream is, ByteArrayOutputStream outBaos, AtomicReference outThreadException, String name) {
+ final Thread outThread = new Thread(() -> {
+ int b;
+ try {
+ while ((b = is.read()) != -1) {
+ outBaos.write(b);
+ }
+ } catch (IOException e) {
+ outThreadException.set(e);
+ }
+ });
+ outThread.setDaemon(true);
+ outThread.setName(name);
+ return outThread;
+ }
+
+ private static File getEncryptKeyBasePath(CardConfig cardConfig) {
+ final File encryptKeyBase;
+ if ((cardConfig != null) && StringUtils.isNoneEmpty(cardConfig.getEncryptKeyBasePath())) {
+ encryptKeyBase = new File(cardConfig.getEncryptKeyBasePath());
+ } else {
+ encryptKeyBase = DEFAULT_ENCRYPTION_KEY_BASE_PATH;
+ }
+ if (encryptKeyBase.isDirectory()) {
+ return encryptKeyBase;
+ }
+ LOG.info("Make dirs: " + encryptKeyBase);
+ encryptKeyBase.mkdirs();
+ return encryptKeyBase;
+ }
+
+ private static File getCardConfigFile() throws KeychainAccessException {
+ for (File configFile : Arrays.asList(CARD_CONFIG_FILE1, CARD_CONFIG_FILE2)) {
+ LOG.info("Check config file: " + configFile + ": " + Arrays.asList(configFile.exists(), configFile.isFile()));
+ if (configFile.exists() && configFile.isFile()) {
+ return configFile;
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/me/hatter/integrations/card/UtilsCommandResult.java b/src/main/java/me/hatter/integrations/card/UtilsCommandResult.java
new file mode 100644
index 0000000..c168f03
--- /dev/null
+++ b/src/main/java/me/hatter/integrations/card/UtilsCommandResult.java
@@ -0,0 +1,40 @@
+package me.hatter.integrations.card;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+/**
+ * @author hatterjiang
+ */
+public class UtilsCommandResult {
+ private final int exitValue;
+ private final byte[] stdout;
+ private final byte[] stderr;
+
+ public UtilsCommandResult(int exitValue, byte[] stdout, byte[] stderr) {
+ this.exitValue = exitValue;
+ this.stdout = stdout;
+ this.stderr = stderr;
+ }
+
+ public int getExitValue() {
+ return exitValue;
+ }
+
+ public byte[] getStdout() {
+ return stdout;
+ }
+
+ public byte[] getStderr() {
+ return stderr;
+ }
+
+ @Override
+ public String toString() {
+ return "CommandResult{" +
+ "exitValue=" + exitValue +
+ ", stdout=" + Arrays.toString(stdout) + " (" + new String(stdout, StandardCharsets.UTF_8) + ")" +
+ ", stderr=" + Arrays.toString(stderr) + " (" + new String(stderr, StandardCharsets.UTF_8) + ")" +
+ '}';
+ }
+}
diff --git a/src/main/resources/META-INF/services/org.cryptomator.integrations.keychain.KeychainAccessProvider b/src/main/resources/META-INF/services/org.cryptomator.integrations.keychain.KeychainAccessProvider
new file mode 100644
index 0000000..14c758c
--- /dev/null
+++ b/src/main/resources/META-INF/services/org.cryptomator.integrations.keychain.KeychainAccessProvider
@@ -0,0 +1 @@
+me.hatter.integrations.card.CardAccessProvider
\ No newline at end of file
diff --git a/src/test/java/me/hatter/integrations/card/KeePassXCAccessTest.java b/src/test/java/me/hatter/integrations/card/KeePassXCAccessTest.java
new file mode 100644
index 0000000..0c14b61
--- /dev/null
+++ b/src/test/java/me/hatter/integrations/card/KeePassXCAccessTest.java
@@ -0,0 +1,20 @@
+package me.hatter.integrations.card;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit test for simple KeePassXCAccess.
+ */
+public class KeePassXCAccessTest
+{
+ /**
+ * Rigorous Test :-)
+ */
+ @Test
+ public void shouldAnswerWithTrue()
+ {
+ assertTrue( true );
+ }
+}