mirror of
https://github.com/cryptomator/cryptomator.git
synced 2025-02-13 22:41:39 +00:00
- AES: support for multiple masterkey files
- GUI: one masterkey per user
This commit is contained in:
parent
fd9ca14d85
commit
2fcdd4eb01
@ -81,8 +81,8 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, Fi
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* The decrypted master key. Its lifecycle starts with {@link #unlockStorage(Path, CharSequence)} or
|
||||
* {@link #initializeStorage(Path, CharSequence)}. Its lifecycle ends with {@link #swipeSensitiveData()}.
|
||||
* The decrypted master key. Its lifecycle starts with {@link #randomData(int)} or {@link #encryptMasterKey(Path, CharSequence)}. Its
|
||||
* lifecycle ends with {@link #swipeSensitiveData()}.
|
||||
*/
|
||||
private final byte[] masterKey = new byte[MASTER_KEY_LENGTH];
|
||||
|
||||
@ -100,11 +100,19 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, Fi
|
||||
}
|
||||
}
|
||||
|
||||
public void initializeStorage(OutputStream masterkey, CharSequence password) throws IOException {
|
||||
try {
|
||||
// generate new masterkey:
|
||||
randomMasterKey();
|
||||
/**
|
||||
* Fills the masterkey with new random bytes.
|
||||
*/
|
||||
public void randomizeMasterKey() {
|
||||
SECURE_PRNG.setSeed(SECURE_PRNG.generateSeed(PRNG_SEED_LENGTH));
|
||||
SECURE_PRNG.nextBytes(this.masterKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts the current masterKey with the given password and writes the result to the given output stream.
|
||||
*/
|
||||
public void encryptMasterKey(OutputStream out, CharSequence password) throws IOException {
|
||||
try {
|
||||
// derive key:
|
||||
final byte[] userSalt = randomData(SALT_LENGTH);
|
||||
final SecretKey userKey = pbkdf2(password, userSalt, PBKDF2_PW_ITERATIONS, AES_KEY_LENGTH);
|
||||
@ -116,48 +124,53 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, Fi
|
||||
byte[] encryptedMasterKey = encCipher.doFinal(this.masterKey);
|
||||
|
||||
// save encrypted masterkey:
|
||||
final Keys keys = new Keys();
|
||||
final Keys.Key ownerKey = new Keys.Key();
|
||||
ownerKey.setIterations(PBKDF2_PW_ITERATIONS);
|
||||
ownerKey.setIv(iv);
|
||||
ownerKey.setKeyLength(AES_KEY_LENGTH);
|
||||
ownerKey.setMasterkey(encryptedMasterKey);
|
||||
ownerKey.setSalt(userSalt);
|
||||
ownerKey.setPwVerification(encryptedUserKey);
|
||||
keys.setOwnerKey(ownerKey);
|
||||
objectMapper.writeValue(masterkey, keys);
|
||||
final Key key = new Key();
|
||||
key.setIterations(PBKDF2_PW_ITERATIONS);
|
||||
key.setIv(iv);
|
||||
key.setKeyLength(AES_KEY_LENGTH);
|
||||
key.setMasterkey(encryptedMasterKey);
|
||||
key.setSalt(userSalt);
|
||||
key.setPwVerification(encryptedUserKey);
|
||||
objectMapper.writeValue(out, key);
|
||||
} catch (IllegalBlockSizeException | BadPaddingException ex) {
|
||||
throw new IllegalStateException("Block size hard coded. Padding irrelevant in ENCRYPT_MODE. IV must exist in CBC mode.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void unlockStorage(InputStream masterkey, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException {
|
||||
/**
|
||||
* Reads the encrypted masterkey from the given input stream and decrypts it with the given password.
|
||||
*
|
||||
* @throws DecryptFailedException If the decryption failed for various reasons (including wrong password).
|
||||
* @throws WrongPasswordException If the provided password was wrong. Note: Sometimes the algorithm itself fails due to a wrong
|
||||
* password. In this case a DecryptFailedException will be thrown.
|
||||
* @throws UnsupportedKeyLengthException If the masterkey has been encrypted with a higher key length than supported by the system. In
|
||||
* this case Java JCE needs to be installed.
|
||||
*/
|
||||
public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException {
|
||||
byte[] decrypted = new byte[0];
|
||||
try {
|
||||
// load encrypted masterkey:
|
||||
final Keys keys = objectMapper.readValue(masterkey, Keys.class);
|
||||
;
|
||||
final Keys.Key ownerKey = keys.getOwnerKey();
|
||||
final Key key = objectMapper.readValue(in, Key.class);
|
||||
|
||||
// check, whether the key length is supported:
|
||||
final int maxKeyLen = Cipher.getMaxAllowedKeyLength(CRYPTO_ALGORITHM);
|
||||
if (ownerKey.getKeyLength() > maxKeyLen) {
|
||||
throw new UnsupportedKeyLengthException(ownerKey.getKeyLength(), maxKeyLen);
|
||||
if (key.getKeyLength() > maxKeyLen) {
|
||||
throw new UnsupportedKeyLengthException(key.getKeyLength(), maxKeyLen);
|
||||
}
|
||||
|
||||
// derive key:
|
||||
final SecretKey userKey = pbkdf2(password, ownerKey.getSalt(), ownerKey.getIterations(), ownerKey.getKeyLength());
|
||||
final SecretKey userKey = pbkdf2(password, key.getSalt(), key.getIterations(), key.getKeyLength());
|
||||
|
||||
// check password:
|
||||
final Cipher encCipher = this.cipher(MASTERKEY_CIPHER, userKey, ownerKey.getIv(), Cipher.ENCRYPT_MODE);
|
||||
final Cipher encCipher = this.cipher(MASTERKEY_CIPHER, userKey, key.getIv(), Cipher.ENCRYPT_MODE);
|
||||
byte[] encryptedUserKey = encCipher.doFinal(userKey.getEncoded());
|
||||
if (!Arrays.equals(ownerKey.getPwVerification(), encryptedUserKey)) {
|
||||
if (!Arrays.equals(key.getPwVerification(), encryptedUserKey)) {
|
||||
throw new WrongPasswordException();
|
||||
}
|
||||
|
||||
// decrypt:
|
||||
final Cipher decCipher = this.cipher(MASTERKEY_CIPHER, userKey, ownerKey.getIv(), Cipher.DECRYPT_MODE);
|
||||
decrypted = decCipher.doFinal(ownerKey.getMasterkey());
|
||||
final Cipher decCipher = this.cipher(MASTERKEY_CIPHER, userKey, key.getIv(), Cipher.DECRYPT_MODE);
|
||||
decrypted = decCipher.doFinal(key.getMasterkey());
|
||||
|
||||
// everything ok, move decrypted data to masterkey:
|
||||
final ByteBuffer masterKeyBuffer = ByteBuffer.wrap(this.masterKey);
|
||||
@ -199,11 +212,6 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, Fi
|
||||
return result;
|
||||
}
|
||||
|
||||
private void randomMasterKey() {
|
||||
SECURE_PRNG.setSeed(SECURE_PRNG.generateSeed(PRNG_SEED_LENGTH));
|
||||
SECURE_PRNG.nextBytes(this.masterKey);
|
||||
}
|
||||
|
||||
private SecretKey pbkdf2(byte[] password, byte[] salt, int iterations, int keyLength) {
|
||||
final char[] pw = new char[password.length];
|
||||
try {
|
||||
|
@ -7,19 +7,19 @@
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package de.sebastianstenzel.oce.crypto.aes256;
|
||||
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.PathMatcher;
|
||||
|
||||
import org.apache.commons.codec.binary.Base32;
|
||||
import org.apache.commons.codec.binary.BaseNCodec;
|
||||
|
||||
|
||||
interface FileNamingConventions {
|
||||
|
||||
/**
|
||||
* Name of the masterkey file inside the root directory of the encrypted storage.
|
||||
* Extension of masterkey files inside the root directory of the encrypted storage.
|
||||
*/
|
||||
String MASTERKEY_FILENAME = "masterkey.json";
|
||||
String MASTERKEY_FILE_EXT = ".masterkey.json";
|
||||
|
||||
/**
|
||||
* How to encode the encrypted file names safely.
|
||||
|
@ -0,0 +1,67 @@
|
||||
package de.sebastianstenzel.oce.crypto.aes256;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
|
||||
@JsonPropertyOrder(value = { "salt", "iv", "iterations", "keyLength", "masterkey" })
|
||||
public class Key implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 8578363158959619885L;
|
||||
private byte[] salt;
|
||||
private byte[] iv;
|
||||
private int iterations;
|
||||
private int keyLength;
|
||||
private byte[] pwVerification;
|
||||
private byte[] masterkey;
|
||||
|
||||
public byte[] getSalt() {
|
||||
return salt;
|
||||
}
|
||||
|
||||
public void setSalt(byte[] salt) {
|
||||
this.salt = salt;
|
||||
}
|
||||
|
||||
public byte[] getIv() {
|
||||
return iv;
|
||||
}
|
||||
|
||||
public void setIv(byte[] iv) {
|
||||
this.iv = iv;
|
||||
}
|
||||
|
||||
public int getIterations() {
|
||||
return iterations;
|
||||
}
|
||||
|
||||
public void setIterations(int iterations) {
|
||||
this.iterations = iterations;
|
||||
}
|
||||
|
||||
public int getKeyLength() {
|
||||
return keyLength;
|
||||
}
|
||||
|
||||
public void setKeyLength(int keyLength) {
|
||||
this.keyLength = keyLength;
|
||||
}
|
||||
|
||||
public byte[] getPwVerification() {
|
||||
return pwVerification;
|
||||
}
|
||||
|
||||
public void setPwVerification(byte[] pwVerification) {
|
||||
this.pwVerification = pwVerification;
|
||||
}
|
||||
|
||||
public byte[] getMasterkey() {
|
||||
return masterkey;
|
||||
}
|
||||
|
||||
public void setMasterkey(byte[] masterkey) {
|
||||
this.masterkey = masterkey;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,105 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package de.sebastianstenzel.oce.crypto.aes256;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
||||
@JsonPropertyOrder(value = { "ownerKey", "additionalKeys" })
|
||||
class Keys implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = -19303594304327167L;
|
||||
private Key ownerKey;
|
||||
@JsonDeserialize(as = HashMap.class)
|
||||
private Map<String, Key> additionalKeys;
|
||||
|
||||
public Key getOwnerKey() {
|
||||
return ownerKey;
|
||||
}
|
||||
|
||||
public void setOwnerKey(Key ownerKey) {
|
||||
this.ownerKey = ownerKey;
|
||||
}
|
||||
|
||||
public Map<String, Key> getAdditionalKeys() {
|
||||
return additionalKeys;
|
||||
}
|
||||
|
||||
public void setAdditionalKeys(Map<String, Key> additionalKeys) {
|
||||
this.additionalKeys = additionalKeys;
|
||||
}
|
||||
|
||||
@JsonPropertyOrder(value = { "salt", "iv", "iterations", "keyLength", "masterkey" })
|
||||
public static class Key implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 8578363158959619885L;
|
||||
private byte[] salt;
|
||||
private byte[] iv;
|
||||
private int iterations;
|
||||
private int keyLength;
|
||||
private byte[] pwVerification;
|
||||
private byte[] masterkey;
|
||||
|
||||
public byte[] getSalt() {
|
||||
return salt;
|
||||
}
|
||||
|
||||
public void setSalt(byte[] salt) {
|
||||
this.salt = salt;
|
||||
}
|
||||
|
||||
public byte[] getIv() {
|
||||
return iv;
|
||||
}
|
||||
|
||||
public void setIv(byte[] iv) {
|
||||
this.iv = iv;
|
||||
}
|
||||
|
||||
public int getIterations() {
|
||||
return iterations;
|
||||
}
|
||||
|
||||
public void setIterations(int iterations) {
|
||||
this.iterations = iterations;
|
||||
}
|
||||
|
||||
public int getKeyLength() {
|
||||
return keyLength;
|
||||
}
|
||||
|
||||
public void setKeyLength(int keyLength) {
|
||||
this.keyLength = keyLength;
|
||||
}
|
||||
|
||||
public byte[] getPwVerification() {
|
||||
return pwVerification;
|
||||
}
|
||||
|
||||
public void setPwVerification(byte[] pwVerification) {
|
||||
this.pwVerification = pwVerification;
|
||||
}
|
||||
|
||||
public byte[] getMasterkey() {
|
||||
return masterkey;
|
||||
}
|
||||
|
||||
public void setMasterkey(byte[] masterkey) {
|
||||
this.masterkey = masterkey;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -2,9 +2,22 @@ package de.sebastianstenzel.oce.crypto.exceptions;
|
||||
|
||||
public class UnsupportedKeyLengthException extends StorageCryptingException {
|
||||
private static final long serialVersionUID = 8114147446419390179L;
|
||||
|
||||
|
||||
private final int requestedLength;
|
||||
private final int supportedLength;
|
||||
|
||||
public UnsupportedKeyLengthException(int length, int maxLength) {
|
||||
super(String.format("Key length (%i) exceeds policy maximum (%i).", length, maxLength));
|
||||
this.requestedLength = length;
|
||||
this.supportedLength = maxLength;
|
||||
}
|
||||
|
||||
|
||||
public int getRequestedLength() {
|
||||
return requestedLength;
|
||||
}
|
||||
|
||||
public int getSupportedLength() {
|
||||
return supportedLength;
|
||||
}
|
||||
|
||||
}
|
@ -37,7 +37,7 @@ public class Aes256CryptorTest {
|
||||
final String tmpDirName = (String) System.getProperties().get("java.io.tmpdir");
|
||||
final Path path = FileSystems.getDefault().getPath(tmpDirName);
|
||||
tmpDir = Files.createTempDirectory(path, "oce-crypto-test");
|
||||
masterKey = tmpDir.resolve(Aes256Cryptor.MASTERKEY_FILENAME);
|
||||
masterKey = tmpDir.resolve("test" + Aes256Cryptor.MASTERKEY_FILE_EXT);
|
||||
}
|
||||
|
||||
@After
|
||||
@ -52,12 +52,12 @@ public class Aes256CryptorTest {
|
||||
final String pw = "asd";
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
cryptor.initializeStorage(out, pw);
|
||||
cryptor.encryptMasterKey(out, pw);
|
||||
cryptor.swipeSensitiveData();
|
||||
|
||||
final Aes256Cryptor decryptor = new Aes256Cryptor();
|
||||
final InputStream in = Files.newInputStream(masterKey, StandardOpenOption.READ);
|
||||
decryptor.unlockStorage(in, pw);
|
||||
decryptor.decryptMasterKey(in, pw);
|
||||
}
|
||||
|
||||
@Test(expected = WrongPasswordException.class)
|
||||
@ -65,13 +65,13 @@ public class Aes256CryptorTest {
|
||||
final String pw = "asd";
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
cryptor.initializeStorage(out, pw);
|
||||
cryptor.encryptMasterKey(out, pw);
|
||||
cryptor.swipeSensitiveData();
|
||||
|
||||
final String wrongPw = "foo";
|
||||
final Aes256Cryptor decryptor = new Aes256Cryptor();
|
||||
final InputStream in = Files.newInputStream(masterKey, StandardOpenOption.READ);
|
||||
decryptor.unlockStorage(in, wrongPw);
|
||||
decryptor.decryptMasterKey(in, wrongPw);
|
||||
}
|
||||
|
||||
@Test(expected = NoSuchFileException.class)
|
||||
@ -79,13 +79,13 @@ public class Aes256CryptorTest {
|
||||
final String pw = "asd";
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
cryptor.initializeStorage(out, pw);
|
||||
cryptor.encryptMasterKey(out, pw);
|
||||
cryptor.swipeSensitiveData();
|
||||
|
||||
final Path wrongMasterKey = tmpDir.resolve("notExistingMasterKey.json");
|
||||
final Aes256Cryptor decryptor = new Aes256Cryptor();
|
||||
final InputStream in = Files.newInputStream(wrongMasterKey, StandardOpenOption.READ);
|
||||
decryptor.unlockStorage(in, pw);
|
||||
decryptor.decryptMasterKey(in, pw);
|
||||
}
|
||||
|
||||
@Test(expected = FileAlreadyExistsException.class)
|
||||
@ -93,11 +93,11 @@ public class Aes256CryptorTest {
|
||||
final String pw = "asd";
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
cryptor.initializeStorage(out, pw);
|
||||
cryptor.encryptMasterKey(out, pw);
|
||||
cryptor.swipeSensitiveData();
|
||||
|
||||
final OutputStream outAgain = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
|
||||
cryptor.initializeStorage(outAgain, pw);
|
||||
cryptor.encryptMasterKey(outAgain, pw);
|
||||
cryptor.swipeSensitiveData();
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright (c) 2014 Sebastian Stenzel
|
||||
This file is licensed under the terms of the MIT license.
|
||||
See the LICENSE.txt file for more info.
|
||||
|
||||
Contributors:
|
||||
Sebastian Stenzel - initial API and implementation
|
||||
-->
|
||||
<!-- Copyright (c) 2014 Sebastian Stenzel This file is licensed under the terms of the MIT license. See the LICENSE.txt file for more info. Contributors: Sebastian Stenzel - initial API and implementation -->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
@ -39,12 +32,16 @@
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- apache commons -->
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
@ -107,7 +104,7 @@
|
||||
<fx:deploy outdir="${project.build.directory}/dist" outfile="${project.build.finalName}" nativeBundles="all">
|
||||
<fx:info title="Cloud Encryptor" vendor="cloudencryptor.org">
|
||||
<!-- todo provide .ico and .icns files for osx/win -->
|
||||
<fx:icon href="shortcut.ico" width="32" height="32" depth="8"/>
|
||||
<fx:icon href="shortcut.ico" width="32" height="32" depth="8" />
|
||||
</fx:info>
|
||||
<fx:platform basedir="" javafx="2.2+" j2se="8.0" />
|
||||
<fx:application refid="fxApp" />
|
||||
|
@ -12,6 +12,7 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.InvalidPathException;
|
||||
@ -20,16 +21,21 @@ import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -39,6 +45,7 @@ import de.sebastianstenzel.oce.crypto.exceptions.UnsupportedKeyLengthException;
|
||||
import de.sebastianstenzel.oce.crypto.exceptions.WrongPasswordException;
|
||||
import de.sebastianstenzel.oce.ui.controls.SecPasswordField;
|
||||
import de.sebastianstenzel.oce.ui.settings.Settings;
|
||||
import de.sebastianstenzel.oce.ui.util.MasterKeyFilter;
|
||||
import de.sebastianstenzel.oce.webdav.WebDAVServer;
|
||||
|
||||
public class AccessController implements Initializable {
|
||||
@ -52,6 +59,8 @@ public class AccessController implements Initializable {
|
||||
@FXML
|
||||
private TextField workDirTextField;
|
||||
@FXML
|
||||
private ComboBox<String> usernameBox;
|
||||
@FXML
|
||||
private SecPasswordField passwordField;
|
||||
@FXML
|
||||
private Button startServerButton;
|
||||
@ -61,10 +70,15 @@ public class AccessController implements Initializable {
|
||||
@Override
|
||||
public void initialize(URL url, ResourceBundle rb) {
|
||||
this.localization = rb;
|
||||
workDirTextField.textProperty().addListener(new WorkDirChangeListener());
|
||||
usernameBox.valueProperty().addListener(new UsernameChangeListener());
|
||||
workDirTextField.setText(Settings.load().getWebdavWorkDir());
|
||||
determineStorageValidity();
|
||||
usernameBox.setValue(Settings.load().getUsername());
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 1: Choose encrypted storage:
|
||||
*/
|
||||
@FXML
|
||||
protected void chooseWorkDir(ActionEvent event) {
|
||||
messageLabel.setText(null);
|
||||
@ -74,33 +88,74 @@ public class AccessController implements Initializable {
|
||||
dirChooser.setInitialDirectory(currentFolder);
|
||||
}
|
||||
final File file = dirChooser.showDialog(rootGridPane.getScene().getWindow());
|
||||
if (file == null) {
|
||||
// dialog canceled
|
||||
return;
|
||||
} else if (file.canWrite()) {
|
||||
workDirTextField.setText(file.getPath());
|
||||
Settings.load().setWebdavWorkDir(file.getPath());
|
||||
Settings.save();
|
||||
} else {
|
||||
messageLabel.setText(localization.getString("access.messageLabel.invalidStorageLocation"));
|
||||
if (file != null) {
|
||||
workDirTextField.setText(file.toString());
|
||||
}
|
||||
determineStorageValidity();
|
||||
}
|
||||
|
||||
private void determineStorageValidity() {
|
||||
boolean storageLocationValid;
|
||||
try {
|
||||
final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText());
|
||||
final Path masterKeyPath = storagePath.resolve(Aes256Cryptor.MASTERKEY_FILENAME);
|
||||
storageLocationValid = Files.exists(masterKeyPath);
|
||||
} catch (InvalidPathException ex) {
|
||||
LOG.trace("Invalid path: " + workDirTextField.getText(), ex);
|
||||
storageLocationValid = false;
|
||||
private final class WorkDirChangeListener implements ChangeListener<String> {
|
||||
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends String> property, String oldValue, String newValue) {
|
||||
if (StringUtils.isEmpty(newValue)) {
|
||||
usernameBox.setDisable(true);
|
||||
usernameBox.setValue(null);
|
||||
return;
|
||||
}
|
||||
boolean storageLocationValid;
|
||||
try {
|
||||
final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText());
|
||||
final DirectoryStream<Path> ds = MasterKeyFilter.filteredDirectory(storagePath);
|
||||
final String masterKeyExt = Aes256Cryptor.MASTERKEY_FILE_EXT.toLowerCase();
|
||||
usernameBox.getItems().clear();
|
||||
for (final Path path : ds) {
|
||||
final String fileName = path.getFileName().toString();
|
||||
final int beginOfExt = fileName.toLowerCase().lastIndexOf(masterKeyExt);
|
||||
final String baseName = fileName.substring(0, beginOfExt);
|
||||
usernameBox.getItems().add(baseName);
|
||||
}
|
||||
storageLocationValid = !usernameBox.getItems().isEmpty();
|
||||
} catch (InvalidPathException | IOException ex) {
|
||||
LOG.trace("Invalid path: " + workDirTextField.getText(), ex);
|
||||
storageLocationValid = false;
|
||||
}
|
||||
// valid encrypted folder?
|
||||
if (storageLocationValid) {
|
||||
Settings.load().setWebdavWorkDir(workDirTextField.getText());
|
||||
Settings.save();
|
||||
} else {
|
||||
messageLabel.setText(localization.getString("access.messageLabel.invalidStorageLocation"));
|
||||
}
|
||||
// enable/disable next controls:
|
||||
usernameBox.setDisable(!storageLocationValid);
|
||||
if (usernameBox.getItems().size() == 1) {
|
||||
usernameBox.setValue(usernameBox.getItems().get(0));
|
||||
}
|
||||
}
|
||||
passwordField.setDisable(!storageLocationValid);
|
||||
startServerButton.setDisable(!storageLocationValid);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 2: Choose username
|
||||
*/
|
||||
private final class UsernameChangeListener implements ChangeListener<String> {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends String> property, String oldValue, String newValue) {
|
||||
if (newValue != null) {
|
||||
Settings.load().setUsername(newValue);
|
||||
Settings.save();
|
||||
}
|
||||
passwordField.setDisable(StringUtils.isEmpty(newValue));
|
||||
startServerButton.setDisable(StringUtils.isEmpty(newValue));
|
||||
Platform.runLater(passwordField::requestFocus);
|
||||
}
|
||||
}
|
||||
|
||||
// step 3: Enter password
|
||||
|
||||
/**
|
||||
* Step 4: Unlock storage
|
||||
*/
|
||||
@FXML
|
||||
protected void startStopServer(ActionEvent event) {
|
||||
messageLabel.setText(null);
|
||||
@ -114,12 +169,13 @@ public class AccessController implements Initializable {
|
||||
|
||||
private boolean unlockStorage() {
|
||||
final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText());
|
||||
final Path masterKeyPath = storagePath.resolve(Aes256Cryptor.MASTERKEY_FILENAME);
|
||||
final String masterKeyFileName = usernameBox.getValue() + Aes256Cryptor.MASTERKEY_FILE_EXT;
|
||||
final Path masterKeyPath = storagePath.resolve(masterKeyFileName);
|
||||
final CharSequence password = passwordField.getCharacters();
|
||||
InputStream masterKeyInputStream = null;
|
||||
try {
|
||||
masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ);
|
||||
cryptor.unlockStorage(masterKeyInputStream, password);
|
||||
cryptor.decryptMasterKey(masterKeyInputStream, password);
|
||||
return true;
|
||||
} catch (NoSuchFileException e) {
|
||||
messageLabel.setText(localization.getString("access.messageLabel.invalidStorageLocation"));
|
||||
|
@ -19,6 +19,7 @@ import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
@ -32,15 +33,19 @@ import javafx.scene.layout.GridPane;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import de.sebastianstenzel.oce.crypto.aes256.Aes256Cryptor;
|
||||
import de.sebastianstenzel.oce.ui.controls.ClearOnDisableListener;
|
||||
import de.sebastianstenzel.oce.ui.controls.SecPasswordField;
|
||||
import de.sebastianstenzel.oce.ui.util.MasterKeyFilter;
|
||||
|
||||
public class InitializeController implements Initializable {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class);
|
||||
private static final Pattern USERNAME_PATTERN = Pattern.compile("[a-z0-9_-]*", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private ResourceBundle localization;
|
||||
@FXML
|
||||
@ -48,6 +53,8 @@ public class InitializeController implements Initializable {
|
||||
@FXML
|
||||
private TextField workDirTextField;
|
||||
@FXML
|
||||
private TextField usernameField;
|
||||
@FXML
|
||||
private SecPasswordField passwordField;
|
||||
@FXML
|
||||
private SecPasswordField retypePasswordField;
|
||||
@ -59,8 +66,13 @@ public class InitializeController implements Initializable {
|
||||
@Override
|
||||
public void initialize(URL url, ResourceBundle rb) {
|
||||
this.localization = rb;
|
||||
workDirTextField.textProperty().addListener(new WorkDirChangeListener());
|
||||
usernameField.textProperty().addListener(new UsernameChangeListener());
|
||||
usernameField.disableProperty().addListener(new ClearOnDisableListener(usernameField));
|
||||
passwordField.textProperty().addListener(new PasswordChangeListener());
|
||||
passwordField.disableProperty().addListener(new ClearOnDisableListener(passwordField));
|
||||
retypePasswordField.textProperty().addListener(new RetypePasswordChangeListener());
|
||||
retypePasswordField.disableProperty().addListener(new ClearOnDisableListener(retypePasswordField));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -75,15 +87,50 @@ public class InitializeController implements Initializable {
|
||||
}
|
||||
final File file = dirChooser.showDialog(rootGridPane.getScene().getWindow());
|
||||
if (file != null && file.canWrite()) {
|
||||
workDirTextField.setText(file.getPath());
|
||||
passwordField.setDisable(false);
|
||||
passwordField.selectAll();
|
||||
passwordField.requestFocus();
|
||||
workDirTextField.setText(file.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private final class WorkDirChangeListener implements ChangeListener<String> {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends String> property, String oldValue, String newValue) {
|
||||
if (StringUtils.isEmpty(newValue)) {
|
||||
usernameField.setDisable(true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final Path dir = FileSystems.getDefault().getPath(newValue);
|
||||
final boolean containsMasterKeys = MasterKeyFilter.filteredDirectory(dir).iterator().hasNext();
|
||||
if (containsMasterKeys) {
|
||||
usernameField.setDisable(true);
|
||||
messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
|
||||
} else {
|
||||
usernameField.setDisable(false);
|
||||
messageLabel.setText(null);
|
||||
}
|
||||
} catch (InvalidPathException | IOException e) {
|
||||
usernameField.setDisable(true);
|
||||
messageLabel.setText(localization.getString("initialize.messageLabel.invalidPath"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 2: Defina a password. On success, step 3 will be enabled.
|
||||
* Step 2: Choose a valid username
|
||||
*/
|
||||
private final class UsernameChangeListener implements ChangeListener<String> {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends String> property, String oldValue, String newValue) {
|
||||
final boolean isValidUsername = USERNAME_PATTERN.matcher(newValue).matches();
|
||||
if (!isValidUsername) {
|
||||
usernameField.setText(oldValue);
|
||||
}
|
||||
passwordField.setDisable(StringUtils.isEmpty(usernameField.getText()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 3: Defina a password. On success, step 3 will be enabled.
|
||||
*/
|
||||
private final class PasswordChangeListener implements ChangeListener<String> {
|
||||
@Override
|
||||
@ -93,30 +140,32 @@ public class InitializeController implements Initializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 3: Retype the password. On success, step 4 will be enabled.
|
||||
* Step 4: Retype the password. On success, step 4 will be enabled.
|
||||
*/
|
||||
private final class RetypePasswordChangeListener implements ChangeListener<String> {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends String> property, String oldValue, String newValue) {
|
||||
boolean passwordsAreEqual = passwordField.getText().equals(newValue);
|
||||
boolean passwordsAreEqual = passwordField.getText().equals(retypePasswordField.getText());
|
||||
initWorkDirButton.setDisable(!passwordsAreEqual);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 4: Generate master password file in working directory. On success, print success message.
|
||||
* Step 5: Generate master password file in working directory. On success, print success message.
|
||||
*/
|
||||
@FXML
|
||||
protected void initWorkDir(ActionEvent event) {
|
||||
final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText());
|
||||
final Path masterKeyPath = storagePath.resolve(Aes256Cryptor.MASTERKEY_FILENAME);
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText());
|
||||
final Path masterKeyPath = storagePath.resolve(usernameField.getText() + Aes256Cryptor.MASTERKEY_FILE_EXT);
|
||||
|
||||
final CharSequence password = passwordField.getCharacters();
|
||||
OutputStream masterKeyOutputStream = null;
|
||||
try {
|
||||
masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
|
||||
cryptor.initializeStorage(masterKeyOutputStream, password);
|
||||
cryptor.encryptMasterKey(masterKeyOutputStream, password);
|
||||
cryptor.swipeSensitiveData();
|
||||
workDirTextField.clear();
|
||||
} catch (FileAlreadyExistsException ex) {
|
||||
messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
|
||||
} catch (InvalidPathException ex) {
|
||||
|
@ -20,13 +20,13 @@ import de.sebastianstenzel.oce.ui.settings.Settings;
|
||||
import de.sebastianstenzel.oce.webdav.WebDAVServer;
|
||||
|
||||
public class MainApplication extends Application {
|
||||
|
||||
|
||||
public static void main(String[] args) {
|
||||
launch(args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(final Stage primaryStage) throws IOException {
|
||||
public void start(final Stage primaryStage) throws IOException {
|
||||
final ResourceBundle localizations = ResourceBundle.getBundle("localization");
|
||||
final Parent root = FXMLLoader.load(getClass().getResource("/main.fxml"), localizations);
|
||||
final Scene scene = new Scene(root);
|
||||
@ -36,7 +36,7 @@ public class MainApplication extends Application {
|
||||
primaryStage.setResizable(false);
|
||||
primaryStage.show();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void stop() throws Exception {
|
||||
WebDAVServer.getInstance().stop();
|
||||
|
@ -0,0 +1,22 @@
|
||||
package de.sebastianstenzel.oce.ui.controls;
|
||||
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.control.TextInputControl;
|
||||
|
||||
public class ClearOnDisableListener implements ChangeListener<Boolean> {
|
||||
|
||||
final TextInputControl control;
|
||||
|
||||
public ClearOnDisableListener(TextInputControl control) {
|
||||
this.control = control;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Boolean> property, Boolean wasDisabled, Boolean isDisabled) {
|
||||
if (isDisabled) {
|
||||
control.clear();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -24,7 +24,7 @@ import org.slf4j.LoggerFactory;
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
@JsonPropertyOrder(value = { "webdavWorkDir" })
|
||||
@JsonPropertyOrder(value = {"webdavWorkDir"})
|
||||
public class Settings implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 7609959894417878744L;
|
||||
@ -33,7 +33,7 @@ public class Settings implements Serializable {
|
||||
private static final String SETTINGS_FILE = "settings.json";
|
||||
private static final ObjectMapper JSON_OM = new ObjectMapper();
|
||||
private static Settings INSTANCE = null;
|
||||
|
||||
|
||||
static {
|
||||
final String home = System.getProperty("user.home", ".");
|
||||
final String appdata = System.getenv("APPDATA");
|
||||
@ -51,16 +51,15 @@ public class Settings implements Serializable {
|
||||
SETTINGS_DIR = fs.getPath(home, ".opencloudencryptor");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private String webdavWorkDir;
|
||||
private String username;
|
||||
private int port;
|
||||
|
||||
|
||||
|
||||
private Settings() {
|
||||
// private constructor
|
||||
}
|
||||
|
||||
|
||||
public static synchronized Settings load() {
|
||||
if (INSTANCE == null) {
|
||||
try {
|
||||
@ -76,7 +75,7 @@ public class Settings implements Serializable {
|
||||
}
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
|
||||
public static synchronized void save() {
|
||||
if (INSTANCE != null) {
|
||||
try {
|
||||
@ -89,13 +88,13 @@ public class Settings implements Serializable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static Settings defaultSettings() {
|
||||
final Settings result = new Settings();
|
||||
result.setWebdavWorkDir(System.getProperty("user.home", "."));
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public String getWebdavWorkDir() {
|
||||
@ -106,6 +105,14 @@ public class Settings implements Serializable {
|
||||
this.webdavWorkDir = webdavWorkDir;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
@ -0,0 +1,26 @@
|
||||
package de.sebastianstenzel.oce.ui.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.DirectoryStream.Filter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import de.sebastianstenzel.oce.crypto.aes256.Aes256Cryptor;
|
||||
|
||||
public class MasterKeyFilter implements Filter<Path> {
|
||||
|
||||
public static MasterKeyFilter FILTER = new MasterKeyFilter();
|
||||
|
||||
private final String masterKeyExt = Aes256Cryptor.MASTERKEY_FILE_EXT.toLowerCase();
|
||||
|
||||
@Override
|
||||
public boolean accept(Path child) throws IOException {
|
||||
return child.getFileName().toString().toLowerCase().endsWith(masterKeyExt);
|
||||
}
|
||||
|
||||
public static final DirectoryStream<Path> filteredDirectory(Path dir) throws IOException {
|
||||
return Files.newDirectoryStream(dir, FILTER);
|
||||
}
|
||||
|
||||
}
|
@ -32,19 +32,23 @@
|
||||
|
||||
<children>
|
||||
<!-- Row 0 -->
|
||||
<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%access.label.workDir" />
|
||||
<TextField fx:id="workDirTextField" GridPane.rowIndex="0" GridPane.columnIndex="1" editable="true" />
|
||||
<Button fx:id="chooseWorkDirButton" GridPane.rowIndex="0" GridPane.columnIndex="2" text="%access.button.chooseWorkDir" onAction="#chooseWorkDir" />
|
||||
<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%access.label.workDir" GridPane.halignment="RIGHT" />
|
||||
<TextField fx:id="workDirTextField" GridPane.rowIndex="0" GridPane.columnIndex="1" />
|
||||
<Button fx:id="chooseWorkDirButton" GridPane.rowIndex="0" GridPane.columnIndex="2" text="%access.button.chooseWorkDir" onAction="#chooseWorkDir" focusTraversable="false" />
|
||||
|
||||
<!-- Row 1 -->
|
||||
<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%access.label.password" />
|
||||
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="1" GridPane.columnIndex="1" disable="true" />
|
||||
<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%access.label.username" GridPane.halignment="RIGHT" />
|
||||
<ComboBox fx:id="usernameBox" GridPane.rowIndex="1" GridPane.columnIndex="1" promptText="$access.label.username" disable="true" />
|
||||
|
||||
<!-- Row 2 -->
|
||||
<Button fx:id="startServerButton" text="%access.button.startServer" GridPane.rowIndex="2" GridPane.columnIndex="1" disable="true" onAction="#startStopServer" />
|
||||
<Label GridPane.rowIndex="2" GridPane.columnIndex="0" text="%access.label.password" GridPane.halignment="RIGHT" />
|
||||
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="2" GridPane.columnIndex="1" disable="true" />
|
||||
|
||||
<!-- Row 3 -->
|
||||
<Label fx:id="messageLabel" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="3" textAlignment="CENTER" />
|
||||
<Button fx:id="startServerButton" text="%access.button.startServer" GridPane.rowIndex="3" GridPane.columnIndex="1" disable="true" defaultButton="true" onAction="#startStopServer" focusTraversable="false" />
|
||||
|
||||
<!-- Row 4 -->
|
||||
<Label fx:id="messageLabel" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="3" textAlignment="CENTER" />
|
||||
</children>
|
||||
</GridPane>
|
||||
|
||||
|
@ -30,7 +30,7 @@
|
||||
|
||||
<children>
|
||||
<!-- Row 0 -->
|
||||
<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%advanced.label.port" />
|
||||
<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%advanced.label.port" GridPane.halignment="RIGHT" />
|
||||
<TextField fx:id="portTextField" GridPane.rowIndex="0" GridPane.columnIndex="1" />
|
||||
</children>
|
||||
</GridPane>
|
||||
|
@ -32,23 +32,27 @@
|
||||
|
||||
<children>
|
||||
<!-- Row 0 -->
|
||||
<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%initialize.label.workDir" />
|
||||
<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%initialize.label.workDir" GridPane.halignment="RIGHT" />
|
||||
<TextField fx:id="workDirTextField" GridPane.rowIndex="0" GridPane.columnIndex="1" editable="true" />
|
||||
<Button fx:id="chooseWorkDirButton" GridPane.rowIndex="0" GridPane.columnIndex="2" text="%initialize.button.chooseWorkDir" onAction="#chooseWorkDir" />
|
||||
<Button fx:id="chooseWorkDirButton" GridPane.rowIndex="0" GridPane.columnIndex="2" text="%initialize.button.chooseWorkDir" onAction="#chooseWorkDir" focusTraversable="false" />
|
||||
|
||||
<!-- Row 1 -->
|
||||
<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%initialize.label.password" />
|
||||
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="1" GridPane.columnIndex="1" disable="true" />
|
||||
<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%initialize.label.username" GridPane.halignment="RIGHT" />
|
||||
<TextField fx:id="usernameField" GridPane.rowIndex="1" GridPane.columnIndex="1" disable="true" />
|
||||
|
||||
<!-- Row 2 -->
|
||||
<Label GridPane.rowIndex="2" GridPane.columnIndex="0" text="%initialize.label.retypePassword" />
|
||||
<SecPasswordField fx:id="retypePasswordField" GridPane.rowIndex="2" GridPane.columnIndex="1" disable="true" />
|
||||
<Label GridPane.rowIndex="2" GridPane.columnIndex="0" text="%initialize.label.password" GridPane.halignment="RIGHT" />
|
||||
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="2" GridPane.columnIndex="1" disable="true" />
|
||||
|
||||
<!-- Row 3 -->
|
||||
<Button fx:id="initWorkDirButton" text="%initialize.button.initWorkDir" GridPane.rowIndex="3" GridPane.columnIndex="1" onAction="#initWorkDir" disable="true"/>
|
||||
<Label GridPane.rowIndex="3" GridPane.columnIndex="0" text="%initialize.label.retypePassword" GridPane.halignment="RIGHT" />
|
||||
<SecPasswordField fx:id="retypePasswordField" GridPane.rowIndex="3" GridPane.columnIndex="1" disable="true" />
|
||||
|
||||
<!-- Row 4 -->
|
||||
<Label fx:id="messageLabel" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="3" textAlignment="CENTER" />
|
||||
<Button fx:id="initWorkDirButton" text="%initialize.button.initWorkDir" GridPane.rowIndex="4" GridPane.columnIndex="1" defaultButton="true" onAction="#initWorkDir" disable="true" focusTraversable="false"/>
|
||||
|
||||
<!-- Row 5 -->
|
||||
<Label fx:id="messageLabel" GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="5" textOverrun="ELLIPSIS" />
|
||||
</children>
|
||||
</GridPane>
|
||||
|
||||
|
@ -14,6 +14,7 @@ toolbarbutton.advanced=Advanced Settings
|
||||
# initialize.fxml
|
||||
initialize.label.workDir=New vault location
|
||||
initialize.button.chooseWorkDir=Choose...
|
||||
initialize.label.username=Username
|
||||
initialize.label.password=Password
|
||||
initialize.label.retypePassword=Retype
|
||||
initialize.button.initWorkDir=Initialize Vault
|
||||
@ -22,6 +23,7 @@ initialize.messageLabel.invalidPath=Invalid vault location.
|
||||
|
||||
# access.fxml
|
||||
access.label.workDir=Vault location
|
||||
access.label.username=Username
|
||||
access.label.password=Password
|
||||
access.button.chooseWorkDir=Choose...
|
||||
access.button.startServer=Start Server
|
||||
|
@ -13,7 +13,8 @@
|
||||
-fx-font-family: "lucida-grande";
|
||||
}
|
||||
|
||||
.button {
|
||||
.button,
|
||||
.combo-box {
|
||||
-fx-text-fill: #000000;
|
||||
-fx-background-color: linear-gradient(to bottom, #FFFFFF, #DDDDDD);
|
||||
-fx-border-color: #888888;
|
||||
@ -25,7 +26,37 @@
|
||||
-fx-font-weight: normal;
|
||||
}
|
||||
|
||||
.text-field {
|
||||
-fx-border-radius: 3.0;
|
||||
-fx-border-width: 0.5;
|
||||
-fx-border-color: #888888;
|
||||
-fx-focus-color: #FF0000;
|
||||
-fx-background-color: transparent;
|
||||
-fx-padding: 5 2 5 2;
|
||||
}
|
||||
|
||||
.text-field:focused {
|
||||
-fx-background-color: #DDDDDD;
|
||||
}
|
||||
|
||||
.button:armed,
|
||||
.button:selected {
|
||||
.button:selected,
|
||||
.combo-box:armed,
|
||||
.combo-box:selected {
|
||||
-fx-background-color: linear-gradient(to bottom, #DDDDDD, #CCCCCC 30%, #EEEEEE);
|
||||
}
|
||||
|
||||
.combo-box .list-cell {
|
||||
-fx-background-color: transparent;
|
||||
-fx-text-fill: -fx-text-base-color;
|
||||
}
|
||||
|
||||
.combo-box .list-cell:hover {
|
||||
-fx-background-color: #DDDDDD;
|
||||
}
|
||||
|
||||
.combo-box-popup .list-view {
|
||||
-fx-padding: 0 0 0 0;
|
||||
-fx-background-insets: 0, 0;
|
||||
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.6), 8, 0.0, 0, 0);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user