diff --git a/main/keychain/pom.xml b/main/keychain/pom.xml
index 1983d5873..5c96a8aad 100644
--- a/main/keychain/pom.xml
+++ b/main/keychain/pom.xml
@@ -13,6 +13,15 @@
org.apache.commons
commons-lang3
+
+ com.google.code.gson
+ gson
+ 2.7
+
+
+ commons-codec
+ commons-codec
+
org.bouncycastle
bcprov-jdk15on
diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java
index ab593f674..3f2cf6abc 100644
--- a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java
+++ b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java
@@ -3,18 +3,20 @@ package org.cryptomator.keychain;
import java.util.Optional;
import java.util.Set;
+import org.cryptomator.jni.JniModule;
+
import com.google.common.collect.Sets;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.ElementsIntoSet;
-@Module
+@Module(includes = {JniModule.class})
public class KeychainModule {
@Provides
@ElementsIntoSet
- Set provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsSystemKeychainAccess winKeychain) {
+ Set provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsProtectedKeychainAccess winKeychain) {
return Sets.newHashSet(macKeychain, winKeychain);
}
diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java
index a80d084ef..5e8b27b6c 100644
--- a/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java
+++ b/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java
@@ -1,21 +1,21 @@
package org.cryptomator.keychain;
+import java.util.Optional;
+
import javax.inject.Inject;
-import javax.inject.Singleton;
import org.apache.commons.lang3.SystemUtils;
-import org.cryptomator.jni.JniModule;
+import org.cryptomator.jni.MacFunctions;
import org.cryptomator.jni.MacKeychainAccess;
-@Singleton
class MacSystemKeychainAccess implements KeychainAccessStrategy {
private final MacKeychainAccess keychain;
@Inject
- public MacSystemKeychainAccess() {
- if (JniModule.macFunctions().isPresent()) {
- this.keychain = JniModule.macFunctions().get().getKeychainAccess();
+ public MacSystemKeychainAccess(Optional macFunctions) {
+ if (macFunctions.isPresent()) {
+ this.keychain = macFunctions.get().keychainAccess();
} else {
this.keychain = null;
}
diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java
new file mode 100644
index 000000000..205772d15
--- /dev/null
+++ b/main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java
@@ -0,0 +1,188 @@
+package org.cryptomator.keychain;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.UncheckedIOException;
+import java.io.Writer;
+import java.lang.reflect.Type;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.jni.WinDataProtection;
+import org.cryptomator.jni.WinFunctions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.reflect.TypeToken;
+
+class WindowsProtectedKeychainAccess implements KeychainAccessStrategy {
+
+ private static final Logger LOG = LoggerFactory.getLogger(WindowsProtectedKeychainAccess.class);
+ private static final Gson GSON = new GsonBuilder().setPrettyPrinting() //
+ .registerTypeHierarchyAdapter(byte[].class, new ByteArrayJsonAdapter()) //
+ .disableHtmlEscaping().create();
+
+ private final WinDataProtection dataProtection;
+ private final Path keychainPath;
+ private Map keychainEntries;
+
+ @Inject
+ public WindowsProtectedKeychainAccess(Optional winFunctions) {
+ if (winFunctions.isPresent()) {
+ this.dataProtection = winFunctions.get().dataProtection();
+ } else {
+ this.dataProtection = null;
+ }
+ final String keychainPathProperty = System.getProperty("cryptomator.keychainPath");
+ if (dataProtection != null && keychainPathProperty == null) {
+ LOG.warn("Windows DataProtection module loaded, but no keychainPath configured.");
+ }
+ if (keychainPathProperty != null) {
+ this.keychainPath = FileSystems.getDefault().getPath(keychainPathProperty);
+ } else {
+ this.keychainPath = null;
+ }
+ }
+
+ @Override
+ public void storePassphrase(String key, CharSequence passphrase) {
+ loadKeychainEntriesIfNeeded();
+ ByteBuffer buf = UTF_8.encode(CharBuffer.wrap(passphrase));
+ byte[] cleartext = new byte[buf.remaining()];
+ buf.get(cleartext);
+ KeychainEntry entry = new KeychainEntry();
+ entry.salt = generateSalt();
+ entry.ciphertext = dataProtection.protect(cleartext, entry.salt);
+ Arrays.fill(buf.array(), (byte) 0x00);
+ Arrays.fill(cleartext, (byte) 0x00);
+ keychainEntries.put(key, entry);
+ saveKeychainEntries();
+ }
+
+ @Override
+ public char[] loadPassphrase(String key) {
+ loadKeychainEntriesIfNeeded();
+ KeychainEntry entry = keychainEntries.get(key);
+ if (entry == null) {
+ return null;
+ }
+ byte[] cleartext = dataProtection.unprotect(entry.ciphertext, entry.salt);
+ if (cleartext == null) {
+ return null;
+ }
+ CharBuffer buf = UTF_8.decode(ByteBuffer.wrap(cleartext));
+ char[] passphrase = new char[buf.remaining()];
+ buf.get(passphrase);
+ Arrays.fill(cleartext, (byte) 0x00);
+ Arrays.fill(buf.array(), (char) 0x00);
+ return passphrase;
+ }
+
+ @Override
+ public void deletePassphrase(String key) {
+ loadKeychainEntriesIfNeeded();
+ keychainEntries.remove(key);
+ saveKeychainEntries();
+ }
+
+ @Override
+ public boolean isSupported() {
+ return SystemUtils.IS_OS_WINDOWS && dataProtection != null && keychainPath != null;
+ }
+
+ private byte[] generateSalt() {
+ byte[] result = new byte[2 * Long.BYTES];
+ UUID uuid = UUID.randomUUID();
+ ByteBuffer buf = ByteBuffer.wrap(result);
+ buf.putLong(uuid.getMostSignificantBits());
+ buf.putLong(uuid.getLeastSignificantBits());
+ return result;
+ }
+
+ private void loadKeychainEntriesIfNeeded() {
+ if (keychainEntries == null) {
+ loadKeychainEntries();
+ }
+ assert keychainEntries != null;
+ }
+
+ private void loadKeychainEntries() {
+ Type type = new TypeToken