mirror of
https://github.com/cryptomator/cryptomator.git
synced 2024-11-23 03:59:51 +00:00
- First public version
This commit is contained in:
parent
b78ee8295d
commit
8740e43b96
6
.gitignore
vendored
6
.gitignore
vendored
@ -4,3 +4,9 @@
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
|
||||
# Eclipse Settings Files #
|
||||
.settings
|
||||
.project
|
||||
.classpath
|
||||
target/
|
||||
|
54
oce-main/oce-crypto/pom.xml
Normal file
54
oce-main/oce-crypto/pom.xml
Normal 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>
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
157
oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cache/PseudonymRepository.java
vendored
Normal file
157
oce-main/oce-crypto/src/main/java/de/sebastianstenzel/oce/crypto/cache/PseudonymRepository.java
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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
91
oce-main/oce-ui/pom.xml
Normal 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>
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
51
oce-main/oce-ui/src/main/resources/access.fxml
Normal file
51
oce-main/oce-ui/src/main/resources/access.fxml
Normal 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>
|
||||
|
||||
|
38
oce-main/oce-ui/src/main/resources/advanced.fxml
Normal file
38
oce-main/oce-ui/src/main/resources/advanced.fxml
Normal 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>
|
||||
|
||||
|
55
oce-main/oce-ui/src/main/resources/initialize.fxml
Normal file
55
oce-main/oce-ui/src/main/resources/initialize.fxml
Normal 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>
|
||||
|
||||
|
35
oce-main/oce-ui/src/main/resources/localization.properties
Normal file
35
oce-main/oce-ui/src/main/resources/localization.properties
Normal 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
|
40
oce-main/oce-ui/src/main/resources/main.css
Normal file
40
oce-main/oce-ui/src/main/resources/main.css
Normal 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;
|
||||
}
|
42
oce-main/oce-ui/src/main/resources/main.fxml
Normal file
42
oce-main/oce-ui/src/main/resources/main.fxml
Normal 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>
|
||||
|
||||
|
39
oce-main/oce-ui/src/main/resources/panels.css
Normal file
39
oce-main/oce-ui/src/main/resources/panels.css
Normal 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);
|
||||
}
|
77
oce-main/oce-webdav/pom.xml
Normal file
77
oce-main/oce-webdav/pom.xml
Normal 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>
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
32
oce-main/oce-webdav/src/main/resources/log4j.xml
Normal file
32
oce-main/oce-webdav/src/main/resources/log4j.xml
Normal 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
131
oce-main/pom.xml
Normal 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>
|
Loading…
Reference in New Issue
Block a user