Browse Source

- AES: support for multiple masterkey files
- GUI: one masterkey per user

Sebastian Stenzel 10 years ago
parent
commit
2fcdd4eb01
18 changed files with 408 additions and 227 deletions
  1. 40 32
      oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Aes256Cryptor.java
  2. 3 3
      oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/FileNamingConventions.java
  3. 67 0
      oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Key.java
  4. 0 105
      oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Keys.java
  5. 15 2
      oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/exceptions/UnsupportedKeyLengthException.java
  6. 9 9
      oce-main/oce-crypto-aes/src/test/java/de/sebastianstenzel/oce/crypto/aes256/Aes256CryptorTest.java
  7. 7 10
      oce-main/oce-ui/pom.xml
  8. 80 24
      oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/AccessController.java
  9. 60 11
      oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/InitializeController.java
  10. 3 3
      oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/MainApplication.java
  11. 22 0
      oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/controls/ClearOnDisableListener.java
  12. 17 10
      oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/settings/Settings.java
  13. 26 0
      oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/util/MasterKeyFilter.java
  14. 11 7
      oce-main/oce-ui/src/main/resources/access.fxml
  15. 1 1
      oce-main/oce-ui/src/main/resources/advanced.fxml
  16. 12 8
      oce-main/oce-ui/src/main/resources/initialize.fxml
  17. 2 0
      oce-main/oce-ui/src/main/resources/localization.properties
  18. 33 2
      oce-main/oce-ui/src/main/resources/panels.css

+ 40 - 32
oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Aes256Cryptor.java

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

+ 3 - 3
oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/FileNamingConventions.java

@@ -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.

+ 67 - 0
oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Key.java

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

+ 0 - 105
oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Keys.java

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

+ 15 - 2
oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/exceptions/UnsupportedKeyLengthException.java

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

+ 9 - 9
oce-main/oce-crypto-aes/src/test/java/de/sebastianstenzel/oce/crypto/aes256/Aes256CryptorTest.java

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

+ 7 - 10
oce-main/oce-ui/pom.xml

@@ -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" />

+ 80 - 24
oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/AccessController.java

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

+ 60 - 11
oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/InitializeController.java

@@ -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: 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 2: Defina a password. On success, step 3 will be enabled.
+	 * 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) {

+ 3 - 3
oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/MainApplication.java

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

+ 22 - 0
oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/controls/ClearOnDisableListener.java

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

+ 17 - 10
oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/settings/Settings.java

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

+ 26 - 0
oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/util/MasterKeyFilter.java

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

+ 11 - 7
oce-main/oce-ui/src/main/resources/access.fxml

@@ -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>
 

+ 1 - 1
oce-main/oce-ui/src/main/resources/advanced.fxml

@@ -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>

+ 12 - 8
oce-main/oce-ui/src/main/resources/initialize.fxml

@@ -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>
 

+ 2 - 0
oce-main/oce-ui/src/main/resources/localization.properties

@@ -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

+ 33 - 2
oce-main/oce-ui/src/main/resources/panels.css

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