diff --git a/.gitignore b/.gitignore index 0f182a034..6d21fb45d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,9 @@ *.jar *.war *.ear + +# Eclipse Settings Files # +.settings +.project +.classpath +target/ diff --git a/oce-main/oce-crypto/pom.xml b/oce-main/oce-crypto/pom.xml new file mode 100644 index 000000000..d563a5d80 --- /dev/null +++ b/oce-main/oce-crypto/pom.xml @@ -0,0 +1,54 @@ + + + + 4.0.0 + + de.sebastianstenzel.oce + oce-main + 0.0.1-SNAPSHOT + + oce-crypto + Open Cloud Encryptor Cryptographic module + Provides stream ciphers and filename pseudonymization functions. + + + + + org.slf4j + slf4j-log4j12 + + + + + commons-io + commons-io + + + org.apache.commons + commons-collections4 + + + org.apache.commons + commons-lang3 + + + + + com.fasterxml.jackson.core + jackson-databind + + + + + junit + junit + + + diff --git a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/Cryptor.java b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/Cryptor.java new file mode 100644 index 000000000..ca5da3af7 --- /dev/null +++ b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/Cryptor.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * 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; + +import de.sebastianstenzel.oce.crypto.aes256.AesCryptor; + +public abstract class Cryptor implements FilenamePseudonymizing, StorageCrypting { + + private static final Cryptor DEFAULT_CRYPTOR = new AesCryptor(); + + public static Cryptor getDefaultCryptor() { + return DEFAULT_CRYPTOR; + } + +} diff --git a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/FilenamePseudonymizing.java b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/FilenamePseudonymizing.java new file mode 100644 index 000000000..a72ac951c --- /dev/null +++ b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/FilenamePseudonymizing.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * 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; + +import java.io.IOException; + +public interface FilenamePseudonymizing { + + /** + * Pseudonymizes and caches the given URI. If the doesn't exist yet, the new pseudonyms and its corresponding directory structure is created. + * @return Pseudonymized URI for the provided cleartext URI. + */ + String createPseudonym(String cleartextUri, TransactionAwareFileAccess accessor) throws IOException; + + /** + * Looks up the corresponding cleartext names for a given pseudonymized path. + * @return Cleartext URI for the provided pseudonym URI. Returns null, if the pseudonym can't be resolved. + */ + String uncoverPseudonym(String pseudonymizedUri, TransactionAwareFileAccess accessor) throws IOException; + + /** + * Deletes a pair of cleartext/pseudonym file name from the cache and metadata file. + */ + void deletePseudonym(String pseudonymizedUri, TransactionAwareFileAccess accessor) throws IOException; + +} diff --git a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/StorageCrypting.java b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/StorageCrypting.java new file mode 100644 index 000000000..12feae128 --- /dev/null +++ b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/StorageCrypting.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * 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; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; + +public interface StorageCrypting { + + /** + * Closes the given InputStream, when all content is encrypted. + */ + long encryptFile(String pseudonymizedUri, InputStream content, TransactionAwareFileAccess accessor) throws IOException; + + InputStream decryptFile(String pseudonymizedUri, TransactionAwareFileAccess accessor) throws IOException; + + long getDecryptedContentLength(String pseudonymizedUri, TransactionAwareFileAccess accessor) throws IOException; + + boolean isStorage(Path path); + + void initializeStorage(Path path, CharSequence password) throws AlreadyInitializedException, IOException; + + void unlockStorage(Path path, CharSequence password) throws InvalidStorageLocationException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException; + + void swipeSensitiveData(); + + /* Exceptions */ + + class StorageCryptingException extends Exception { + private static final long serialVersionUID = -6622699014483319376L; + + public StorageCryptingException(String string) { + super(string); + } + + public StorageCryptingException(String string, Throwable t) { + super(string, t); + } + } + + class AlreadyInitializedException extends StorageCryptingException { + private static final long serialVersionUID = -8928660250898037968L; + + public AlreadyInitializedException(Path path) { + super(path.toString() + " already contains a vault."); + } + } + + class InvalidStorageLocationException extends StorageCryptingException { + private static final long serialVersionUID = -967813718181720188L; + + public InvalidStorageLocationException(Path path) { + super("Can't read vault in path " + path.toString()); + } + } + + class WrongPasswordException extends StorageCryptingException { + private static final long serialVersionUID = -602047799678568780L; + + public WrongPasswordException() { + super("Wrong password."); + } + } + + class DecryptFailedException extends StorageCryptingException { + private static final long serialVersionUID = -3855673600374897828L; + + public DecryptFailedException(Throwable t) { + super("Decryption failed.", t); + } + } + + class UnsupportedKeyLengthException extends StorageCryptingException { + private static final long serialVersionUID = 8114147446419390179L; + + public UnsupportedKeyLengthException(int length, int maxLength) { + super(String.format("Key length (%i) exceeds policy maximum (%i).", length, maxLength)); + } + + } + +} + + diff --git a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/TransactionAwareFileAccess.java b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/TransactionAwareFileAccess.java new file mode 100644 index 000000000..e2313bfe5 --- /dev/null +++ b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/TransactionAwareFileAccess.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * 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; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Path; + +/** + * IoC for I/O streams. The streams provied by these methods are closed by the caller. Thus the callee implementing this interface must not + * close the streams again. + */ +public interface TransactionAwareFileAccess { + + /** + * @return Path relative to the current working directory, regardless of leading slashes. + */ + Path resolveUri(String uri); + + InputStream openFileForRead(Path path) throws IOException; + + OutputStream openFileForWrite(Path path) throws IOException; + +} diff --git a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/aes256/AesCryptor.java b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/aes256/AesCryptor.java new file mode 100644 index 000000000..871e3a1dd --- /dev/null +++ b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/aes256/AesCryptor.java @@ -0,0 +1,585 @@ +/******************************************************************************* + * 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.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +import org.apache.commons.io.Charsets; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import de.sebastianstenzel.oce.crypto.Cryptor; +import de.sebastianstenzel.oce.crypto.TransactionAwareFileAccess; +import de.sebastianstenzel.oce.crypto.cache.PseudonymRepository; + +/** + * Default cryptor using PBKDF2 to derive an AES user key of up to 256 bit length. + * This user key is used to decrypt the masterkey, which is a secure random chunk of data. + * The masterkey in turn is used to decrypt all files in the secure storage location. + */ +public class AesCryptor extends Cryptor { + + private static final Logger LOG = LoggerFactory.getLogger(AesCryptor.class); + private static final String METADATA_FILENAME = "metadata.json"; + private static final String KEYS_FILENAME = "keys.json"; + private static final char URI_PATH_SEP = '/'; + + /** + * PRNG for cryptographically secure random numbers. + * Defaults to SHA1-based number generator. + * @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#SecureRandom + */ + private static final SecureRandom SECURE_PRNG; + + /** + * Factory for deriveing keys. + * Defaults to PBKDF2/HMAC-SHA1. + * @see PKCS #5, defined in RFC 2898 + * @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#SecretKeyFactory + */ + private static final SecretKeyFactory PBKDF2_FACTORY; + + /** + * Number of bytes used as seed for the PRNG. + */ + private static final int PRNG_SEED_LENGTH = 16; + + /** + * Number of bytes of the master key. + * Should be significantly higher than the {@link #AES_KEY_LENGTH}, + * as a corrupted masterkey can't be changed without decrypting and re-encrypting all files first. + */ + private static final int MASTER_KEY_LENGTH = 512; + + /** + * Number of bytes used as salt, where needed. + */ + private static final int SALT_LENGTH = 8; + + /** + * Our cryptographic algorithm. + * @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#AlgorithmParameters + */ + private static final String ALGORITHM = "AES"; + + /** + * More detailed specification for {@link #ALGORITHM}. + * @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher + */ + private static final String CIPHER = "AES/CBC/PKCS5Padding"; + + /** + * AES block size is 128 bit or 16 bytes. + */ + private static final int AES_BLOCK_LENGTH = 16; + + /** + * Defined in static initializer. + * Defaults to 256, but falls back to maximum value possible, if JCE isn't installed. + * JCE can be installed from here: http://www.oracle.com/technetwork/java/javase/downloads/. + */ + private static final int AES_KEY_LENGTH; + + /** + * Number of iterations for key derived from user pw. + * High iteration count for better resistance to bruteforcing. + */ + private static final int PBKDF2_PW_ITERATIONS = 1000; + + /** + * Number of iterations for key derived from masterkey. + * Low iteration count for better performance. + * No additional security is added by high values. + */ + private static final int PBKDF2_MASTERKEY_ITERATIONS = 1; + + /** + * Jackson JSON-Mapper. + */ + 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()}. + */ + private final byte[] masterKey = new byte[MASTER_KEY_LENGTH]; + + static { + final String keyFactoryName = "PBKDF2WithHmacSHA1"; + final String prngName = "SHA1PRNG"; + try { + PBKDF2_FACTORY = SecretKeyFactory.getInstance(keyFactoryName); + SECURE_PRNG = SecureRandom.getInstance(prngName); + final int maxKeyLen = Cipher.getMaxAllowedKeyLength(ALGORITHM); + AES_KEY_LENGTH = (maxKeyLen >= 256) ? 256 : maxKeyLen; + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should exist.", e); + } + } + + @Override + public boolean isStorage(Path path) { + try { + final Path keysPath = path.resolve(KEYS_FILENAME); + return Files.isReadable(keysPath); + } catch(SecurityException ex) { + return false; + } + } + + @Override + public void initializeStorage(Path path, CharSequence password) throws AlreadyInitializedException, IOException { + final Path keysPath = path.resolve(KEYS_FILENAME); + if (Files.exists(keysPath)) { + throw new AlreadyInitializedException(path); + } + try { + // generate new masterkey: + randomMasterKey(); + + // derive key: + final byte[] userSalt = randomData(SALT_LENGTH); + final SecretKey userKey = pbkdf2(password, userSalt, PBKDF2_PW_ITERATIONS, AES_KEY_LENGTH); + + // encrypt: + final byte[] iv = randomData(AES_BLOCK_LENGTH); + final Cipher encCipher = this.cipher(userKey, iv, Cipher.ENCRYPT_MODE); + byte[] encryptedUserKey = encCipher.doFinal(userKey.getEncoded()); + 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); + this.saveKeys(keys, keysPath); + } catch (IllegalBlockSizeException | BadPaddingException ex) { + throw new IllegalStateException("Block size hard coded. Padding irrelevant in ENCRYPT_MODE. IV must exist in CBC mode.", ex); + } + } + + @Override + public void unlockStorage(Path path, CharSequence password) throws InvalidStorageLocationException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException { + final Path keysPath = path.resolve("keys.json"); + if (!this.isStorage(path)) { + throw new InvalidStorageLocationException(path); + } + byte[] decrypted = new byte[0]; + try { + // load encrypted masterkey: + final Keys keys = this.loadKeys(keysPath); + final Keys.Key ownerKey = keys.getOwnerKey(); + + // check, whether the key length is supported: + final int maxKeyLen = Cipher.getMaxAllowedKeyLength(ALGORITHM); + if (ownerKey.getKeyLength() > maxKeyLen) { + throw new UnsupportedKeyLengthException(ownerKey.getKeyLength(), maxKeyLen); + } + + // derive key: + final SecretKey userKey = pbkdf2(password, ownerKey.getSalt(), ownerKey.getIterations(), ownerKey.getKeyLength()); + + // check password: + final Cipher encCipher = this.cipher(userKey, ownerKey.getIv(), Cipher.ENCRYPT_MODE); + byte[] encryptedUserKey = encCipher.doFinal(userKey.getEncoded()); + if (!Arrays.equals(ownerKey.getPwVerification(), encryptedUserKey)) { + throw new WrongPasswordException(); + } + + // decrypt: + final Cipher decCipher = this.cipher(userKey, ownerKey.getIv(), Cipher.DECRYPT_MODE); + decrypted = decCipher.doFinal(ownerKey.getMasterkey()); + + // everything ok, move decrypted data to masterkey: + final ByteBuffer masterKeyBuffer = ByteBuffer.wrap(this.masterKey); + masterKeyBuffer.put(decrypted); + } catch (IllegalBlockSizeException | BadPaddingException | BufferOverflowException ex) { + throw new DecryptFailedException(ex); + } catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException("Algorithm should exist.", ex); + } finally { + Arrays.fill(decrypted, (byte) 0); + } + } + + @Override + public long encryptFile(String pseudonymizedUri, InputStream in, TransactionAwareFileAccess accessor) throws IOException { + final Path path = accessor.resolveUri(pseudonymizedUri); + OutputStream out = null; + try { + // unencrypted output stream: + final byte[] salt = this.randomData(SALT_LENGTH); + final byte[] iv = this.randomData(AES_BLOCK_LENGTH); + out = accessor.openFileForWrite(path); + out.write(salt, 0, salt.length); + out.write(iv, 0, iv.length); + + // turn outputstream into an encrypting output stream: + final SecretKey key = this.pbkdf2(masterKey, salt, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH); + final Cipher encCipher = this.cipher(key, iv, Cipher.ENCRYPT_MODE); + out = new CipherOutputStream(out, encCipher); + + // write payload to encrypted out: + final long decryptedFilesize = IOUtils.copyLarge(in, out); + + // save filesize to metadata: + final String folderUri = FilenameUtils.getPath(pseudonymizedUri); + final String pseudonym = FilenameUtils.getName(pseudonymizedUri); + final Metadata metadata = loadOrCreateMetadata(accessor, folderUri); + metadata.getFilesizes().put(pseudonym, decryptedFilesize); + saveMetadata(metadata, accessor, folderUri); + + return decryptedFilesize; + } finally { + in.close(); + if (out != null) { + out.close(); + } + } + } + + @Override + public InputStream decryptFile(String pseudonymizedUri, TransactionAwareFileAccess accessor) throws IOException { + // plain input stream: + final Path path = accessor.resolveUri(pseudonymizedUri); + final InputStream in = accessor.openFileForRead(path); + final byte[] salt = new byte[SALT_LENGTH]; + final byte[] iv = new byte[AES_BLOCK_LENGTH]; + in.read(salt, 0, salt.length); + in.read(iv, 0, iv.length); + + // deecrypting input stream: + final SecretKey key = this.pbkdf2(masterKey, salt, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH); + final Cipher decCipher = this.cipher(key, iv, Cipher.DECRYPT_MODE); + return new CipherInputStream(in, decCipher); + } + + @Override + public long getDecryptedContentLength(String pseudonymizedUri, TransactionAwareFileAccess accessor) throws IOException { + final String folderUri = FilenameUtils.getPath(pseudonymizedUri); + final String pseudonym = FilenameUtils.getName(pseudonymizedUri); + final Metadata metadata = loadOrCreateMetadata(accessor, folderUri); + if (metadata.getFilesizes().containsKey(pseudonym)) { + return metadata.getFilesizes().get(pseudonym); + } else { + return -1; + } + } + + /** + * Overwrites the {@link #masterKey} with zeros. + * As masterKey is a final field, this operation is ensured to work on its actual data. + * Otherwise developers could accidentally just assign a new object to the variable. + */ + @Override + public void swipeSensitiveData() { + Arrays.fill(this.masterKey, (byte) 0); + } + + private Cipher cipher(SecretKey key, byte[] iv, int cipherMode) { + try { + final Cipher cipher = Cipher.getInstance(CIPHER); + cipher.init(cipherMode, key, new IvParameterSpec(iv)); + return cipher; + } catch (InvalidKeyException ex) { + throw new IllegalArgumentException("Invalid key.", ex); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException ex) { + throw new IllegalStateException("Algorithm/Padding should exist and accept an IV.", ex); + } + } + + private byte[] randomData(int length) { + final byte[] result = new byte[length]; + SECURE_PRNG.setSeed(SECURE_PRNG.generateSeed(PRNG_SEED_LENGTH)); + SECURE_PRNG.nextBytes(result); + 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 { + byteToChar(password, pw); + return pbkdf2(CharBuffer.wrap(pw), salt, iterations, keyLength); + } finally { + Arrays.fill(pw, (char) 0); + } + } + + private SecretKey pbkdf2(CharSequence password, byte[] salt, int iterations, int keyLength) { + final int pwLen = password.length(); + final char[] pw = new char[pwLen]; + CharBuffer.wrap(password).get(pw, 0, pwLen); + try { + final KeySpec specs = new PBEKeySpec(pw, salt, iterations, keyLength); + final SecretKey pbkdf2Key = PBKDF2_FACTORY.generateSecret(specs); + final SecretKey aesKey = new SecretKeySpec(pbkdf2Key.getEncoded(), ALGORITHM); + return aesKey; + } catch (InvalidKeySpecException ex) { + throw new IllegalStateException("Specs are hard-coded.", ex); + } finally { + Arrays.fill(pw, (char) 0); + } + } + + private void byteToChar(byte[] source, char[] destination) { + if (source.length != destination.length) { + throw new IllegalArgumentException("char[] needs to be the same length as byte[]"); + } + for (int i = 0; i < source.length; i++) { + destination[i] = (char) (source[i] & 0xFF); + } + } + + private Keys loadKeys(Path keysFile) throws IOException { + InputStream in = null; + try { + in = Files.newInputStream(keysFile, StandardOpenOption.READ); + return objectMapper.readValue(in, Keys.class); + } finally { + if (in != null) { + in.close(); + } + } + } + + private void saveKeys(Keys keys, Path keysFile) throws IOException { + OutputStream out = null; + try { + out = Files.newOutputStream(keysFile, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.SYNC, StandardOpenOption.CREATE); + objectMapper.writeValue(out, keys); + } finally { + if (out != null) { + out.close(); + } + } + } + + /* Pseudonymizing */ + + @Override + public String createPseudonym(String cleartextUri, TransactionAwareFileAccess access) throws IOException { + final List cleartextUriComps = this.splitUri(cleartextUri); + final List pseudonymUriComps = PseudonymRepository.pseudonymizedPathComponents(cleartextUriComps); + + // return immediately if path is already known: + if (pseudonymUriComps.size() == cleartextUriComps.size()) { + return concatUri(pseudonymUriComps); + } + + // append further path components otherwise: + for (int i = pseudonymUriComps.size(); i < cleartextUriComps.size(); i++) { + final String currentFolder = concatUri(pseudonymUriComps); + final String cleartext = cleartextUriComps.get(i); + String pseudonym = readPseudonymFromMetadata(access, currentFolder, cleartext); + if (pseudonym == null) { + pseudonym = UUID.randomUUID().toString(); + this.addToMetadata(access, currentFolder, cleartext, pseudonym); + } + pseudonymUriComps.add(pseudonym); + } + PseudonymRepository.registerPath(cleartextUriComps, pseudonymUriComps); + + return concatUri(pseudonymUriComps); + } + + @Override + public String uncoverPseudonym(String pseudonymizedUri, TransactionAwareFileAccess access) throws IOException { + final List pseudonymUriComps = this.splitUri(pseudonymizedUri); + final List cleartextUriComps = PseudonymRepository.cleartextPathComponents(pseudonymUriComps); + + // return immediately if path is already known: + if (cleartextUriComps.size() == pseudonymUriComps.size()) { + return concatUri(cleartextUriComps); + } + + // append further path components otherwise: + for (int i = cleartextUriComps.size(); i < pseudonymUriComps.size(); i++) { + final String currentFolder = concatUri(pseudonymUriComps.subList(0, i)); + final String pseudonym = pseudonymUriComps.get(i); + try { + final String cleartext = this.readCleartextFromMetadata(access, currentFolder, pseudonym); + if (cleartext == null) { + return null; + } + cleartextUriComps.add(cleartext); + } catch (IOException ex) { + LOG.warn("Unresolvable pseudonym: " + currentFolder + "/" + pseudonym); + return null; + } + } + PseudonymRepository.registerPath(cleartextUriComps, pseudonymUriComps); + + return concatUri(cleartextUriComps); + } + + @Override + public void deletePseudonym(String pseudonymizedUri, TransactionAwareFileAccess access) throws IOException { + // find parent folder: + final int lastPathSeparator = pseudonymizedUri.lastIndexOf(URI_PATH_SEP); + final String parentUri; + if (lastPathSeparator > 0) { + parentUri = pseudonymizedUri.substring(0, lastPathSeparator); + } else { + parentUri = "/"; + } + + // delete from metadata file: + final String pseudonym = pseudonymizedUri.substring(lastPathSeparator + 1); + final Metadata metadata = this.loadOrCreateMetadata(access, parentUri); + metadata.getFilenames().remove(pseudonym); + metadata.getFilesizes().remove(pseudonym); + this.saveMetadata(metadata, access, parentUri); + + // delete from cache: + final List pseudonymUriComps = this.splitUri(pseudonymizedUri); + PseudonymRepository.unregisterPath(pseudonymUriComps); + } + + /* Metadata load & save */ + + private String readPseudonymFromMetadata(TransactionAwareFileAccess access, String parentFolder, String cleartext) throws IOException { + final Metadata metadata = loadOrCreateMetadata(access, parentFolder); + return metadata.getFilenames().getKey(cleartext); + } + + private String readCleartextFromMetadata(TransactionAwareFileAccess access, String parentFolder, String pseudonym) throws IOException { + final Metadata metadata = loadOrCreateMetadata(access, parentFolder); + final byte[] encryptedFilename = metadata.getFilenames().get(pseudonym); + if (encryptedFilename == null) { + return null; + } + try { + // decrypt filename: + final SecretKey key = this.pbkdf2(masterKey, metadata.getSalt(), PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH); + final Cipher decCipher = this.cipher(key, metadata.getIv(), Cipher.DECRYPT_MODE); + byte[] decryptedFilename = decCipher.doFinal(encryptedFilename); + return new String(decryptedFilename, Charsets.UTF_8); + } catch (IllegalBlockSizeException | BadPaddingException ex) { + LOG.error("Can't decrypt filename " + pseudonym + " in folder " + parentFolder, ex); + return null; + } + } + + private void addToMetadata(TransactionAwareFileAccess access, String parentFolder, String cleartext, String pseudonym) throws IOException { + final Metadata metadata = loadOrCreateMetadata(access, parentFolder); + try { + // encrypt filename: + final SecretKey key = this.pbkdf2(masterKey, metadata.getSalt(), PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH); + final Cipher encCipher = this.cipher(key, metadata.getIv(), Cipher.ENCRYPT_MODE); + byte[] encryptedFilename = encCipher.doFinal(cleartext.getBytes(Charsets.UTF_8)); + + // save metadata + metadata.getFilenames().put(pseudonym, encryptedFilename); + saveMetadata(metadata, access, parentFolder); + } catch (IllegalBlockSizeException | BadPaddingException ex) { + LOG.error("Can't encrypt filename " + pseudonym + " (" + cleartext + ") in folder " + parentFolder, ex); + } + } + + private Metadata loadOrCreateMetadata(TransactionAwareFileAccess access, String parentFolder) throws IOException { + InputStream in = null; + try { + final Path path = access.resolveUri(parentFolder).resolve(METADATA_FILENAME); + in = access.openFileForRead(path); + return objectMapper.readValue(in, Metadata.class); + } catch (IOException ex) { + final byte[] salt = randomData(SALT_LENGTH); + final byte[] iv = randomData(AES_BLOCK_LENGTH); + return new Metadata(iv, salt); + } finally { + if (in != null) { + in.close(); + } + } + } + + private void saveMetadata(Metadata metadata, TransactionAwareFileAccess access, String parentFolder) throws IOException { + OutputStream out = null; + try { + final Path path = access.resolveUri(parentFolder).resolve(METADATA_FILENAME); + out = access.openFileForWrite(path); + objectMapper.writeValue(out, metadata); + } finally { + if (out != null) { + out.close(); + } + } + } + + /* utility stuff */ + + private String concatUri(final List uriComponents) { + final StringBuilder sb = new StringBuilder(); + for (final String comp : uriComponents) { + sb.append(URI_PATH_SEP).append(comp); + } + return sb.toString(); + } + + private List splitUri(final String uri) { + final List result = new ArrayList<>(); + int begin = 0; + int end = 0; + do { + end = uri.indexOf(URI_PATH_SEP, begin); + end = (end == -1) ? uri.length() : end; + if (end > begin) { + result.add(uri.substring(begin, end)); + } + begin = end + 1; + } while (end < uri.length()); + return result; + } + +} diff --git a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Keys.java b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Keys.java new file mode 100644 index 000000000..ee2b4828e --- /dev/null +++ b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Keys.java @@ -0,0 +1,105 @@ +/******************************************************************************* + * 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/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Metadata.java b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Metadata.java new file mode 100644 index 000000000..9e69c784f --- /dev/null +++ b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/aes256/Metadata.java @@ -0,0 +1,79 @@ +/******************************************************************************* + * 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 org.apache.commons.collections4.BidiMap; +import org.apache.commons.collections4.bidimap.DualHashBidiMap; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonPropertyOrder(value = { "iv", "salt", "files" }) +class Metadata implements Serializable { + private static final long serialVersionUID = 6214509403824421320L; + private byte[] iv; + private byte[] salt; + @JsonDeserialize(as = DualHashBidiMap.class) + private BidiMap filenames; + private Map filesizes; + + Metadata() { + // used by jackson + } + + Metadata(byte[] iv, byte[] salt) { + this.iv = iv; + this.salt = salt; + } + + /* Getter/Setter */ + + public byte[] getIv() { + return iv; + } + + public void setIv(byte[] iv) { + this.iv = iv; + } + + public byte[] getSalt() { + return salt; + } + + public void setSalt(byte[] salt) { + this.salt = salt; + } + + public BidiMap getFilenames() { + if (filenames == null) { + filenames = new DualHashBidiMap<>(); + } + return filenames; + } + + public void setFilenames(BidiMap filesnames) { + this.filenames = filesnames; + } + + public Map getFilesizes() { + if (filesizes == null) { + filesizes = new HashMap<>(); + } + return filesizes; + } + + public void setFilesizes(Map filesizes) { + this.filesizes = filesizes; + } + +} diff --git a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cache/PseudonymRepository.java b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cache/PseudonymRepository.java new file mode 100644 index 000000000..a1663e0e8 --- /dev/null +++ b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cache/PseudonymRepository.java @@ -0,0 +1,157 @@ +/******************************************************************************* + * 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.cache; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +public final class PseudonymRepository { + + private static final Node ROOT = new Node(null, "/", "/"); + + private PseudonymRepository() { + throw new IllegalStateException(); + } + + /** + * @return The deepest resolvable cleartext path for the requested pseudonymized path. + */ + public static List cleartextPathComponents(final List pseudonymizedPathComponents) { + final List result = new ArrayList<>(pseudonymizedPathComponents.size()); + Node node = ROOT; + for (final String pseudonym : pseudonymizedPathComponents) { + node = node.subnodesByPseudonym.get(pseudonym); + if (node == null) { + return result; + } + result.add(node.cleartext); + } + return result; + } + + /** + * @return The deepest resolvable pseudonymized path for the requested cleartext path. + */ + public static List pseudonymizedPathComponents(final List cleartextPathComponents) { + final List result = new ArrayList<>(cleartextPathComponents.size()); + Node node = ROOT; + for (final String cleartext : cleartextPathComponents) { + Node subnode = node.subnodesByCleartext.get(cleartext); + if (subnode == null) { + return result; + } + node = subnode; + result.add(node.pseudonym); + } + return result; + } + + /** + * Caches a path of cleartext/pseudonym pairs. + */ + public static void registerPath(final List cleartextPathComponents, final List pseudonymPathComponents) { + if (cleartextPathComponents.size() != pseudonymPathComponents.size()) { + throw new IllegalArgumentException("Cannot register pseudonymized path, that isn't matching the length of its cleartext equivalent."); + } + + Node node = ROOT; + for (int i=0; i pseudonymPathComponents) { + Node node = ROOT; + for (final String pseudonymComp : pseudonymPathComponents) { + node = node.subnodesByPseudonym.get(pseudonymComp); + } + if (!ROOT.equals(node)) { + node.detach(); + } + } + + + /** + * Node in a tree of cleartext/pseudonym pairs, that can be traversed root to leaf. The whole tree is threadsafe. + * As each node of the tree has its own synchronization, multithreaded access is balanced. + */ + private static final class Node { + private final Node parent; + private final String cleartext; + private final String pseudonym; + private final Map subnodesByCleartext; + private final Map subnodesByPseudonym; + + Node(Node parent, String cleartext, String pseudonym) { + this.parent = parent; + this.cleartext = cleartext; + this.pseudonym = pseudonym; + this.subnodesByCleartext = new ConcurrentHashMap<>(); + this.subnodesByPseudonym = new ConcurrentHashMap<>(); + } + + /** + * @return New subnode attached to this. + */ + Node getOrCreateSubnode(String cleartext, String pseudonym) { + if (subnodesByCleartext.containsKey(cleartext) && subnodesByPseudonym.containsKey(pseudonym)) { + return subnodesByCleartext.get(cleartext); + } + final Node subnode = new Node(this, cleartext, pseudonym); + this.subnodesByCleartext.put(cleartext, subnode); + this.subnodesByPseudonym.put(pseudonym, subnode); + return subnode; + } + + /** + * Removes a node from its parent node. + */ + void detach() { + // the following two lines don't need to be synchronized, + // as inconsistencies are self-healing over the transactional metadata files. + this.parent.subnodesByCleartext.remove(this.cleartext); + this.parent.subnodesByPseudonym.remove(this.pseudonym); + } + + @Override + public int hashCode() { + final HashCodeBuilder hash = new HashCodeBuilder(); + hash.append(parent); + hash.append(cleartext); + hash.append(pseudonym); + return hash.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Node) { + final Node other = (Node) obj; + final EqualsBuilder eq = new EqualsBuilder(); + eq.append(this.parent, other.parent); + eq.append(this.cleartext, other.cleartext); + eq.append(this.pseudonym, other.pseudonym); + return eq.isEquals(); + } else { + return false; + } + } + + } + +} diff --git a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cleartext/Metadata.java b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cleartext/Metadata.java new file mode 100644 index 000000000..2fbacbf39 --- /dev/null +++ b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cleartext/Metadata.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * 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.cleartext; + +import java.io.Serializable; + +import org.apache.commons.collections4.BidiMap; +import org.apache.commons.collections4.bidimap.DualHashBidiMap; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonPropertyOrder(value = { "filenames" }) +class Metadata implements Serializable { + + private static final long serialVersionUID = -8160643291781073247L; + + @JsonDeserialize(as = DualHashBidiMap.class) + private final BidiMap filenames = new DualHashBidiMap<>(); + + public BidiMap getFilenames() { + return filenames; + } + +} diff --git a/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cleartext/NoCryptor.java b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cleartext/NoCryptor.java new file mode 100644 index 000000000..f7388c1f1 --- /dev/null +++ b/oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cleartext/NoCryptor.java @@ -0,0 +1,246 @@ +/******************************************************************************* + * 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.cleartext; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import de.sebastianstenzel.oce.crypto.Cryptor; +import de.sebastianstenzel.oce.crypto.TransactionAwareFileAccess; +import de.sebastianstenzel.oce.crypto.cache.PseudonymRepository; + +/** + * This Cryptor doesn't encrypting anything. It just pseudonymizes path names. + * @deprecated Used for testing only. Will be removed soon. + */ +@Deprecated +public class NoCryptor extends Cryptor { + + private static final Logger LOG = LoggerFactory.getLogger(NoCryptor.class); + private static String METADATA_FILENAME = "metadata.json"; + + private static final char URI_PATH_SEP = '/'; + private final ObjectMapper objectMapper = new ObjectMapper(); + + /* Crypting */ + + @Override + public boolean isStorage(Path path) { + // NoCryptor doesn't depend on any special folder structure. + return true; + } + + @Override + public void initializeStorage(Path path, CharSequence password) { + // Do nothing + } + + @Override + public void unlockStorage(Path path, CharSequence password) { + // Do nothing + } + + @Override + public long encryptFile(String pseudonymizedUri, InputStream in, TransactionAwareFileAccess accessor) throws IOException { + final Path path = accessor.resolveUri(pseudonymizedUri); + OutputStream out = null; + try { + out = accessor.openFileForWrite(path); + return IOUtils.copyLarge(in, out); + } finally { + in.close(); + if (out != null) { + out.close(); + } + } + } + + @Override + public InputStream decryptFile(String pseudonymizedUri, TransactionAwareFileAccess accessor) throws IOException { + final Path path = accessor.resolveUri(pseudonymizedUri); + return accessor.openFileForRead(path); + } + + @Override + public long getDecryptedContentLength(String pseudonymizedUri, TransactionAwareFileAccess accessor) throws IOException { + final Path path = accessor.resolveUri(pseudonymizedUri); + return Files.size(path); + } + + @Override + public void swipeSensitiveData() { + // Do nothing + } + + /* Pseudonymizing */ + + @Override + public String createPseudonym(String cleartextUri, TransactionAwareFileAccess access) throws IOException { + final List cleartextUriComps = this.splitUri(cleartextUri); + final List pseudonymUriComps = PseudonymRepository.pseudonymizedPathComponents(cleartextUriComps); + + // return immediately if path is already known: + if (pseudonymUriComps.size() == cleartextUriComps.size()) { + return concatUri(pseudonymUriComps); + } + + // append further path components otherwise: + for (int i = pseudonymUriComps.size(); i < cleartextUriComps.size(); i++) { + final String currentFolder = concatUri(pseudonymUriComps); + final String cleartext = cleartextUriComps.get(i); + String pseudonym = readPseudonymFromMetadata(access, currentFolder, cleartext); + if (pseudonym == null) { + pseudonym = UUID.randomUUID().toString(); + this.addToMetadata(access, currentFolder, cleartext, pseudonym); + } + pseudonymUriComps.add(pseudonym); + } + PseudonymRepository.registerPath(cleartextUriComps, pseudonymUriComps); + + return concatUri(pseudonymUriComps); + } + + @Override + public String uncoverPseudonym(String pseudonymizedUri, TransactionAwareFileAccess access) throws IOException { + final List pseudonymUriComps = this.splitUri(pseudonymizedUri); + final List cleartextUriComps = PseudonymRepository.cleartextPathComponents(pseudonymUriComps); + + // return immediately if path is already known: + if (cleartextUriComps.size() == pseudonymUriComps.size()) { + return concatUri(cleartextUriComps); + } + + // append further path components otherwise: + for (int i = cleartextUriComps.size(); i < pseudonymUriComps.size(); i++) { + final String currentFolder = concatUri(pseudonymUriComps.subList(0, i)); + final String pseudonym = pseudonymUriComps.get(i); + try { + final String cleartext = this.readCleartextFromMetadata(access, currentFolder, pseudonym); + if (cleartext == null) { + return null; + } + cleartextUriComps.add(cleartext); + } catch (IOException ex) { + LOG.warn("Unresolvable pseudonym: " + currentFolder + "/" + pseudonym); + return null; + } + } + PseudonymRepository.registerPath(cleartextUriComps, pseudonymUriComps); + + return concatUri(cleartextUriComps); + } + + @Override + public void deletePseudonym(String pseudonymizedUri, TransactionAwareFileAccess access) throws IOException { + // find parent folder: + final int lastPathSeparator = pseudonymizedUri.lastIndexOf(URI_PATH_SEP); + final String parentUri; + if (lastPathSeparator > 0) { + parentUri = pseudonymizedUri.substring(0, lastPathSeparator); + } else { + parentUri = "/"; + } + + // delete from metadata file: + final String pseudonym = pseudonymizedUri.substring(lastPathSeparator + 1); + final Metadata metadata = this.loadOrCreateMetadata(access, parentUri); + metadata.getFilenames().remove(pseudonym); + this.saveMetadata(metadata, access, parentUri); + + // delete from cache: + final List pseudonymUriComps = this.splitUri(pseudonymizedUri); + PseudonymRepository.unregisterPath(pseudonymUriComps); + } + + /* Metadata load & save */ + + private String readPseudonymFromMetadata(TransactionAwareFileAccess access, String parentFolder, String cleartext) throws IOException { + final Metadata metadata = loadOrCreateMetadata(access, parentFolder); + return metadata.getFilenames().getKey(cleartext); + } + + private String readCleartextFromMetadata(TransactionAwareFileAccess access, String parentFolder, String pseudonym) throws IOException { + final Metadata metadata = loadOrCreateMetadata(access, parentFolder); + return metadata.getFilenames().get(pseudonym); + } + + private void addToMetadata(TransactionAwareFileAccess access, String parentFolder, String cleartext, String pseudonym) throws IOException { + final Metadata metadata = loadOrCreateMetadata(access, parentFolder); + if (!pseudonym.equals(metadata.getFilenames().getKey(cleartext))) { + metadata.getFilenames().put(pseudonym, cleartext); + saveMetadata(metadata, access, parentFolder); + } + } + + private Metadata loadOrCreateMetadata(TransactionAwareFileAccess access, String parentFolder) throws IOException { + InputStream in = null; + try { + final Path path = access.resolveUri(parentFolder).resolve(METADATA_FILENAME); + in = access.openFileForRead(path); + return objectMapper.readValue(in, Metadata.class); + } catch (IOException ex) { + return new Metadata(); + } finally { + if (in != null) { + in.close(); + } + } + } + + private void saveMetadata(Metadata metadata, TransactionAwareFileAccess access, String parentFolder) throws IOException { + OutputStream out = null; + try { + final Path path = access.resolveUri(parentFolder).resolve(METADATA_FILENAME); + out = access.openFileForWrite(path); + objectMapper.writeValue(out, metadata); + } finally { + if (out != null) { + out.close(); + } + } + } + + /* utility stuff */ + + private String concatUri(final List uriComponents) { + final StringBuilder sb = new StringBuilder(); + for (final String comp : uriComponents) { + sb.append(URI_PATH_SEP).append(comp); + } + return sb.toString(); + } + + private List splitUri(final String uri) { + final List result = new ArrayList<>(); + int begin = 0; + int end = 0; + do { + end = uri.indexOf(URI_PATH_SEP, begin); + end = (end == -1) ? uri.length() : end; + if (end > begin) { + result.add(uri.substring(begin, end)); + } + begin = end + 1; + } while (end < uri.length()); + return result; + } + +} diff --git a/oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/AesCryptorTest.java b/oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/AesCryptorTest.java new file mode 100644 index 000000000..9ffd1f1f9 --- /dev/null +++ b/oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/AesCryptorTest.java @@ -0,0 +1,92 @@ +/******************************************************************************* + * 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.test; + +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.commons.io.FileUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import de.sebastianstenzel.oce.crypto.StorageCrypting; +import de.sebastianstenzel.oce.crypto.StorageCrypting.AlreadyInitializedException; +import de.sebastianstenzel.oce.crypto.StorageCrypting.DecryptFailedException; +import de.sebastianstenzel.oce.crypto.StorageCrypting.InvalidStorageLocationException; +import de.sebastianstenzel.oce.crypto.StorageCrypting.UnsupportedKeyLengthException; +import de.sebastianstenzel.oce.crypto.StorageCrypting.WrongPasswordException; +import de.sebastianstenzel.oce.crypto.aes256.AesCryptor; + +public class AesCryptorTest { + + private Path workingDir; + + @Before + public void prepareTmpDir() throws IOException { + final String tmpDirName = (String) System.getProperties().get("java.io.tmpdir"); + final Path path = FileSystems.getDefault().getPath(tmpDirName); + workingDir = Files.createTempDirectory(path, "oce-crypto-test"); + } + + @Test + public void testCorrectPassword() throws IOException, AlreadyInitializedException, InvalidStorageLocationException, WrongPasswordException, DecryptFailedException, UnsupportedKeyLengthException { + final String pw = "asd"; + final StorageCrypting encryptor = new AesCryptor(); + encryptor.initializeStorage(workingDir, pw); + encryptor.swipeSensitiveData(); + + final StorageCrypting decryptor = new AesCryptor(); + decryptor.unlockStorage(workingDir, pw); + } + + @Test(expected=WrongPasswordException.class) + public void testWrongPassword() throws IOException, AlreadyInitializedException, InvalidStorageLocationException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException { + final String pw = "asd"; + final StorageCrypting encryptor = new AesCryptor(); + encryptor.initializeStorage(workingDir, pw); + encryptor.swipeSensitiveData(); + + final String wrongPw = "foo"; + final StorageCrypting decryptor = new AesCryptor(); + decryptor.unlockStorage(workingDir, wrongPw); + } + + @Test(expected=InvalidStorageLocationException.class) + public void testWrongLocation() throws IOException, AlreadyInitializedException, InvalidStorageLocationException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException { + final String pw = "asd"; + final StorageCrypting encryptor = new AesCryptor(); + encryptor.initializeStorage(workingDir, pw); + encryptor.swipeSensitiveData(); + + final Path wrongWorkginDir = workingDir.resolve("wrongSubResource"); + final StorageCrypting decryptor = new AesCryptor(); + decryptor.unlockStorage(wrongWorkginDir, pw); + } + + @Test(expected=AlreadyInitializedException.class) + public void testReInitialization() throws IOException, AlreadyInitializedException { + final String pw = "asd"; + final StorageCrypting encryptor1 = new AesCryptor(); + encryptor1.initializeStorage(workingDir, pw); + encryptor1.swipeSensitiveData(); + + final StorageCrypting encryptor2 = new AesCryptor(); + encryptor2.initializeStorage(workingDir, pw); + encryptor2.swipeSensitiveData(); + } + + @After + public void dropTmpDir() throws IOException { + FileUtils.deleteDirectory(workingDir.toFile()); + } + +} diff --git a/oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/FilenamePseudonymizerTest.java b/oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/FilenamePseudonymizerTest.java new file mode 100644 index 000000000..d658ea72b --- /dev/null +++ b/oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/FilenamePseudonymizerTest.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * 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.test; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +import org.apache.commons.io.FileUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import de.sebastianstenzel.oce.crypto.Cryptor; +import de.sebastianstenzel.oce.crypto.FilenamePseudonymizing; +import de.sebastianstenzel.oce.crypto.TransactionAwareFileAccess; + +public class FilenamePseudonymizerTest { + + private final FilenamePseudonymizing pseudonymizer = Cryptor.getDefaultCryptor(); + private Path workingDir; + + @Before + public void prepareTmpDir() throws IOException { + final String tmpDirName = (String) System.getProperties().get("java.io.tmpdir"); + final Path path = FileSystems.getDefault().getPath(tmpDirName); + workingDir = Files.createTempDirectory(path, "oce-crypto-test"); + } + + @Test + public void testCreatePseudonym() throws IOException { + final Accessor accessor = new Accessor(); + final String originalCleartextUri = "/foo/bar/test.txt"; + + final String pseudonym = pseudonymizer.createPseudonym(originalCleartextUri, accessor); + Assert.assertNotNull(pseudonym); + + final String cleartext = pseudonymizer.uncoverPseudonym(pseudonym, accessor); + Assert.assertEquals(originalCleartextUri, cleartext); + } + + @After + public void dropTmpDir() throws IOException { + FileUtils.deleteDirectory(workingDir.toFile()); + } + + private class Accessor implements TransactionAwareFileAccess { + + @Override + public OutputStream openFileForWrite(final Path path) throws IOException { + Files.createDirectories(path.getParent()); + return Files.newOutputStream(path, StandardOpenOption.CREATE, StandardOpenOption.WRITE); + } + + @Override + public InputStream openFileForRead(final Path path) throws IOException { + return Files.newInputStream(path, StandardOpenOption.READ); + } + + @Override + public Path resolveUri(String uri) { + return workingDir.resolve(removeLeadingSlash(uri)); + } + + private String removeLeadingSlash(String path) { + if (path.length() == 0) { + return path; + } else if (path.charAt(0) == '/') { + return path.substring(1); + } else { + return path; + } + } + + } + +} diff --git a/oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/PseudonymRepositoryTest.java b/oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/PseudonymRepositoryTest.java new file mode 100644 index 000000000..70b1df838 --- /dev/null +++ b/oce-main/oce-crypto/src/test/java/de/sebastianstenzel/oce/crypto/test/PseudonymRepositoryTest.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * 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.test; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import de.sebastianstenzel.oce.crypto.cache.PseudonymRepository; + +public class PseudonymRepositoryTest { + + @Test + public void testPseudonymRepos() { + // register first pair: + final List clear1 = Arrays.asList("foo", "bar", "baz", "info.txt"); + final List pseudo1 = Arrays.asList("frog", "bear", "bear", "iguana"); + PseudonymRepository.registerPath(clear1, pseudo1); + + // get pseudonymized path: + final List result1 = PseudonymRepository.pseudonymizedPathComponents(clear1); + Assert.assertEquals(pseudo1, result1); + + // get cleartext path: + final List result2 = PseudonymRepository.cleartextPathComponents(pseudo1); + Assert.assertEquals(clear1, result2); + + // register additional path: + final List clear2 = Arrays.asList("foo", "bar", "zab", "info.txt"); + final List pseudo2 = Arrays.asList("frog", "bear", "zebra", "iguana"); + PseudonymRepository.registerPath(clear2, pseudo2); + + // get pseudonymized path: + final List result3 = PseudonymRepository.pseudonymizedPathComponents(clear2); + Assert.assertEquals(pseudo2, result3); + + // get cleartext path: + final List result4 = PseudonymRepository.cleartextPathComponents(pseudo2); + Assert.assertEquals(clear2, result4); + } + +} diff --git a/oce-main/oce-ui/pom.xml b/oce-main/oce-ui/pom.xml new file mode 100644 index 000000000..ebc6d348f --- /dev/null +++ b/oce-main/oce-ui/pom.xml @@ -0,0 +1,91 @@ + + + + 4.0.0 + + de.sebastianstenzel.oce + oce-main + 0.0.1-SNAPSHOT + + oce-ui + Open Cloud Encryptor GUI + + + de.sebastianstenzel.oce.ui.MainApplication + + + + + de.sebastianstenzel.oce + oce-webdav + ${project.parent.version} + + + + + com.fasterxml.jackson.core + jackson-databind + + + + + com.oracle + javafx + + + + + + + + + + + + + + + com.zenjava + javafx-maven-plugin + 2.0 + + de.sebastianstenzel.oce.ui.MainWindow + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + ${javafx.version} + ${exec.mainClass} + com/javafx/main/Main + + + + jar-with-dependencies + + + + + assemble-all + package + + single + + + + + + + + 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 new file mode 100644 index 000000000..51467f003 --- /dev/null +++ b/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/AccessController.java @@ -0,0 +1,148 @@ +/******************************************************************************* + * 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.ui; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.FileSystems; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.ResourceBundle; + +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; +import javafx.stage.DirectoryChooser; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.sebastianstenzel.oce.crypto.Cryptor; +import de.sebastianstenzel.oce.crypto.StorageCrypting.DecryptFailedException; +import de.sebastianstenzel.oce.crypto.StorageCrypting.InvalidStorageLocationException; +import de.sebastianstenzel.oce.crypto.StorageCrypting.UnsupportedKeyLengthException; +import de.sebastianstenzel.oce.crypto.StorageCrypting.WrongPasswordException; +import de.sebastianstenzel.oce.ui.controls.SecPasswordField; +import de.sebastianstenzel.oce.ui.settings.Settings; +import de.sebastianstenzel.oce.webdav.WebDAVServer; + +public class AccessController implements Initializable { + + private static final Logger LOG = LoggerFactory.getLogger(AccessController.class); + + private ResourceBundle localization; + @FXML private GridPane rootGridPane; + @FXML private TextField workDirTextField; + @FXML private SecPasswordField passwordField; + @FXML private Button startServerButton; + @FXML private Label messageLabel; + + @Override + public void initialize(URL url, ResourceBundle rb) { + this.localization = rb; + workDirTextField.setText(Settings.load().getWebdavWorkDir()); + determineStorageValidity(); + } + + @FXML + protected void chooseWorkDir(ActionEvent event) { + messageLabel.setText(null); + final File currentFolder = new File(workDirTextField.getText()); + final DirectoryChooser dirChooser = new DirectoryChooser(); + if (currentFolder.exists()) { + 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")); + } + determineStorageValidity(); + } + + private void determineStorageValidity() { + boolean storageLocationValid; + try { + final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText()); + storageLocationValid = Cryptor.getDefaultCryptor().isStorage(storagePath); + } catch(InvalidPathException ex) { + LOG.trace("Invalid path: " + workDirTextField.getText(), ex); + storageLocationValid = false; + } + passwordField.setDisable(!storageLocationValid); + startServerButton.setDisable(!storageLocationValid); + } + + @FXML + protected void startStopServer(ActionEvent event) { + messageLabel.setText(null); + if (WebDAVServer.getInstance().isRunning()) { + this.tryStop(); + Cryptor.getDefaultCryptor().swipeSensitiveData(); + } else if (this.unlockStorage()) { + this.tryStart(); + } + } + + private boolean unlockStorage() { + final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText()); + final CharSequence password = passwordField.getCharacters(); + try { + Cryptor.getDefaultCryptor().unlockStorage(storagePath, password); + return true; + } catch (InvalidStorageLocationException e) { + messageLabel.setText(localization.getString("access.messageLabel.invalidStorageLocation")); + LOG.warn("Invalid path: " + storagePath.toString()); + } catch (DecryptFailedException ex) { + messageLabel.setText(localization.getString("access.messageLabel.decryptionFailed")); + LOG.error("Decryption failed for technical reasons.", ex); + } catch (WrongPasswordException e) { + messageLabel.setText(localization.getString("access.messageLabel.wrongPassword")); + } catch (UnsupportedKeyLengthException ex) { + messageLabel.setText(localization.getString("access.messageLabel.unsupportedKeyLengthInstallJCE")); + LOG.error("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex); + } catch (IOException ex) { + LOG.error("I/O Exception", ex); + } finally { + passwordField.swipe(); + } + return false; + } + + private void tryStart() { + try { + final Settings settings = Settings.load(); + if (WebDAVServer.getInstance().start(settings.getWebdavWorkDir(), settings.getPort())) { + startServerButton.setText(localization.getString("access.button.stopServer")); + passwordField.setDisable(true); + } + } catch (NumberFormatException ex) { + LOG.error("Invalid port", ex); + } + } + + private void tryStop() { + if (WebDAVServer.getInstance().stop()) { + startServerButton.setText(localization.getString("access.button.startServer")); + passwordField.setDisable(false); + } + } + +} diff --git a/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/AdvancedController.java b/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/AdvancedController.java new file mode 100644 index 000000000..a169e8f36 --- /dev/null +++ b/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/AdvancedController.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * 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.ui; + +import java.net.URL; +import java.util.ResourceBundle; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.event.EventHandler; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.TextField; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.GridPane; +import de.sebastianstenzel.oce.ui.settings.Settings; + +public class AdvancedController implements Initializable { + + private static final Logger LOG = LoggerFactory.getLogger(AdvancedController.class); + + @FXML + private GridPane rootGridPane; + + @FXML + private TextField portTextField; + + @Override + public void initialize(URL url, ResourceBundle rb) { + portTextField.setText(String.valueOf(Settings.load().getPort())); + portTextField.addEventFilter(KeyEvent.KEY_TYPED, new NumericKeyTypeEventFilter()); + portTextField.focusedProperty().addListener(new PortTextFieldFocusListener()); + } + + /** + * Consumes key events, if typed key is not 0-9. + */ + private static final class NumericKeyTypeEventFilter implements EventHandler { + public void handle(KeyEvent t) { + if (t.getCharacter() == null || t.getCharacter().length() == 0) { + return; + } + char c = t.getCharacter().charAt(0); + if (!(c >= '0' && c <= '9')) { + t.consume(); + } + } + } + + /** + * Saves port settings, when textfield loses focus. + */ + private class PortTextFieldFocusListener implements ChangeListener { + @Override + public void changed(ObservableValue property, Boolean wasFocused, Boolean isFocused) { + final Settings settings = Settings.load(); + try { + int port = Integer.valueOf(portTextField.getText()); + settings.setPort(port); + } catch (NumberFormatException ex) { + LOG.warn("Invalid port " + portTextField.getText()); + portTextField.setText(String.valueOf(settings.getPort())); + } + } + } + +} 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 new file mode 100644 index 000000000..5ec0af236 --- /dev/null +++ b/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/InitializeController.java @@ -0,0 +1,124 @@ +/******************************************************************************* + * 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.ui; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.FileSystems; +import java.nio.file.InvalidPathException; +import java.util.ResourceBundle; + +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.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; +import javafx.stage.DirectoryChooser; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.sebastianstenzel.oce.crypto.Cryptor; +import de.sebastianstenzel.oce.crypto.StorageCrypting.AlreadyInitializedException; +import de.sebastianstenzel.oce.ui.controls.SecPasswordField; + +public class InitializeController implements Initializable { + + private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class); + + private ResourceBundle localization; + @FXML private GridPane rootGridPane; + @FXML private TextField workDirTextField; + @FXML private SecPasswordField passwordField; + @FXML private SecPasswordField retypePasswordField; + @FXML private Button initWorkDirButton; + @FXML private Label messageLabel; + + @Override + public void initialize(URL url, ResourceBundle rb) { + this.localization = rb; + passwordField.textProperty().addListener(new PasswordChangeListener()); + retypePasswordField.textProperty().addListener(new RetypePasswordChangeListener()); + } + + /** + * Step 1: Choose a directory, that shall be encrypted. + * On success, step 2 will be enabled. + */ + @FXML + protected void chooseWorkDir(ActionEvent event) { + final File currentFolder = new File(workDirTextField.getText()); + final DirectoryChooser dirChooser = new DirectoryChooser(); + if (currentFolder.exists()) { + dirChooser.setInitialDirectory(currentFolder); + } + final File file = dirChooser.showDialog(rootGridPane.getScene().getWindow()); + if (file != null && file.canWrite()) { + workDirTextField.setText(file.getPath()); + passwordField.setDisable(false); + passwordField.selectAll(); + passwordField.requestFocus(); + } + } + + /** + * Step 2: Defina a password. + * On success, step 3 will be enabled. + */ + private final class PasswordChangeListener implements ChangeListener { + @Override + public void changed(ObservableValue property, String oldValue, String newValue) { + retypePasswordField.setDisable(newValue.isEmpty()); + } + } + + /** + * Step 3: 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); + initWorkDirButton.setDisable(!passwordsAreEqual); + } + } + + /** + * Step 4: Generate master password file in working directory. + * On success, print success message. + */ + @FXML + protected void initWorkDir(ActionEvent event) { + try { + Cryptor.getDefaultCryptor().initializeStorage(FileSystems.getDefault().getPath(workDirTextField.getText()), passwordField.getText()); + Cryptor.getDefaultCryptor().swipeSensitiveData(); + } catch (AlreadyInitializedException ex) { + messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized")); + } catch(InvalidPathException ex) { + messageLabel.setText(localization.getString("initialize.messageLabel.invalidPath")); + } catch (IOException ex) { + LOG.error("I/O Exception", ex); + } finally { + swipePasswordFields(); + } + } + + private void swipePasswordFields() { + passwordField.swipe(); + retypePasswordField.swipe(); + } + + +} 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 new file mode 100644 index 000000000..d941f2a08 --- /dev/null +++ b/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/MainApplication.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * 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.ui; + +import java.io.IOException; +import java.util.ResourceBundle; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; +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 { + final ResourceBundle localizations = ResourceBundle.getBundle("localization"); + final Parent root = FXMLLoader.load(getClass().getResource("/main.fxml"), localizations); + final Scene scene = new Scene(root); + primaryStage.setTitle("Open Cloud Encryptor"); + primaryStage.setScene(scene); + primaryStage.sizeToScene(); + primaryStage.setResizable(false); + primaryStage.show(); + } + + @Override + public void stop() throws Exception { + WebDAVServer.getInstance().stop(); + Settings.save(); + super.stop(); + } + +} diff --git a/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/MainController.java b/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/MainController.java new file mode 100644 index 000000000..a482a347b --- /dev/null +++ b/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/MainController.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * 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.ui; + +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.layout.Pane; +import javafx.scene.layout.VBox; + +public class MainController { + + @FXML + private VBox rootVBox; + + @FXML + private Pane initializePanel; + + @FXML + private Pane accessPanel; + + @FXML + private Pane advancedPanel; + + @FXML + protected void showInitializePane(ActionEvent event) { + showPanel(initializePanel); + } + + @FXML + protected void showAccessPane(ActionEvent event) { + showPanel(accessPanel); + } + + @FXML + protected void showAdvancedPane(ActionEvent event) { + showPanel(advancedPanel); + } + + private void showPanel(Pane panel) { + rootVBox.getChildren().remove(1); + rootVBox.getChildren().add(panel); + rootVBox.getScene().getWindow().sizeToScene(); + } + +} diff --git a/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/controls/SecPasswordField.java b/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/controls/SecPasswordField.java new file mode 100644 index 000000000..1bc6808fa --- /dev/null +++ b/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/controls/SecPasswordField.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * 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.ui.controls; + +import java.util.Arrays; + +import javafx.scene.control.PasswordField; + +/** + * Compromise in security. While the text can be swiped, any access to the {@link #getText()} method will create a copy of the String in the heap. + */ +public class SecPasswordField extends PasswordField { + + private static final char SWIPE_CHAR = ' '; + + /** + * {@link #getContent()} uses a StringBuilder, which in turn is backed by a char[]. + * The delete operation of AbstractStringBuilder closes the gap, that forms by deleting chars, by moving up the following chars. + *
+ * Imagine the following example with pass being the password, x being the swipe char and ' being the offset of the char array: + *
    + *
  1. Append filling chars to the end of the password: passxxxx'
  2. + *
  3. Delete first 4 chars. Internal implementation will then copy the following chars to the position, where the deletion occured: xxxx'xxxx
  4. + *
  5. Delete first 4 chars again, as we appended 4 chars in step 1: 'xxxxxx
  6. + *
+ */ + public void swipe() { + final int pwLength = this.getContent().length(); + final char[] fillingChars = new char[pwLength]; + Arrays.fill(fillingChars, SWIPE_CHAR); + this.getContent().insert(pwLength, new String(fillingChars), false); + this.getContent().delete(0, pwLength, true); + this.getContent().delete(0, pwLength, true); + } + + + +} diff --git a/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/controls/SecurePasswordField.java b/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/controls/SecurePasswordField.java new file mode 100644 index 000000000..4dcbd6a69 --- /dev/null +++ b/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/controls/SecurePasswordField.java @@ -0,0 +1,213 @@ +/******************************************************************************* + * 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.ui.controls; + +import java.nio.CharBuffer; +import java.util.Arrays; + +import javafx.beans.InvalidationListener; +import javafx.beans.value.ChangeListener; +import javafx.scene.control.TextInputControl; + +import com.sun.javafx.binding.ExpressionHelper; + +/** + * Don't use, won't work. + * Just an experiment. Will be moved to a separate branch, when I have some time for cleanup stuff. + */ +@Deprecated +public class SecurePasswordField extends TextInputControl { + + public SecurePasswordField() { + this(""); + } + + public SecurePasswordField(String text) { + super(new SecureContent()); + getStyleClass().add("password-field"); + this.setText(text); + } + + public void swipe() { + final Content content = this.getContent(); + if (content instanceof SecureContent) { + final SecureContent secureContent = (SecureContent) content; + secureContent.swipe(); + } + } + + @Override + public void cut() { + // No-op + } + + @Override + public void copy() { + // No-op + } + + /** + * Content based on a CharBuffer, whose backing char[] can be swiped on demand. + */ + private static final class SecureContent implements Content { + private static final int INITIAL_BUFFER_LENGTH = 64; + private static final int BUFFER_GROWTH_FACTOR = 2; + + private ExpressionHelper helper = null; + private CharBuffer buffer = CharBuffer.allocate(INITIAL_BUFFER_LENGTH); + + public void swipe() { + assert (buffer.hasArray()); + Arrays.fill(buffer.array(), (char) 0); + buffer.position(0); + } + + @Override + public String get() { + return buffer.toString(); + } + + @Override + public void addListener(ChangeListener changeListener) { + helper = ExpressionHelper.addListener(helper, this, changeListener); + + } + + @Override + public String getValue() { + return get(); + } + + @Override + public void removeListener(ChangeListener changeListener) { + helper = ExpressionHelper.removeListener(helper, changeListener); + + } + + @Override + public void addListener(InvalidationListener listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + + } + + @Override + public void removeListener(InvalidationListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + + } + + @Override + public void delete(int start, int end, boolean notifyListeners) { + final int delLen = end - start; + final int pos = buffer.position(); + if (delLen <= 0 || end > pos) { + return; + } + final char[] followingChars = new char[pos - end]; + try { + // save follow-up chars: + buffer.get(followingChars, end, buffer.position() - end); + // close gap: + buffer.put(followingChars, start, followingChars.length); + // zeroing out freed space at end of buffer + final char[] zeros = new char[delLen]; + buffer.put(zeros, pos - delLen, delLen); + // adjust length: + buffer.position(pos - delLen); + if (notifyListeners) { + ExpressionHelper.fireValueChangedEvent(helper); + } + } finally { + // swipe tmp variable + Arrays.fill(followingChars, (char) 0); + } + } + + @Override + public String get(int start, int end) { + final char[] tmp = new char[end - start]; + try { + buffer.get(tmp, start, end - start); + return new String(tmp); + } finally { + Arrays.fill(tmp, (char) 0); + } + } + + @Override + public void insert(int index, String text, boolean notifyListeners) { + if (text.isEmpty()) { + return; + } + final String filteredInput; + if (SecurePasswordField.containsIllegalChars(text)) { + filteredInput = SecurePasswordField.removeIllegalChars(text); + } else { + filteredInput = text; + } + while (filteredInput.length() > buffer.remaining()) { + extendBuffer(); + } + final int pos = buffer.position(); + final char[] followingChars = new char[pos - index]; + try { + // create empty gap for new text: + buffer.get(followingChars, index, followingChars.length); + // insert text at index: + buffer.put(filteredInput, index, filteredInput.length() - index); + // insert chars previously at this position afterwards + final int posAfterNewText = index + filteredInput.length(); + buffer.put(followingChars, posAfterNewText, followingChars.length - posAfterNewText); + // adjust length: + buffer.position(pos + filteredInput.length()); + if (notifyListeners) { + ExpressionHelper.fireValueChangedEvent(helper); + } + } finally { + // swipe tmp variable + Arrays.fill(followingChars, (char) 0); + } + } + + private void extendBuffer() { + int currentCapacity = buffer.capacity(); + buffer = CharBuffer.allocate(currentCapacity * BUFFER_GROWTH_FACTOR); + } + + @Override + public int length() { + return buffer.length(); + } + + } + + static boolean containsIllegalChars(String string) { + for (char c : string.toCharArray()) { + if (SecurePasswordField.isIllegalChar(c)) { + return true; + } + } + return false; + } + + static String removeIllegalChars(String string) { + final StringBuilder sb = new StringBuilder(string.length()); + for (char c : string.toCharArray()) { + if (!SecurePasswordField.isIllegalChar(c)) { + sb.append(c); + } + } + return sb.toString(); + } + + static boolean isIllegalChar(char c) { + return (c == 0x7F || c == 0x0A || c == 0x09 || c < 0x20); + } + +} 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 new file mode 100644 index 000000000..932d0bf21 --- /dev/null +++ b/oce-main/oce-ui/src/main/java/de/sebastianstenzel/oce/ui/settings/Settings.java @@ -0,0 +1,117 @@ +/******************************************************************************* + * 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.ui.settings; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.ObjectMapper; + +@JsonPropertyOrder(value = { "webdavWorkDir" }) +public class Settings implements Serializable { + + private static final long serialVersionUID = 7609959894417878744L; + private static final Logger LOG = LoggerFactory.getLogger(Settings.class); + private static final Path SETTINGS_DIR; + 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"); + final String os = System.getProperty("os.name").toLowerCase(); + final FileSystem fs = FileSystems.getDefault(); + + if (os.contains("win") && appdata != null) { + SETTINGS_DIR = fs.getPath(appdata, "opencloudencryptor"); + } else if (os.contains("win") && appdata == null) { + SETTINGS_DIR = fs.getPath(home, ".opencloudencryptor"); + } else if (os.contains("mac")) { + SETTINGS_DIR = fs.getPath(home, "Library/Application Support/opencloudencryptor"); + } else { + // (os.contains("solaris") || os.contains("sunos") || os.contains("linux") || os.contains("unix")) + SETTINGS_DIR = fs.getPath(home, ".opencloudencryptor"); + } + } + + + private String webdavWorkDir; + private int port; + + + private Settings() { + // private constructor + } + + public static synchronized Settings load() { + if (INSTANCE == null) { + try { + Files.createDirectories(SETTINGS_DIR); + final Path settingsFile = SETTINGS_DIR.resolve(SETTINGS_FILE); + final InputStream in = Files.newInputStream(settingsFile, StandardOpenOption.READ); + INSTANCE = JSON_OM.readValue(in, Settings.class); + return INSTANCE; + } catch (IOException e) { + LOG.warn("Failed to load settings, creating new one."); + INSTANCE = Settings.defaultSettings(); + } + } + return INSTANCE; + } + + public static synchronized void save() { + if (INSTANCE != null) { + try { + Files.createDirectories(SETTINGS_DIR); + final Path settingsFile = SETTINGS_DIR.resolve(SETTINGS_FILE); + final OutputStream out = Files.newOutputStream(settingsFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE); + JSON_OM.writeValue(out, INSTANCE); + } catch (IOException e) { + LOG.error("Failed to save settings.", e); + } + } + } + + private static Settings defaultSettings() { + final Settings result = new Settings(); + result.setWebdavWorkDir(System.getProperty("user.home", ".")); + return result; + } + + /* Getter/Setter */ + + public String getWebdavWorkDir() { + return webdavWorkDir; + } + + public void setWebdavWorkDir(String webdavWorkDir) { + this.webdavWorkDir = webdavWorkDir; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + +} diff --git a/oce-main/oce-ui/src/main/resources/access.fxml b/oce-main/oce-ui/src/main/resources/access.fxml new file mode 100644 index 000000000..5dfa0cc50 --- /dev/null +++ b/oce-main/oce-ui/src/main/resources/access.fxml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +