mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-18 10:41:26 +00:00
Replace CharBuffer and char[] instances by destroyable Passphrase
This commit is contained in:
115
src/main/java/org/cryptomator/common/Passphrase.java
Normal file
115
src/main/java/org/cryptomator/common/Passphrase.java
Normal file
@@ -0,0 +1,115 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
import org.cryptomator.cryptolib.common.MessageDigestSupplier;
|
||||
|
||||
import javax.security.auth.Destroyable;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* A destroyable CharSequence.
|
||||
*/
|
||||
public class Passphrase implements Destroyable, CharSequence {
|
||||
|
||||
private final char[] data;
|
||||
private final int offset;
|
||||
private final int length;
|
||||
private boolean destroyed;
|
||||
|
||||
/**
|
||||
* Wraps (doesn't copy) the given data.
|
||||
*
|
||||
* @param data The wrapped data. Any changes to this will be reflected in this passphrase
|
||||
*/
|
||||
public Passphrase(char[] data) {
|
||||
this(data, 0, data.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps (doesn't copy) a subarray of the given data.
|
||||
*
|
||||
* @param data The wrapped data. Any changes to this will be reflected in this passphrase
|
||||
* @param offset The subarray offset, i.e. the first character of this passphrase
|
||||
* @param length The subarray length, i.e. the length of this passphrase
|
||||
*/
|
||||
public Passphrase(char[] data, int offset, int length) {
|
||||
if (offset < 0 || length < 0 || offset + length > data.length) {
|
||||
throw new IndexOutOfBoundsException("[%1$d %1$d + %2$d[ not within [0, %3$d[".formatted(offset, length, data.length));
|
||||
}
|
||||
this.data = data;
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public static Passphrase copyOf(CharSequence cs) {
|
||||
char[] result = new char[cs.length()];
|
||||
for (int i = 0; i < cs.length(); i++) {
|
||||
result[i] = cs.charAt(i);
|
||||
}
|
||||
return new Passphrase(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Passphrase that = (Passphrase) o;
|
||||
// time-constant comparison
|
||||
int diff = 0;
|
||||
for (int i = 0; i < length; i++) {
|
||||
diff |= charAt(i) ^ that.charAt(i);
|
||||
}
|
||||
return diff == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
// TODO: do we really need to a secure hashcode? toString leaks the pw anyway
|
||||
var md = MessageDigestSupplier.SHA256.get();
|
||||
ByteBuffer buf = ByteBuffer.allocate(Character.BYTES * length);
|
||||
for (int i = 0; i < length; i++) {
|
||||
char c = charAt(i);
|
||||
buf.putChar(i * Character.BYTES, c);
|
||||
}
|
||||
buf.flip();
|
||||
md.update(buf);
|
||||
return Arrays.hashCode(md.digest());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new String(data, offset, length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length() {
|
||||
return length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public char charAt(int index) {
|
||||
if (index < 0 || index >= length) {
|
||||
throw new IndexOutOfBoundsException("%d not within [0, %d[".formatted(index, length));
|
||||
}
|
||||
return data[offset + index];
|
||||
}
|
||||
|
||||
@Override
|
||||
public Passphrase subSequence(int start, int end) {
|
||||
if (start < 0 || end < 0 || end > length || start > end) {
|
||||
throw new IndexOutOfBoundsException("[%d, %d[ not within [0, %d[".formatted(start, end, length));
|
||||
}
|
||||
return new Passphrase(Arrays.copyOfRange(data, offset + start, offset + end));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
return destroyed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
Arrays.fill(data, offset, offset + length, '\0');
|
||||
destroyed = true;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.cryptomator.ui.controls;
|
||||
|
||||
import org.cryptomator.common.Passphrase;
|
||||
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.StringProperty;
|
||||
@@ -82,7 +84,7 @@ public class NiceSecurePasswordField extends StackPane {
|
||||
return passwordField.textProperty();
|
||||
}
|
||||
|
||||
public CharSequence getCharacters() {
|
||||
public Passphrase getCharacters() {
|
||||
return passwordField.getCharacters();
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
package org.cryptomator.ui.controls;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import org.cryptomator.common.Passphrase;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.NamedArg;
|
||||
@@ -28,7 +29,6 @@ import javafx.scene.input.KeyCodeCombination;
|
||||
import javafx.scene.input.KeyCombination;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.input.TransferMode;
|
||||
import java.nio.CharBuffer;
|
||||
import java.text.Normalizer;
|
||||
import java.text.Normalizer.Form;
|
||||
import java.util.Arrays;
|
||||
@@ -203,8 +203,8 @@ public class SecurePasswordField extends TextField {
|
||||
* @see #wipe()
|
||||
*/
|
||||
@Override
|
||||
public CharSequence getCharacters() {
|
||||
return CharBuffer.wrap(content, 0, length);
|
||||
public Passphrase getCharacters() {
|
||||
return new Passphrase(content, 0, length);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.cryptomator.integrations.keychain.KeychainAccessException;
|
||||
import org.cryptomator.ui.common.Animations;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.common.Passphrase;
|
||||
import org.cryptomator.ui.common.UserInteractionLock;
|
||||
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
|
||||
@@ -26,10 +27,8 @@ import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
@@ -49,7 +48,7 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
|
||||
private final AtomicReference<Path> filePath;
|
||||
private final KeychainManager keychain;
|
||||
|
||||
private char[] passphrase;
|
||||
private Passphrase passphrase;
|
||||
private boolean savePassphrase;
|
||||
private boolean wrongPassphrase;
|
||||
|
||||
@@ -63,7 +62,7 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
|
||||
this.masterkeyFileProvisionLock = masterkeyFileProvisionLock;
|
||||
this.filePath = filePath;
|
||||
this.keychain = keychain;
|
||||
this.passphrase = savedPassphrase.orElse(null);
|
||||
this.passphrase = savedPassphrase.map(Passphrase::new).orElse(null);
|
||||
this.savePassphrase = savedPassphrase.isPresent();
|
||||
}
|
||||
|
||||
@@ -78,7 +77,7 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
|
||||
if (passphrase == null) {
|
||||
askForPassphrase();
|
||||
}
|
||||
var masterkey = masterkeyFileAccess.load(filePath, CharBuffer.wrap(passphrase));
|
||||
var masterkey = masterkeyFileAccess.load(filePath, passphrase);
|
||||
//backup
|
||||
if (filePath.startsWith(vault.getPath())) {
|
||||
try {
|
||||
@@ -100,7 +99,7 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
|
||||
public boolean recoverFromException(MasterkeyLoadingFailedException exception) {
|
||||
if (exception instanceof InvalidPassphraseException) {
|
||||
this.wrongPassphrase = true;
|
||||
Arrays.fill(passphrase, '\0');
|
||||
passphrase.destroy();
|
||||
this.passphrase = null;
|
||||
return true; // reattempting key load
|
||||
} else {
|
||||
@@ -113,13 +112,15 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
|
||||
if (unlockedSuccessfully && savePassphrase) {
|
||||
savePasswordToSystemkeychain(passphrase);
|
||||
}
|
||||
Arrays.fill(passphrase, '\0');
|
||||
if (passphrase != null) {
|
||||
passphrase.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
private void savePasswordToSystemkeychain(char[] passphrase) {
|
||||
private void savePasswordToSystemkeychain(Passphrase passphrase) {
|
||||
if (keychain.isSupported()) {
|
||||
try {
|
||||
keychain.storePassphrase(vault.getId(), vault.getDisplayName(), CharBuffer.wrap(passphrase));
|
||||
keychain.storePassphrase(vault.getId(), vault.getDisplayName(), passphrase);
|
||||
} catch (KeychainAccessException e) {
|
||||
LOG.error("Failed to store passphrase in system keychain.", e);
|
||||
}
|
||||
|
||||
@@ -3,15 +3,11 @@ package org.cryptomator.ui.keyloading.masterkeyfile;
|
||||
import dagger.BindsInstance;
|
||||
import dagger.Subcomponent;
|
||||
import org.cryptomator.common.Nullable;
|
||||
import org.cryptomator.ui.common.Animations;
|
||||
import org.cryptomator.common.Passphrase;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
@PassphraseEntryScoped
|
||||
@Subcomponent(modules = {PassphraseEntryModule.class})
|
||||
@@ -27,7 +23,7 @@ public interface PassphraseEntryComponent {
|
||||
interface Builder {
|
||||
|
||||
@BindsInstance
|
||||
PassphraseEntryComponent.Builder savedPassword(@Nullable @Named("savedPassword") char[] savedPassword);
|
||||
PassphraseEntryComponent.Builder savedPassword(@Nullable @Named("savedPassword") Passphrase savedPassword);
|
||||
|
||||
PassphraseEntryComponent build();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import org.cryptomator.common.Nullable;
|
||||
import org.cryptomator.common.keychain.KeychainManager;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.common.Passphrase;
|
||||
import org.cryptomator.ui.common.WeakBindings;
|
||||
import org.cryptomator.ui.controls.NiceSecurePasswordField;
|
||||
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
|
||||
@@ -44,7 +45,7 @@ public class PassphraseEntryController implements FxController {
|
||||
private final Stage window;
|
||||
private final Vault vault;
|
||||
private final CompletableFuture<PassphraseEntryResult> result;
|
||||
private final char[] savedPassword;
|
||||
private final Passphrase savedPassword;
|
||||
private final ForgetPasswordComponent.Builder forgetPassword;
|
||||
private final KeychainManager keychain;
|
||||
private final StringBinding vaultName;
|
||||
@@ -63,7 +64,7 @@ public class PassphraseEntryController implements FxController {
|
||||
public Animation unlockAnimation;
|
||||
|
||||
@Inject
|
||||
public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading Vault vault, CompletableFuture<PassphraseEntryResult> result, @Nullable @Named("savedPassword") char[] savedPassword, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) {
|
||||
public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading Vault vault, CompletableFuture<PassphraseEntryResult> result, @Nullable @Named("savedPassword") Passphrase savedPassword, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) {
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
this.result = result;
|
||||
@@ -137,10 +138,7 @@ public class PassphraseEntryController implements FxController {
|
||||
LOG.trace("UnlockController.unlock()");
|
||||
unlockInProgress.set(true);
|
||||
CharSequence pwFieldContents = passwordField.getCharacters();
|
||||
char[] pw = new char[pwFieldContents.length()];
|
||||
for (int i = 0; i < pwFieldContents.length(); i++) {
|
||||
pw[i] = pwFieldContents.charAt(i);
|
||||
}
|
||||
Passphrase pw = Passphrase.copyOf(pwFieldContents);
|
||||
result.complete(new PassphraseEntryResult(pw, savePasswordCheckbox.isSelected()));
|
||||
startUnlockAnimation();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.cryptomator.ui.keyloading.masterkeyfile;
|
||||
|
||||
import org.cryptomator.common.Passphrase;
|
||||
|
||||
// TODO needs to be public due to Dagger -.-
|
||||
public record PassphraseEntryResult(char[] passphrase, boolean savePassphrase) {
|
||||
public record PassphraseEntryResult(Passphrase passphrase, boolean savePassphrase) {
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user