Replace CharBuffer and char[] instances by destroyable Passphrase

This commit is contained in:
Sebastian Stenzel
2021-12-16 16:41:07 +01:00
parent 85a5146d4b
commit d72d9f533c
8 changed files with 270 additions and 26 deletions

View 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;
}
}

View File

@@ -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();
}

View File

@@ -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);
}
/**

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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) {
}