- First public version

This commit is contained in:
Sebastian Stenzel 2014-02-02 01:57:14 +01:00
parent b78ee8295d
commit 8740e43b96
40 changed files with 3802 additions and 0 deletions

6
.gitignore vendored
View File

@ -4,3 +4,9 @@
*.jar
*.war
*.ear
# Eclipse Settings Files #
.settings
.project
.classpath
target/

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2014 Sebastian Stenzel
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Sebastian Stenzel - initial API and implementation
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>de.sebastianstenzel.oce</groupId>
<artifactId>oce-main</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>oce-crypto</artifactId>
<name>Open Cloud Encryptor Cryptographic module</name>
<description>Provides stream ciphers and filename pseudonymization functions.</description>
<dependencies>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
<!-- Commons -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- JUnit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
</project>

View File

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

View File

@ -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 <code>null</code>, 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;
}

View File

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

View File

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

View File

@ -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<String> cleartextUriComps = this.splitUri(cleartextUri);
final List<String> 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<String> pseudonymUriComps = this.splitUri(pseudonymizedUri);
final List<String> 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<String> 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<String> uriComponents) {
final StringBuilder sb = new StringBuilder();
for (final String comp : uriComponents) {
sb.append(URI_PATH_SEP).append(comp);
}
return sb.toString();
}
private List<String> splitUri(final String uri) {
final List<String> 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;
}
}

View File

@ -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<String, Key> additionalKeys;
public Key getOwnerKey() {
return ownerKey;
}
public void setOwnerKey(Key ownerKey) {
this.ownerKey = ownerKey;
}
public Map<String, Key> getAdditionalKeys() {
return additionalKeys;
}
public void setAdditionalKeys(Map<String, Key> additionalKeys) {
this.additionalKeys = additionalKeys;
}
@JsonPropertyOrder(value = { "salt", "iv", "iterations", "keyLength", "masterkey" })
public static class Key implements Serializable {
private static final long serialVersionUID = 8578363158959619885L;
private byte[] salt;
private byte[] iv;
private int iterations;
private int keyLength;
private byte[] pwVerification;
private byte[] masterkey;
public byte[] getSalt() {
return salt;
}
public void setSalt(byte[] salt) {
this.salt = salt;
}
public byte[] getIv() {
return iv;
}
public void setIv(byte[] iv) {
this.iv = iv;
}
public int getIterations() {
return iterations;
}
public void setIterations(int iterations) {
this.iterations = iterations;
}
public int getKeyLength() {
return keyLength;
}
public void setKeyLength(int keyLength) {
this.keyLength = keyLength;
}
public byte[] getPwVerification() {
return pwVerification;
}
public void setPwVerification(byte[] pwVerification) {
this.pwVerification = pwVerification;
}
public byte[] getMasterkey() {
return masterkey;
}
public void setMasterkey(byte[] masterkey) {
this.masterkey = masterkey;
}
}
}

View File

@ -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<String, byte[]> filenames;
private Map<String, Long> 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<String, byte[]> getFilenames() {
if (filenames == null) {
filenames = new DualHashBidiMap<>();
}
return filenames;
}
public void setFilenames(BidiMap<String, byte[]> filesnames) {
this.filenames = filesnames;
}
public Map<String, Long> getFilesizes() {
if (filesizes == null) {
filesizes = new HashMap<>();
}
return filesizes;
}
public void setFilesizes(Map<String, Long> filesizes) {
this.filesizes = filesizes;
}
}

View File

@ -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<String> cleartextPathComponents(final List<String> pseudonymizedPathComponents) {
final List<String> 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<String> pseudonymizedPathComponents(final List<String> cleartextPathComponents) {
final List<String> 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<String> cleartextPathComponents, final List<String> 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<cleartextPathComponents.size(); i++) {
final String cleartextComp = cleartextPathComponents.get(i);
final String pseudonymComp = pseudonymPathComponents.get(i);
node = node.getOrCreateSubnode(cleartextComp, pseudonymComp);
}
}
/**
* Removes a path of cleartext/pseudonym pairs from the cache.
*/
public static void unregisterPath(final List<String> 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<String, Node> subnodesByCleartext;
private final Map<String, Node> 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;
}
}
}
}

View File

@ -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<String, String> filenames = new DualHashBidiMap<>();
public BidiMap<String, String> getFilenames() {
return filenames;
}
}

View File

@ -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<String> cleartextUriComps = this.splitUri(cleartextUri);
final List<String> 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<String> pseudonymUriComps = this.splitUri(pseudonymizedUri);
final List<String> 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<String> 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<String> uriComponents) {
final StringBuilder sb = new StringBuilder();
for (final String comp : uriComponents) {
sb.append(URI_PATH_SEP).append(comp);
}
return sb.toString();
}
private List<String> splitUri(final String uri) {
final List<String> 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;
}
}

View File

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

View File

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

View File

@ -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<String> clear1 = Arrays.asList("foo", "bar", "baz", "info.txt");
final List<String> pseudo1 = Arrays.asList("frog", "bear", "bear", "iguana");
PseudonymRepository.registerPath(clear1, pseudo1);
// get pseudonymized path:
final List<String> result1 = PseudonymRepository.pseudonymizedPathComponents(clear1);
Assert.assertEquals(pseudo1, result1);
// get cleartext path:
final List<String> result2 = PseudonymRepository.cleartextPathComponents(pseudo1);
Assert.assertEquals(clear1, result2);
// register additional path:
final List<String> clear2 = Arrays.asList("foo", "bar", "zab", "info.txt");
final List<String> pseudo2 = Arrays.asList("frog", "bear", "zebra", "iguana");
PseudonymRepository.registerPath(clear2, pseudo2);
// get pseudonymized path:
final List<String> result3 = PseudonymRepository.pseudonymizedPathComponents(clear2);
Assert.assertEquals(pseudo2, result3);
// get cleartext path:
final List<String> result4 = PseudonymRepository.cleartextPathComponents(pseudo2);
Assert.assertEquals(clear2, result4);
}
}

91
oce-main/oce-ui/pom.xml Normal file
View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2014 Sebastian Stenzel
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Sebastian Stenzel - initial API and implementation
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>de.sebastianstenzel.oce</groupId>
<artifactId>oce-main</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>oce-ui</artifactId>
<name>Open Cloud Encryptor GUI</name>
<properties>
<exec.mainClass>de.sebastianstenzel.oce.ui.MainApplication</exec.mainClass>
</properties>
<dependencies>
<dependency>
<groupId>de.sebastianstenzel.oce</groupId>
<artifactId>oce-webdav</artifactId>
<version>${project.parent.version}</version>
</dependency>
<!-- JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- JavaFX 2 -->
<dependency>
<groupId>com.oracle</groupId>
<artifactId>javafx</artifactId>
</dependency>
<!-- <dependency> -->
<!-- <groupId>com.aquafx-project</groupId> -->
<!-- <artifactId>aquafx</artifactId> -->
<!-- <version>0.1</version> -->
<!-- </dependency> -->
</dependencies>
<build>
<plugins>
<!-- allows building using the maven goal "com.zenjava:javafx-maven-plugin:jar" -->
<!-- Java < 8: invoke this before your first build: http://zenjava.com/javafx/maven/fix-classpath.html -->
<plugin>
<groupId>com.zenjava</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>2.0</version>
<configuration>
<mainClass>de.sebastianstenzel.oce.ui.MainWindow</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<JavaFX-Version>${javafx.version}</JavaFX-Version>
<JavaFX-Application-Class>${exec.mainClass}</JavaFX-Application-Class>
<Main-Class>com/javafx/main/Main</Main-Class>
</manifestEntries>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>assemble-all</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

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

View File

@ -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<KeyEvent> {
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<Boolean> {
@Override
public void changed(ObservableValue<? extends Boolean> 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()));
}
}
}
}

View File

@ -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<String> {
@Override
public void changed(ObservableValue<? extends String> 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<String> {
@Override
public void changed(ObservableValue<? extends String> 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();
}
}

View File

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

View File

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

View File

@ -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.
* <br/>
* Imagine the following example with <code>pass</code> being the password, <code>x</code> being the swipe char and <code>'</code> being the offset of the char array:
* <ol>
* <li>Append filling chars to the end of the password: <code>passxxxx'</code></li>
* <li>Delete first 4 chars. Internal implementation will then copy the following chars to the position, where the deletion occured: <code>xxxx'xxxx</code></li>
* <li>Delete first 4 chars again, as we appended 4 chars in step 1: <code>'xxxxxx</code></li>
* </ol>
*/
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);
}
}

View File

@ -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<String> 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<? super String> changeListener) {
helper = ExpressionHelper.addListener(helper, this, changeListener);
}
@Override
public String getValue() {
return get();
}
@Override
public void removeListener(ChangeListener<? super String> 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);
}
}

View File

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

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2014 Sebastian Stenzel
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Sebastian Stenzel - initial API and implementation
-->
<?import java.net.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import de.sebastianstenzel.oce.ui.controls.*?>
<GridPane fx:id="rootGridPane" fx:controller="de.sebastianstenzel.oce.ui.AccessController" xmlns:fx="http://javafx.com/fxml" styleClass="root" gridLinesVisible="false" vgap="5" hgap="5" prefWidth="480">
<stylesheets>
<URL value="@panels.css" />
</stylesheets>
<padding>
<Insets top="10" right="10" bottom="10" left="10" />
</padding>
<columnConstraints>
<ColumnConstraints minWidth="150" maxWidth="150" hgrow="NEVER" />
<ColumnConstraints minWidth="200" hgrow="ALWAYS" />
<ColumnConstraints minWidth="50" maxWidth="120" hgrow="NEVER" />
</columnConstraints>
<children>
<!-- Row 0 -->
<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%access.label.workDir" />
<TextField fx:id="workDirTextField" GridPane.rowIndex="0" GridPane.columnIndex="1" editable="true" />
<Button fx:id="chooseWorkDirButton" GridPane.rowIndex="0" GridPane.columnIndex="2" text="%access.button.chooseWorkDir" onAction="#chooseWorkDir" />
<!-- Row 1 -->
<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%access.label.password" />
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="1" GridPane.columnIndex="1" disable="true" />
<!-- Row 2 -->
<Button fx:id="startServerButton" text="%access.button.startServer" GridPane.rowIndex="2" GridPane.columnIndex="1" disable="true" onAction="#startStopServer" />
<!-- Row 3 -->
<Label fx:id="messageLabel" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="3" textAlignment="CENTER" />
</children>
</GridPane>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2014 Sebastian Stenzel
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Sebastian Stenzel - initial API and implementation
-->
<?import java.net.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<GridPane fx:id="rootGridPane" fx:controller="de.sebastianstenzel.oce.ui.AdvancedController" xmlns:fx="http://javafx.com/fxml" styleClass="root" gridLinesVisible="false" vgap="5" hgap="5" prefWidth="480">
<stylesheets>
<URL value="@panels.css" />
</stylesheets>
<padding>
<Insets top="10" right="10" bottom="10" left="10" />
</padding>
<columnConstraints>
<ColumnConstraints minWidth="150" maxWidth="150" hgrow="NEVER" />
<ColumnConstraints minWidth="250" hgrow="ALWAYS" />
</columnConstraints>
<children>
<!-- Row 0 -->
<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%advanced.label.port" />
<TextField fx:id="portTextField" GridPane.rowIndex="0" GridPane.columnIndex="1" />
</children>
</GridPane>

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2014 Sebastian Stenzel
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Sebastian Stenzel - initial API and implementation
-->
<?import java.net.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import de.sebastianstenzel.oce.ui.controls.*?>
<GridPane fx:id="rootGridPane" fx:controller="de.sebastianstenzel.oce.ui.InitializeController" xmlns:fx="http://javafx.com/fxml" styleClass="root" gridLinesVisible="false" vgap="5" hgap="5" prefWidth="480">
<stylesheets>
<URL value="@panels.css" />
</stylesheets>
<padding>
<Insets top="10" right="10" bottom="10" left="10" />
</padding>
<columnConstraints>
<ColumnConstraints minWidth="150" maxWidth="150" hgrow="NEVER" />
<ColumnConstraints minWidth="200" hgrow="ALWAYS" />
<ColumnConstraints minWidth="50" maxWidth="120" hgrow="NEVER" />
</columnConstraints>
<children>
<!-- Row 0 -->
<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%initialize.label.workDir" />
<TextField fx:id="workDirTextField" GridPane.rowIndex="0" GridPane.columnIndex="1" editable="true" />
<Button fx:id="chooseWorkDirButton" GridPane.rowIndex="0" GridPane.columnIndex="2" text="%initialize.button.chooseWorkDir" onAction="#chooseWorkDir" />
<!-- Row 1 -->
<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%initialize.label.password" />
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="1" GridPane.columnIndex="1" disable="true" />
<!-- Row 2 -->
<Label GridPane.rowIndex="2" GridPane.columnIndex="0" text="%initialize.label.retypePassword" />
<SecPasswordField fx:id="retypePasswordField" GridPane.rowIndex="2" GridPane.columnIndex="1" disable="true" />
<!-- Row 3 -->
<Button fx:id="initWorkDirButton" text="%initialize.button.initWorkDir" GridPane.rowIndex="3" GridPane.columnIndex="1" onAction="#initWorkDir" disable="true"/>
<!-- Row 4 -->
<Label fx:id="messageLabel" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="3" textAlignment="CENTER" />
</children>
</GridPane>

View File

@ -0,0 +1,35 @@
#-------------------------------------------------------------------------------
# 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
#-------------------------------------------------------------------------------
# main.fxml
toolbarbutton.initialize=Initialize Vault
toolbarbutton.access=Access Vault
toolbarbutton.advanced=Advanced Settings
# initialize.fxml
initialize.label.workDir=New vault location
initialize.button.chooseWorkDir=Choose...
initialize.label.password=Password
initialize.label.retypePassword=Retype
initialize.button.initWorkDir=Initialize Vault
initialize.messageLabel.alreadyInitialized=Vault in this location already exists.
initialize.messageLabel.invalidPath=Invalid vault location.
# access.fxml
access.label.workDir=Vault location
access.label.password=Password
access.button.chooseWorkDir=Choose...
access.button.startServer=Start Server
access.button.stopServer=Stop Server
access.messageLabel.wrongPassword=Wrong password.
access.messageLabel.invalidStorageLocation=Vault directory invalid.
access.messageLabel.decryptionFailed=Decryption failed.
access.messageLabel.unsupportedKeyLengthInstallJCE=Decryption failed. Please install Oracle JCE.
# advanced.fxml
advanced.label.port=WebDAV Port

View File

@ -0,0 +1,40 @@
#-------------------------------------------------------------------------------
# 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
#-------------------------------------------------------------------------------
@CHARSET "US-ASCII";
.text {
-fx-font-smoothing-type: lcd;
}
.tool-bar {
-fx-background-color: linear-gradient(to bottom, #888888, #222222);
-fx-padding: 5.0 10.0 5.0 10.0;
-fx-border-color: #888888;
-fx-border-width: 1.0 0.0 1.0 0.0;
-fx-border-insets: 0.0;
-fx-alignment: CENTER;
}
.tool-bar .toggle-button {
-fx-text-fill: #FFFFFF;
-fx-background-color: linear-gradient(to bottom, #888888, #222222);
-fx-border-color: #888888;
-fx-background-insets: 0.0, 1.0;
-fx-background-radius: 4.0, 4.0;
-fx-border-radius: 3.0;
-fx-border-width: 0.5;
-fx-font-family: "lucida-grande";
-fx-font-weight: bold;
}
.tool-bar .toggle-button:armed,
.tool-bar .toggle-button:selected {
-fx-background-color: linear-gradient(to bottom, #444444, #555555 30%, #000000);
-fx-border-color: #FFFFFF;
}

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2014 Sebastian Stenzel
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Sebastian Stenzel - initial API and implementation
-->
<?import java.net.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<VBox fx:id="rootVBox" fx:controller="de.sebastianstenzel.oce.ui.MainController" xmlns:fx="http://javafx.com/fxml">
<stylesheets>
<URL value="@main.css" />
</stylesheets>
<fx:define>
<fx:include fx:id="initializePanel" source="initialize.fxml" />
<fx:include fx:id="accessPanel" source="access.fxml" />
<fx:include fx:id="advancedPanel" source="advanced.fxml" />
</fx:define>
<children>
<ToolBar>
<items>
<fx:define>
<ToggleGroup fx:id="toolbarButtonGroup" />
</fx:define>
<ToggleButton text="%toolbarbutton.initialize" toggleGroup="$toolbarButtonGroup" onAction="#showInitializePane" />
<ToggleButton text="%toolbarbutton.access" toggleGroup="$toolbarButtonGroup" onAction="#showAccessPane" selected="true" />
<ToggleButton text="%toolbarbutton.advanced" toggleGroup="$toolbarButtonGroup" onAction="#showAdvancedPane" />
</items>
</ToolBar>
<fx:reference source="accessPanel"/>
</children>
</VBox>

View File

@ -0,0 +1,39 @@
#-------------------------------------------------------------------------------
# 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
#-------------------------------------------------------------------------------
@CHARSET "US-ASCII";
.root {
-fx-background-color: linear-gradient(to bottom, #FFFFFF, #DDDDDD);
}
.text {
-fx-font-smoothing-type: lcd;
}
.label {
-fx-alignment: CENTER;
-fx-font-family: "lucida-grande";
}
.button {
-fx-text-fill: #000000;
-fx-background-color: linear-gradient(to bottom, #FFFFFF, #DDDDDD);
-fx-border-color: #888888;
-fx-background-insets: 0.0, 1.0;
-fx-background-radius: 4.0, 4.0;
-fx-border-radius: 3.0;
-fx-border-width: 0.5;
-fx-font-family: "lucida-grande";
-fx-font-weight: normal;
}
.button:armed,
.button:selected {
-fx-background-color: linear-gradient(to bottom, #DDDDDD, #CCCCCC 30%, #EEEEEE);
}

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2014 Sebastian Stenzel
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Sebastian Stenzel - initial API and implementation
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>de.sebastianstenzel.oce</groupId>
<artifactId>oce-main</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>oce-webdav</artifactId>
<name>Open Cloud Encryptor WebDAV module</name>
<properties>
<jetty.version>9.1.0.v20131115</jetty.version>
<webdavservlet.version>2.0</webdavservlet.version>
<commons.transaction.version>1.2</commons.transaction.version>
<jta.version>1.1</jta.version>
</properties>
<dependencies>
<dependency>
<groupId>de.sebastianstenzel.oce</groupId>
<artifactId>oce-crypto</artifactId>
<version>${project.parent.version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
<!-- Jetty (Servlet Container) -->
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
<version>${jetty.version}</version>
</dependency>
<!-- WebDAV Servlet -->
<dependency>
<groupId>net.sf.webdav-servlet</groupId>
<artifactId>webdav-servlet</artifactId>
<version>${webdavservlet.version}</version>
</dependency>
<!-- I/O -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
<groupId>net.java.xadisk</groupId>
<artifactId>xadisk</artifactId>
<version>1.2.2</version>
</dependency>
<!-- JEE 6 implementation used by XADisk -->
<dependency>
<groupId>org.apache.openejb</groupId>
<artifactId>javaee-api</artifactId>
<version>6.0-5</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,39 @@
/*******************************************************************************
* 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.webdav;
import java.io.File;
import net.sf.webdav.IWebdavStore;
import net.sf.webdav.WebdavServlet;
public class EnhancedWebDavServlet extends WebdavServlet {
private static final long serialVersionUID = 7198160595132838601L;
private EnhancedWebdavStore<?> enhancedStore;
@Override
protected IWebdavStore constructStore(String clazzName, File root) {
final IWebdavStore store = super.constructStore(clazzName, root);
if (store instanceof EnhancedWebdavStore) {
this.enhancedStore = (EnhancedWebdavStore<?>) store;
}
return store;
}
@Override
public void destroy() {
if (this.enhancedStore != null) {
this.enhancedStore.destroy();
}
super.destroy();
}
}

View File

@ -0,0 +1,120 @@
/*******************************************************************************
* 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.webdav;
import java.io.InputStream;
import java.security.Principal;
import net.sf.webdav.ITransaction;
import net.sf.webdav.IWebdavStore;
import net.sf.webdav.StoredObject;
public abstract class EnhancedWebdavStore <T extends ITransaction> implements IWebdavStore {
private final Class<T> transactionClass;
protected EnhancedWebdavStore(final Class<T> transactionClass) {
this.transactionClass = transactionClass;
}
private T cast(final ITransaction transaction) {
if (transactionClass.isAssignableFrom(transaction.getClass())) {
return transactionClass.cast(transaction);
} else {
throw new IllegalStateException("transaction " + transaction + " is not of type " + transactionClass.getName());
}
}
abstract void destroy();
@Override
public final ITransaction begin(Principal principal) {
return beginTransactionInternal(principal);
}
protected abstract T beginTransactionInternal(Principal principal);
@Override
public final void checkAuthentication(ITransaction transaction) {
checkAuthenticationInternal(cast(transaction));
}
protected abstract void checkAuthenticationInternal(T transaction);
@Override
public void commit(ITransaction transaction) {
commitInternal(cast(transaction));
}
protected abstract void commitInternal(T transaction);
@Override
public void rollback(ITransaction transaction) {
rollbackInternal(cast(transaction));
}
protected abstract void rollbackInternal(T transaction);
@Override
public void createFolder(ITransaction transaction, String folderUri) {
createFolderInternal(cast(transaction), folderUri);
}
protected abstract void createFolderInternal(T transaction, String folderUri);
@Override
public void createResource(ITransaction transaction, String resourceUri) {
createResourceInternal(cast(transaction), resourceUri);
}
protected abstract void createResourceInternal(T transaction, String resourceUri);
@Override
public InputStream getResourceContent(ITransaction transaction, String resourceUri) {
return getResourceContentInternal(cast(transaction), resourceUri);
}
protected abstract InputStream getResourceContentInternal(T transaction, String resourceUri);
@Override
public long setResourceContent(ITransaction transaction, String resourceUri, InputStream content, String contentType, String characterEncoding) {
return setResourceContentInternal(cast(transaction), resourceUri, content, contentType, characterEncoding);
}
protected abstract long setResourceContentInternal(T transaction, String resourceUri, InputStream content, String contentType, String characterEncoding);
@Override
public String[] getChildrenNames(ITransaction transaction, String folderUri) {
return getChildrenNamesInternal(cast(transaction), folderUri);
}
protected abstract String[] getChildrenNamesInternal(T transaction, String folderUri);
@Override
public long getResourceLength(ITransaction transaction, String path) {
return getResourceLengthInternal(cast(transaction), path);
}
protected abstract long getResourceLengthInternal(T transaction, String path);
@Override
public void removeObject(ITransaction transaction, String uri) {
removeObjectInternal(cast(transaction), uri);
}
protected abstract void removeObjectInternal(T transaction, String uri);
@Override
public StoredObject getStoredObject(ITransaction transaction, String uri) {
return getStoredObjectInternal(cast(transaction), uri);
}
protected abstract StoredObject getStoredObjectInternal(T transaction, String uri);
}

View File

@ -0,0 +1,183 @@
/*******************************************************************************
* 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.webdav;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xadisk.additional.XAFileInputStreamWrapper;
import org.xadisk.additional.XAFileOutputStreamWrapper;
import org.xadisk.bridge.proxies.interfaces.Session;
import org.xadisk.filesystem.exceptions.NoTransactionAssociatedException;
import org.xadisk.filesystem.exceptions.XAApplicationException;
import de.sebastianstenzel.oce.crypto.Cryptor;
import de.sebastianstenzel.oce.crypto.TransactionAwareFileAccess;
import de.sebastianstenzel.oce.crypto.aes256.AesCryptor;
final class FsWebdavCryptoAdapter {
private static final Logger LOG = LoggerFactory.getLogger(FsWebdavCryptoAdapter.class);
private final Cryptor cryptor = new AesCryptor();
private final Path workDir;
public FsWebdavCryptoAdapter(final String workingDirectory) {
this.workDir = FileSystems.getDefault().getPath(workingDirectory);
}
/**
* Creates a new folder and initializes its metadata file.
*
* @return The pseudonymized URI of the created folder.
*/
public String initializeNewFolder(final Session session, final String clearUri) throws IOException {
final String pseudonymized = this.pseudonymizedUri(session, clearUri);
final TransactionAwareFileAccess accessor = new FileLoader(session);
final File folder = accessor.resolveUri(pseudonymized).toFile();
try {
if (!session.fileExistsAndIsDirectory(folder)) {
session.createFile(folder, true);
}
} catch (NoTransactionAssociatedException ex) {
throw new IllegalStateException("Session closed.", ex);
} catch (XAApplicationException | InterruptedException ex) {
throw new IOException(ex);
}
return pseudonymized;
}
/**
* @return List of all cleartext child resource names for the directory with
* the given URI.
*/
public String[] uncoveredChildrenNames(final Session session, final String pseudonymizedUri) throws IOException {
try {
final TransactionAwareFileAccess accessor = new FileLoader(session);
final File file = accessor.resolveUri(pseudonymizedUri).toFile();
final List<String> result = new ArrayList<>();
if (file.isDirectory()) {
String[] children = session.listFiles(file);
for (final String child : children) {
final String pseudonym = FilenameUtils.concat(pseudonymizedUri, child);
final String cleartext = cryptor.uncoverPseudonym(pseudonym, accessor);
if (cleartext != null) {
result.add(FilenameUtils.getName(cleartext));
}
}
}
return result.toArray(new String[result.size()]);
} catch (XAApplicationException | InterruptedException e) {
throw new IOException(e);
}
}
/**
* @return The pseudonyimzed URI for the given clear URI.
*/
public String pseudonymizedUri(final Session session, final String clearUri) throws IOException {
final TransactionAwareFileAccess fileLoader = new FileLoader(session);
return cryptor.createPseudonym(clearUri, fileLoader);
}
/**
* Deletes a pseudonym.
*/
public void deletePseudonym(final Session session, final String pseudonymizedUri) throws IOException {
final TransactionAwareFileAccess fileLoader = new FileLoader(session);
cryptor.deletePseudonym(pseudonymizedUri, fileLoader);
}
public InputStream decryptResource(Session session, String pseudonymized) throws IOException {
final TransactionAwareFileAccess accessor = new FileLoader(session);
return cryptor.decryptFile(pseudonymized, accessor);
}
public long encryptResource(Session session, String pseudonymized, InputStream in) throws IOException {
final TransactionAwareFileAccess accessor = new FileLoader(session);
return cryptor.encryptFile(pseudonymized, in, accessor);
}
public long getDecryptedFileLength(Session session, String pseudonymized) throws IOException {
final TransactionAwareFileAccess accessor = new FileLoader(session);
return cryptor.getDecryptedContentLength(pseudonymized, accessor);
}
/**
* Transaction-aware implementation of MetadataLoading.
*/
private class FileLoader implements TransactionAwareFileAccess {
private final Session session;
private FileLoader(final Session session) {
this.session = session;
}
@Override
public InputStream openFileForRead(Path path) throws IOException {
try {
final File file = path.toFile();
if (!session.fileExists(file)) {
session.createFile(file, false);
}
return new XAFileInputStreamWrapper(session.createXAFileInputStream(file));
} catch (XAApplicationException | InterruptedException ex) {
LOG.error("Failed to open resource for reading: " + path.toString(), ex);
throw new IOException("Failed to open resource for reading: " + path.toString(), ex);
}
}
@Override
public OutputStream openFileForWrite(Path path) throws IOException {
try {
final File file = path.toFile();
if (!session.fileExists(file)) {
session.createFile(file, false);
} else {
session.truncateFile(file, 0);
}
return new XAFileOutputStreamWrapper(session.createXAFileOutputStream(file, false));
} catch (NoTransactionAssociatedException ex) {
LOG.error("Session closed.", ex);
throw new IllegalStateException("Session closed.", ex);
} catch (XAApplicationException | InterruptedException ex) {
LOG.error("Failed to open resource for writing: " + path.toString(), ex);
throw new IOException("Failed to open resource for writing: " + path.toString(), ex);
}
}
@Override
public Path resolveUri(String uri) {
return workDir.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;
}
}
}
}

View File

@ -0,0 +1,228 @@
/*******************************************************************************
* 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.webdav;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.Principal;
import java.util.Date;
import net.sf.webdav.StoredObject;
import net.sf.webdav.exceptions.WebdavException;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xadisk.bridge.proxies.interfaces.Session;
import org.xadisk.bridge.proxies.interfaces.XAFileSystem;
import org.xadisk.bridge.proxies.interfaces.XAFileSystemProxy;
import org.xadisk.filesystem.exceptions.NoTransactionAssociatedException;
import org.xadisk.filesystem.exceptions.XAApplicationException;
import org.xadisk.filesystem.standalone.StandaloneFileSystemConfiguration;
public class FsWebdavResourceHandler extends EnhancedWebdavStore<FsWebdavTransaction> {
private static final Logger LOG = LoggerFactory.getLogger(FsWebdavResourceHandler.class);
private static final String XA_SYS_DIR_PREFIX = "oce-webdav";
private static final Path XA_SYS_DIR;
static {
final String tmpDirName = (String) System.getProperties().get("java.io.tmpdir");
final Path tmpDirPath = FileSystems.getDefault().getPath(tmpDirName);
try {
XA_SYS_DIR = Files.createTempDirectory(tmpDirPath, XA_SYS_DIR_PREFIX);
} catch (IOException e) {
throw new IllegalStateException("Can't create tmp directory at " + tmpDirPath.toString());
}
}
private final XAFileSystem xafs;
private final String workingDirectory;
private final FsWebdavCryptoAdapter cryptoAdapter;
public FsWebdavResourceHandler(final File root) {
super(FsWebdavTransaction.class);
this.workingDirectory = FilenameUtils.normalizeNoEndSeparator(root.getAbsolutePath());
final StandaloneFileSystemConfiguration configuration = new StandaloneFileSystemConfiguration(XA_SYS_DIR.toString(), "test");
this.xafs = XAFileSystemProxy.bootNativeXAFileSystem(configuration);
this.cryptoAdapter = new FsWebdavCryptoAdapter(this.workingDirectory);
try {
this.xafs.waitForBootup(1000L);
LOG.info("Started XADisk at " + XA_SYS_DIR.toString());
final Session session = xafs.createSessionForLocalTransaction();
cryptoAdapter.initializeNewFolder(session, "/");
session.commit();
} catch (IOException | XAApplicationException | InterruptedException ex) {
throw new IllegalStateException("Could not initialize I/O components.", ex);
}
}
private File getFileInWorkDir(final String relativeUri) {
final String fullPath = this.workingDirectory.concat(relativeUri);
return new File(FilenameUtils.normalize(fullPath));
}
@Override
public void destroy() {
try {
this.xafs.shutdown();
FileUtils.deleteDirectory(XA_SYS_DIR.toFile());
} catch (IOException e) {
LOG.warn("Failed to shutdown normally", e);
}
}
@Override
public FsWebdavTransaction beginTransactionInternal(Principal principal) {
final Session session = this.xafs.createSessionForLocalTransaction();
LOG.trace("started transaction " + session);
return new FsWebdavTransaction(principal, session);
}
@Override
public void checkAuthenticationInternal(FsWebdavTransaction transaction) {
// TODO Auto-generated method stub
}
@Override
public void commitInternal(FsWebdavTransaction transaction) {
try {
transaction.getSession().commit();
LOG.trace("committed transaction " + transaction.getSession());
} catch (NoTransactionAssociatedException e) {
throw new WebdavException("Error committing transaction " + transaction.getSession(), e);
}
}
@Override
public void rollbackInternal(FsWebdavTransaction transaction) {
try {
transaction.getSession().rollback();
LOG.warn("rolled back transaction " + transaction.getSession());
} catch (NoTransactionAssociatedException e) {
throw new WebdavException("Error rolling back transaction " + transaction.getSession(), e);
}
}
@Override
public void createFolderInternal(FsWebdavTransaction transaction, String folderUri) {
try {
cryptoAdapter.initializeNewFolder(transaction.getSession(), folderUri);
} catch (IOException e) {
throw new WebdavException(e);
}
}
@Override
public void createResourceInternal(FsWebdavTransaction transaction, String resourceUri) {
try {
final String pseudonymized = cryptoAdapter.pseudonymizedUri(transaction.getSession(), resourceUri);
final File file = getFileInWorkDir(pseudonymized);
transaction.getSession().createFile(file, false);
} catch (IOException | XAApplicationException | InterruptedException e) {
throw new WebdavException(e);
}
}
@Override
public InputStream getResourceContentInternal(FsWebdavTransaction transaction, String resourceUri) {
try {
// Note: The requesting entity is in charge of closing the stream.
final String pseudonymized = cryptoAdapter.pseudonymizedUri(transaction.getSession(), resourceUri);
return cryptoAdapter.decryptResource(transaction.getSession(), pseudonymized);
} catch (IOException e) {
throw new WebdavException(e);
}
}
@Override
public long setResourceContentInternal(FsWebdavTransaction transaction, String resourceUri, InputStream in, String contentType, String characterEncoding) {
try {
final String pseudonymized = cryptoAdapter.pseudonymizedUri(transaction.getSession(), resourceUri);
return cryptoAdapter.encryptResource(transaction.getSession(), pseudonymized, in);
} catch (IOException e) {
throw new WebdavException(e);
}
}
@Override
public String[] getChildrenNamesInternal(FsWebdavTransaction transaction, String folderUri) {
try {
final String pseudonymized = cryptoAdapter.pseudonymizedUri(transaction.getSession(), folderUri);
return cryptoAdapter.uncoveredChildrenNames(transaction.getSession(), pseudonymized);
} catch (IOException e) {
throw new WebdavException(e);
}
}
@Override
public long getResourceLengthInternal(FsWebdavTransaction transaction, String uri) {
try {
final String pseudonymized = cryptoAdapter.pseudonymizedUri(transaction.getSession(), uri);
return cryptoAdapter.getDecryptedFileLength(transaction.getSession(), pseudonymized);
} catch (IOException e) {
throw new WebdavException(e);
}
}
@Override
public void removeObjectInternal(FsWebdavTransaction transaction, String uri) {
try {
final String pseudonymized = cryptoAdapter.pseudonymizedUri(transaction.getSession(), uri);
final File file = getFileInWorkDir(pseudonymized);
deleteRecursively(transaction.getSession(), file);
cryptoAdapter.deletePseudonym(transaction.getSession(), pseudonymized);
} catch (IOException | XAApplicationException | InterruptedException e) {
LOG.error("removeObject" + uri + " failed", e);
throw new WebdavException(e);
}
}
private void deleteRecursively(Session session, File file) throws XAApplicationException, InterruptedException {
if (file.isDirectory()) {
final String[] children = session.listFiles(file);
for (final String childName : children) {
final File childFile = new File(file, childName);
deleteRecursively(session, childFile);
}
}
session.deleteFile(file);
}
@Override
public StoredObject getStoredObjectInternal(FsWebdavTransaction transaction, String uri) {
try {
final String pseudonymized = cryptoAdapter.pseudonymizedUri(transaction.getSession(), uri);
final File file = getFileInWorkDir(pseudonymized);
if (transaction.getSession().fileExists(file)) {
final StoredObject so = new StoredObject();
so.setFolder(file.isDirectory());
so.setLastModified(new Date(file.lastModified()));
so.setCreationDate(new Date(file.lastModified()));
if (!file.isDirectory()) {
so.setResourceLength(transaction.getSession().getFileLength(file));
}
return so;
} else {
return null;
}
} catch (IOException | XAApplicationException | InterruptedException e) {
throw new WebdavException(e);
}
}
}

View File

@ -0,0 +1,40 @@
/*******************************************************************************
* 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.webdav;
import java.security.Principal;
import org.xadisk.bridge.proxies.interfaces.Session;
import net.sf.webdav.ITransaction;
public class FsWebdavTransaction implements ITransaction {
private final Principal principal;
private final Session session;
/**
* @param principal WebDAV User
* @param session XADisk Session
*/
FsWebdavTransaction(final Principal principal, final Session session) {
this.principal = principal;
this.session = session;
}
@Override
public Principal getPrincipal() {
return principal;
}
public Session getSession() {
return session;
}
}

View File

@ -0,0 +1,73 @@
/*******************************************************************************
* 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.webdav;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class WebDAVServer {
private static final Logger LOG = LoggerFactory.getLogger(WebDAVServer.class);
private static final WebDAVServer INSTANCE = new WebDAVServer();
private final Server server = new Server();
private WebDAVServer() {
// make constructor private
}
public static WebDAVServer getInstance() {
return INSTANCE;
}
public boolean start(final String workDir, final int port) {
final ServerConnector connector = new ServerConnector(server);
connector.setHost("127.0.0.1");
connector.setPort(port);
server.setConnectors(new Connector[] { connector });
final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath("/");
context.addServlet(getWebDAVServletHolder(workDir), "/*");
server.setHandler(context);
try {
server.start();
} catch (Exception ex) {
LOG.error("Server couldn't be started", ex);
}
return server.isStarted();
}
public boolean isRunning() {
return server.isRunning();
}
public boolean stop() {
try {
server.stop();
} catch (Exception ex) {
LOG.error("Server couldn't be stopped", ex);
}
return server.isStopped();
}
private ServletHolder getWebDAVServletHolder(final String rootpath) {
final ServletHolder result = new ServletHolder("OCE-WebdavServlet", EnhancedWebDavServlet.class);
result.setInitParameter("ResourceHandlerImplementation", FsWebdavResourceHandler.class.getName());
result.setInitParameter("rootpath", rootpath);
return result;
}
}

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright (c) 2014 Sebastian Stenzel
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Sebastian Stenzel - initial API and implementation
-->
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
<appender name="console" class="org.apache.log4j.ConsoleAppender">
<param name="Target" value="System.out"/>
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%16d %-5p [%c{1}:%L] %m%n" />
</layout>
</appender>
<appender name="fileAppender" class="org.apache.log4j.DailyRollingFileAppender">
<param name="File" value="/tmp/webdav.log" />
<param name="Append" value="true" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%16d %-5p [%c{1}:%L] %m%n" />
</layout>
</appender>
<root>
<priority value="INFO" />
<appender-ref ref="console" />
</root>
</log4j:configuration>

131
oce-main/pom.xml Normal file
View File

@ -0,0 +1,131 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2014 Sebastian Stenzel
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Sebastian Stenzel - initial API and implementation
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>de.sebastianstenzel.oce</groupId>
<artifactId>oce-main</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Open Cloud Encryptor</name>
<organization>
<name>sebastianstenzel.de</name>
</organization>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.java.version>1.7</project.java.version>
<!-- dependency versions -->
<log4j.version>1.2.16</log4j.version>
<slf4j.version>1.7.5</slf4j.version>
<junit.version>4.11</junit.version>
<commons-io.version>2.4</commons-io.version>
<commons-collections.version>4.0</commons-collections.version>
<commons-lang.version>3.1</commons-lang.version>
<!-- Will be included in Java 8. Until then we need this dependency -->
<javafx.version>2.2</javafx.version>
<jdk.home>/Library/Java/JavaVirtualMachines/jdk1.7.0_45.jdk/Contents/Home</jdk.home>
<javafx.runtime.lib.jar>${jdk.home}/jre/lib/jfxrt.jar</javafx.runtime.lib.jar>
<javafx.tools.ant.jar>${jdk.home}/lib/ant-javafx.jar</javafx.tools.ant.jar>
</properties>
<developers>
<developer>
<name>Sebastian Stenzel</name>
<email>mail@sebastianstenzel.de</email>
</developer>
</developers>
<dependencyManagement>
<dependencies>
<!-- Logging -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j.version}</version>
</dependency>
<!-- commons -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>${commons-collections.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang.version}</version>
</dependency>
<!-- JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.3.0</version>
</dependency>
<!-- JavaFX -->
<dependency>
<groupId>com.oracle</groupId>
<artifactId>javafx</artifactId>
<version>${javafx.version}</version>
<systemPath>${javafx.runtime.lib.jar}</systemPath>
<scope>system</scope>
</dependency>
<!-- JUnit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
<modules>
<module>oce-webdav</module>
<module>oce-ui</module>
<module>oce-crypto</module>
</modules>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.0</version>
<configuration>
<source>${project.java.version}</source>
<target>${project.java.version}</target>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>