diff --git a/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Aes256Cryptor.java b/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Aes256Cryptor.java index 30aa589a7..5db9e5471 100644 --- a/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Aes256Cryptor.java +++ b/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 { diff --git a/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/FileNamingConventions.java b/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/FileNamingConventions.java index 665c81efd..da3fbc1b9 100644 --- a/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/FileNamingConventions.java +++ b/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. diff --git a/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Key.java b/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Key.java new file mode 100644 index 000000000..cb4a570ae --- /dev/null +++ b/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; + } + + +} \ No newline at end of file diff --git a/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Keys.java b/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Keys.java deleted file mode 100644 index ee2b4828e..000000000 --- a/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Keys.java +++ /dev/null @@ -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 additionalKeys; - - public Key getOwnerKey() { - return ownerKey; - } - - public void setOwnerKey(Key ownerKey) { - this.ownerKey = ownerKey; - } - - public Map getAdditionalKeys() { - return additionalKeys; - } - - public void setAdditionalKeys(Map 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; - } - - - } - - -} diff --git a/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/exceptions/UnsupportedKeyLengthException.java b/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/exceptions/UnsupportedKeyLengthException.java index 839a1afa7..53268faa6 100644 --- a/oce-main/oce-crypto-aes/src/main/java/de/sebastianstenzel/oce/crypto/exceptions/UnsupportedKeyLengthException.java +++ b/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; + } + } \ No newline at end of file diff --git a/oce-main/oce-crypto-aes/src/test/java/de/sebastianstenzel/oce/crypto/aes256/Aes256CryptorTest.java b/oce-main/oce-crypto-aes/src/test/java/de/sebastianstenzel/oce/crypto/aes256/Aes256CryptorTest.java index e2e48a81d..d77b47455 100644 --- a/oce-main/oce-crypto-aes/src/test/java/de/sebastianstenzel/oce/crypto/aes256/Aes256CryptorTest.java +++ b/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(); } diff --git a/oce-main/oce-ui/pom.xml b/oce-main/oce-ui/pom.xml index 9264524b6..8aa7bcb57 100644 --- a/oce-main/oce-ui/pom.xml +++ b/oce-main/oce-ui/pom.xml @@ -1,12 +1,5 @@ - + 4.0.0 @@ -39,12 +32,16 @@ com.fasterxml.jackson.core jackson-databind - + commons-io commons-io + + org.apache.commons + commons-lang3 + @@ -107,7 +104,7 @@ - + diff --git a/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/AccessController.java b/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/AccessController.java index 41334d8f9..f4b527ba8 100644 --- a/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/AccessController.java +++ b/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 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 { + + @Override + public void changed(ObservableValue 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 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 { + @Override + public void changed(ObservableValue 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")); diff --git a/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/InitializeController.java b/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/InitializeController.java index 0ae0a9f2c..4b0f2f3a8 100644 --- a/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/InitializeController.java +++ b/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 { + @Override + public void changed(ObservableValue 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 { + @Override + public void changed(ObservableValue 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 { @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 { @Override public void changed(ObservableValue 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) { diff --git a/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/MainApplication.java b/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/MainApplication.java index d941f2a08..487d83a84 100644 --- a/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/MainApplication.java +++ b/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(); diff --git a/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/controls/ClearOnDisableListener.java b/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/controls/ClearOnDisableListener.java new file mode 100644 index 000000000..eb0e18599 --- /dev/null +++ b/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 { + + final TextInputControl control; + + public ClearOnDisableListener(TextInputControl control) { + this.control = control; + } + + @Override + public void changed(ObservableValue property, Boolean wasDisabled, Boolean isDisabled) { + if (isDisabled) { + control.clear(); + } + } + +} \ No newline at end of file diff --git a/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/settings/Settings.java b/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/settings/Settings.java index 932d0bf21..b8e87cb9f 100644 --- a/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/settings/Settings.java +++ b/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; } diff --git a/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/util/MasterKeyFilter.java b/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/util/MasterKeyFilter.java new file mode 100644 index 000000000..9d1e327ca --- /dev/null +++ b/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 { + + 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 filteredDirectory(Path dir) throws IOException { + return Files.newDirectoryStream(dir, FILTER); + } + +} diff --git a/oce-main/oce-ui/src/main/resources/access.fxml b/oce-main/oce-ui/src/main/resources/access.fxml index 5dfa0cc50..c0e299593 100644 --- a/oce-main/oce-ui/src/main/resources/access.fxml +++ b/oce-main/oce-ui/src/main/resources/access.fxml @@ -32,19 +32,23 @@ -