|
@@ -2,6 +2,7 @@ package org.cryptomator.common.keychain;
|
|
|
|
|
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
|
|
import com.github.benmanes.caffeine.cache.LoadingCache;
|
|
|
+import org.cryptomator.common.Passphrase;
|
|
|
import org.cryptomator.integrations.keychain.KeychainAccessException;
|
|
|
import org.cryptomator.integrations.keychain.KeychainAccessProvider;
|
|
|
|
|
@@ -13,20 +14,24 @@ import javafx.beans.property.BooleanProperty;
|
|
|
import javafx.beans.property.ReadOnlyBooleanProperty;
|
|
|
import javafx.beans.property.SimpleBooleanProperty;
|
|
|
import java.util.Arrays;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.concurrent.locks.ReentrantReadWriteLock;
|
|
|
|
|
|
@Singleton
|
|
|
public class KeychainManager implements KeychainAccessProvider {
|
|
|
|
|
|
private final ObjectExpression<KeychainAccessProvider> keychain;
|
|
|
private final LoadingCache<String, BooleanProperty> passphraseStoredProperties;
|
|
|
+ private final ReentrantReadWriteLock lock;
|
|
|
|
|
|
@Inject
|
|
|
KeychainManager(ObjectExpression<KeychainAccessProvider> selectedKeychain) {
|
|
|
this.keychain = selectedKeychain;
|
|
|
this.passphraseStoredProperties = Caffeine.newBuilder() //
|
|
|
- .weakValues() //
|
|
|
+ .softValues() //
|
|
|
.build(this::createStoredPassphraseProperty);
|
|
|
keychain.addListener(ignored -> passphraseStoredProperties.invalidateAll());
|
|
|
+ this.lock = new ReentrantReadWriteLock(false);
|
|
|
}
|
|
|
|
|
|
private KeychainAccessProvider getKeychainOrFail() throws KeychainAccessException {
|
|
@@ -42,29 +47,59 @@ public class KeychainManager implements KeychainAccessProvider {
|
|
|
return getClass().getName();
|
|
|
}
|
|
|
|
|
|
+ @Override
|
|
|
+ public void storePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException {
|
|
|
+ storePassphrase(key, displayName, passphrase, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ //TODO: remove ignored parameter once the API is fixed
|
|
|
@Override
|
|
|
public void storePassphrase(String key, String displayName, CharSequence passphrase, boolean ignored) throws KeychainAccessException {
|
|
|
- getKeychainOrFail().storePassphrase(key, displayName, passphrase);
|
|
|
+ try {
|
|
|
+ lock.writeLock().lock();
|
|
|
+ var kc = getKeychainOrFail();
|
|
|
+ //this is the only keychain actually using the parameter
|
|
|
+ var usesOSAuth = (kc.getClass().getName().equals("org.cryptomator.macos.keychain.TouchIdKeychainAccess"));
|
|
|
+ kc.storePassphrase(key, displayName, passphrase, usesOSAuth);
|
|
|
+ } finally {
|
|
|
+ lock.writeLock().unlock();
|
|
|
+ }
|
|
|
setPassphraseStored(key, true);
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public char[] loadPassphrase(String key) throws KeychainAccessException {
|
|
|
- char[] passphrase = getKeychainOrFail().loadPassphrase(key);
|
|
|
+ char[] passphrase = null;
|
|
|
+ try {
|
|
|
+ lock.readLock().lock();
|
|
|
+ passphrase = getKeychainOrFail().loadPassphrase(key);
|
|
|
+ } finally {
|
|
|
+ lock.readLock().unlock();
|
|
|
+ }
|
|
|
setPassphraseStored(key, passphrase != null);
|
|
|
return passphrase;
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void deletePassphrase(String key) throws KeychainAccessException {
|
|
|
- getKeychainOrFail().deletePassphrase(key);
|
|
|
+ try {
|
|
|
+ lock.writeLock().lock();
|
|
|
+ getKeychainOrFail().deletePassphrase(key);
|
|
|
+ } finally {
|
|
|
+ lock.writeLock().unlock();
|
|
|
+ }
|
|
|
setPassphraseStored(key, false);
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void changePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException {
|
|
|
if (isPassphraseStored(key)) {
|
|
|
- getKeychainOrFail().changePassphrase(key, displayName, passphrase);
|
|
|
+ try {
|
|
|
+ lock.writeLock().lock();
|
|
|
+ getKeychainOrFail().changePassphrase(key, displayName, passphrase);
|
|
|
+ } finally {
|
|
|
+ lock.writeLock().unlock();
|
|
|
+ }
|
|
|
setPassphraseStored(key, true);
|
|
|
}
|
|
|
}
|
|
@@ -101,13 +136,11 @@ public class KeychainManager implements KeychainAccessProvider {
|
|
|
}
|
|
|
|
|
|
private void setPassphraseStored(String key, boolean value) {
|
|
|
- BooleanProperty property = passphraseStoredProperties.getIfPresent(key);
|
|
|
- if (property != null) {
|
|
|
- if (Platform.isFxApplicationThread()) {
|
|
|
- property.set(value);
|
|
|
- } else {
|
|
|
- Platform.runLater(() -> property.set(value));
|
|
|
- }
|
|
|
+ BooleanProperty property = passphraseStoredProperties.get(key, _ -> new SimpleBooleanProperty(value));
|
|
|
+ if (Platform.isFxApplicationThread()) {
|
|
|
+ property.set(value);
|
|
|
+ } else {
|
|
|
+ Platform.runLater(() -> property.set(value));
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -134,4 +167,22 @@ public class KeychainManager implements KeychainAccessProvider {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ public ObjectExpression<KeychainAccessProvider> getKeychainImplementation() {
|
|
|
+ return this.keychain;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static void migrate(KeychainAccessProvider oldProvider, KeychainAccessProvider newProvider, Map<String, String> idsAndNames) throws KeychainAccessException {
|
|
|
+ if (oldProvider instanceof KeychainManager || newProvider instanceof KeychainManager) {
|
|
|
+ throw new IllegalArgumentException("KeychainManger must not be the source or target of migration");
|
|
|
+ }
|
|
|
+ for (var entry : idsAndNames.entrySet()) {
|
|
|
+ var passphrase = oldProvider.loadPassphrase(entry.getKey());
|
|
|
+ if (passphrase != null) {
|
|
|
+ var wrapper = new Passphrase(passphrase);
|
|
|
+ oldProvider.deletePassphrase(entry.getKey()); //we cannot apply "first-write-then-delete" pattern here, since we can potentially write to the same passphrase store (e.g., touchID and regular keychain)
|
|
|
+ newProvider.storePassphrase(entry.getKey(), entry.getValue(), wrapper);
|
|
|
+ wrapper.destroy();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|