mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-10-01 04:06:01 +00:00
renamed filesystem layer maven modules
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* 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 org.cryptomator.crypto.engine;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class CryptoException extends IOException {
|
||||
|
||||
private static final long serialVersionUID = -6536997506620449023L;
|
||||
|
||||
public CryptoException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* 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 org.cryptomator.crypto.engine;
|
||||
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
/**
|
||||
* A Cryptor instance, once initialized with a set of keys, provides access to threadsafe cryptographic routines.
|
||||
*/
|
||||
public interface Cryptor extends Destroyable {
|
||||
|
||||
FilenameCryptor getFilenameCryptor();
|
||||
|
||||
void randomizeMasterkey();
|
||||
|
||||
boolean readKeysFromMasterkeyFile(byte[] masterkeyFileContents, CharSequence passphrase);
|
||||
|
||||
byte[] writeKeysToMasterkeyFile(CharSequence passphrase);
|
||||
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* 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 org.cryptomator.crypto.engine;
|
||||
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
/**
|
||||
* Provides deterministic encryption capabilities as filenames must not change on subsequent encryption attempts,
|
||||
* otherwise each change results in major directory structure changes which would be a terrible idea for cloud storage encryption.
|
||||
*
|
||||
* @see <a href="https://en.wikipedia.org/wiki/Deterministic_encryption">Wikipedia on deterministic encryption</a>
|
||||
*/
|
||||
public interface FilenameCryptor extends Destroyable {
|
||||
|
||||
/**
|
||||
* @return constant length string, that is unlikely to collide with any other name.
|
||||
*/
|
||||
String hashDirectoryId(String cleartextDirectoryId);
|
||||
|
||||
/**
|
||||
* @param cleartextName original filename including cleartext file extension
|
||||
* @return encrypted filename without any file extension
|
||||
*/
|
||||
String encryptFilename(String cleartextName);
|
||||
|
||||
/**
|
||||
* @param ciphertextName Ciphertext only, with any additional strings like file extensions stripped first.
|
||||
* @return cleartext filename, probably including its cleartext file extension.
|
||||
*/
|
||||
String decryptFilename(String ciphertextName);
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* 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 org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
final class AesKeyWrap {
|
||||
|
||||
private static final String RFC3394_CIPHER = "AESWrap";
|
||||
|
||||
private AesKeyWrap() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param kek Key encrypting key
|
||||
* @param key Key to be wrapped
|
||||
* @return Wrapped key
|
||||
*/
|
||||
public static byte[] wrap(SecretKey kek, SecretKey key) {
|
||||
final Cipher cipher;
|
||||
try {
|
||||
cipher = Cipher.getInstance(RFC3394_CIPHER);
|
||||
cipher.init(Cipher.WRAP_MODE, kek);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalArgumentException("Invalid key.", e);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new IllegalStateException("Algorithm/Padding should exist.", e);
|
||||
}
|
||||
|
||||
try {
|
||||
return cipher.wrap(key);
|
||||
} catch (InvalidKeyException | IllegalBlockSizeException e) {
|
||||
throw new IllegalStateException("Unable to wrap key.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param kek Key encrypting key
|
||||
* @param wrappedKey Key to be unwrapped
|
||||
* @param keyAlgorithm Key designation, i.e. algorithm name to be associated with the unwrapped key.
|
||||
* @return Unwrapped key
|
||||
* @throws NoSuchAlgorithmException If keyAlgorithm is unknown
|
||||
* @throws InvalidKeyException If unwrapping failed (i.e. wrong kek)
|
||||
*/
|
||||
public static SecretKey unwrap(SecretKey kek, byte[] wrappedKey, String keyAlgorithm) throws InvalidKeyException, NoSuchAlgorithmException {
|
||||
final Cipher cipher;
|
||||
try {
|
||||
cipher = Cipher.getInstance(RFC3394_CIPHER);
|
||||
cipher.init(Cipher.UNWRAP_MODE, kek);
|
||||
} catch (InvalidKeyException ex) {
|
||||
throw new IllegalArgumentException("Invalid key.", ex);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException ex) {
|
||||
throw new IllegalStateException("Algorithm/Padding should exist.", ex);
|
||||
}
|
||||
|
||||
return (SecretKey) cipher.unwrap(wrappedKey, keyAlgorithm, Cipher.SECRET_KEY);
|
||||
}
|
||||
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* 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 org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
|
||||
import org.cryptomator.crypto.engine.Cryptor;
|
||||
import org.cryptomator.crypto.engine.FilenameCryptor;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
public class CryptorImpl implements Cryptor {
|
||||
|
||||
private static final int SCRYPT_SALT_LENGTH = 8;
|
||||
private static final int SCRYPT_COST_PARAM = 1 << 14;
|
||||
private static final int SCRYPT_BLOCK_SIZE = 8;
|
||||
private static final int KEYLENGTH_IN_BYTES = 32;
|
||||
private static final String ENCRYPTION_ALG = "AES";
|
||||
private static final String MAC_ALG = "HmacSHA256";
|
||||
|
||||
private SecretKey encryptionKey;
|
||||
private SecretKey macKey;
|
||||
private final AtomicReference<FilenameCryptor> filenameCryptor = new AtomicReference<>();
|
||||
private final SecureRandom randomSource;
|
||||
|
||||
public CryptorImpl(SecureRandom randomSource) {
|
||||
this.randomSource = randomSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilenameCryptor getFilenameCryptor() {
|
||||
// lazy initialization pattern as proposed here http://stackoverflow.com/a/30247202/4014509
|
||||
final FilenameCryptor existingCryptor = filenameCryptor.get();
|
||||
if (existingCryptor != null) {
|
||||
return existingCryptor;
|
||||
} else {
|
||||
final FilenameCryptorImpl newCryptor = new FilenameCryptorImpl(encryptionKey, macKey);
|
||||
if (filenameCryptor.compareAndSet(null, newCryptor)) {
|
||||
return newCryptor;
|
||||
} else {
|
||||
// CAS failed: other thread set an object
|
||||
newCryptor.destroy();
|
||||
return filenameCryptor.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void randomizeMasterkey() {
|
||||
final byte[] randomBytes = new byte[KEYLENGTH_IN_BYTES];
|
||||
try {
|
||||
randomSource.nextBytes(randomBytes);
|
||||
encryptionKey = new SecretKeySpec(randomBytes, ENCRYPTION_ALG);
|
||||
randomSource.nextBytes(randomBytes);
|
||||
macKey = new SecretKeySpec(randomBytes, ENCRYPTION_ALG);
|
||||
} finally {
|
||||
Arrays.fill(randomBytes, (byte) 0x00);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readKeysFromMasterkeyFile(byte[] masterkeyFileContents, CharSequence passphrase) {
|
||||
final KeyFile keyFile;
|
||||
try {
|
||||
final ObjectMapper om = new ObjectMapper();
|
||||
keyFile = om.readValue(masterkeyFileContents, KeyFile.class);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException("Unable to parse masterkeyFileContents", e);
|
||||
}
|
||||
|
||||
// check version
|
||||
if (keyFile.getVersion() != KeyFile.CURRENT_VERSION) {
|
||||
// TODO
|
||||
// throw new UnsupportedVaultException(keyfile.getVersion(), KeyFile.CURRENT_VERSION);
|
||||
throw new IllegalArgumentException("Unsupported key (expected version: " + KeyFile.CURRENT_VERSION + ", actual version: " + keyFile.getVersion() + ")");
|
||||
}
|
||||
|
||||
final byte[] kekBytes = Scrypt.scrypt(passphrase, keyFile.getScryptSalt(), keyFile.getScryptCostParam(), keyFile.getScryptBlockSize(), KEYLENGTH_IN_BYTES);
|
||||
try {
|
||||
final SecretKey kek = new SecretKeySpec(kekBytes, ENCRYPTION_ALG);
|
||||
this.encryptionKey = AesKeyWrap.unwrap(kek, keyFile.getEncryptionMasterKey(), ENCRYPTION_ALG);
|
||||
this.macKey = AesKeyWrap.unwrap(kek, keyFile.getMacMasterKey(), MAC_ALG);
|
||||
return true;
|
||||
} catch (InvalidKeyException e) {
|
||||
return false;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("Hard-coded algorithm doesn't exist.", e);
|
||||
} finally {
|
||||
Arrays.fill(kekBytes, (byte) 0x00);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] writeKeysToMasterkeyFile(CharSequence passphrase) {
|
||||
final byte[] scryptSalt = new byte[SCRYPT_SALT_LENGTH];
|
||||
randomSource.nextBytes(scryptSalt);
|
||||
|
||||
final byte[] kekBytes = Scrypt.scrypt(passphrase, scryptSalt, SCRYPT_COST_PARAM, SCRYPT_BLOCK_SIZE, KEYLENGTH_IN_BYTES);
|
||||
final byte[] wrappedEncryptionKey;
|
||||
final byte[] wrappedMacKey;
|
||||
try {
|
||||
final SecretKey kek = new SecretKeySpec(kekBytes, ENCRYPTION_ALG);
|
||||
wrappedEncryptionKey = AesKeyWrap.wrap(kek, encryptionKey);
|
||||
wrappedMacKey = AesKeyWrap.wrap(kek, macKey);
|
||||
} finally {
|
||||
Arrays.fill(kekBytes, (byte) 0x00);
|
||||
}
|
||||
|
||||
final KeyFile keyfile = new KeyFile();
|
||||
keyfile.setVersion(KeyFile.CURRENT_VERSION);
|
||||
keyfile.setScryptSalt(scryptSalt);
|
||||
keyfile.setScryptCostParam(SCRYPT_COST_PARAM);
|
||||
keyfile.setScryptBlockSize(SCRYPT_BLOCK_SIZE);
|
||||
keyfile.setEncryptionMasterKey(wrappedEncryptionKey);
|
||||
keyfile.setMacMasterKey(wrappedMacKey);
|
||||
|
||||
try {
|
||||
final ObjectMapper om = new ObjectMapper();
|
||||
return om.writeValueAsBytes(keyfile);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalArgumentException("Unable to create JSON from " + keyfile, e);
|
||||
}
|
||||
}
|
||||
|
||||
/* ======================= destruction ======================= */
|
||||
|
||||
@Override
|
||||
public void destroy() throws DestroyFailedException {
|
||||
TheDestroyer.destroyQuietly(encryptionKey);
|
||||
TheDestroyer.destroyQuietly(macKey);
|
||||
if (filenameCryptor.get() != null) {
|
||||
TheDestroyer.destroyQuietly(getFilenameCryptor());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
return encryptionKey.isDestroyed() && macKey.isDestroyed() && (filenameCryptor.get() == null || filenameCryptor.get().isDestroyed());
|
||||
}
|
||||
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* 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 org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.AEADBadTagException;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.apache.commons.codec.binary.Base32;
|
||||
import org.apache.commons.codec.binary.BaseNCodec;
|
||||
import org.cryptomator.crypto.engine.CryptoException;
|
||||
import org.cryptomator.crypto.engine.FilenameCryptor;
|
||||
import org.cryptomator.siv.SivMode;
|
||||
|
||||
class FilenameCryptorImpl implements FilenameCryptor {
|
||||
|
||||
private static final BaseNCodec BASE32 = new Base32();
|
||||
private static final ThreadLocal<MessageDigest> SHA1 = new ThreadLocalSha1();
|
||||
private static final SivMode AES_SIV = new SivMode();
|
||||
|
||||
private final SecretKey encryptionKey;
|
||||
private final SecretKey macKey;
|
||||
|
||||
FilenameCryptorImpl(SecretKey encryptionKey, SecretKey macKey) {
|
||||
if (encryptionKey == null || macKey == null) {
|
||||
throw new IllegalArgumentException("Key must not be null");
|
||||
}
|
||||
this.encryptionKey = encryptionKey;
|
||||
this.macKey = macKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String hashDirectoryId(String cleartextDirectoryId) {
|
||||
final byte[] cleartextBytes = cleartextDirectoryId.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] encryptedBytes = AES_SIV.encrypt(encryptionKey, macKey, cleartextBytes);
|
||||
final byte[] hashedBytes = SHA1.get().digest(encryptedBytes);
|
||||
return BASE32.encodeAsString(hashedBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encryptFilename(String cleartextName) {
|
||||
final byte[] cleartextBytes = cleartextName.getBytes(StandardCharsets.UTF_8);
|
||||
final byte[] encryptedBytes = AES_SIV.encrypt(encryptionKey, macKey, cleartextBytes);
|
||||
return BASE32.encodeAsString(encryptedBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decryptFilename(String ciphertextName) {
|
||||
final byte[] encryptedBytes = BASE32.decode(ciphertextName);
|
||||
try {
|
||||
final byte[] cleartextBytes = AES_SIV.decrypt(encryptionKey, macKey, encryptedBytes);
|
||||
return new String(cleartextBytes, StandardCharsets.UTF_8);
|
||||
} catch (AEADBadTagException e) {
|
||||
throw new UncheckedIOException(new CryptoException("Authentication failed.", e));
|
||||
}
|
||||
}
|
||||
|
||||
private static class ThreadLocalSha1 extends ThreadLocal<MessageDigest> {
|
||||
|
||||
@Override
|
||||
protected MessageDigest initialValue() {
|
||||
try {
|
||||
return MessageDigest.getInstance("SHA-1");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError("SHA-1 exists in every JVM");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageDigest get() {
|
||||
final MessageDigest messageDigest = super.get();
|
||||
messageDigest.reset();
|
||||
return messageDigest;
|
||||
}
|
||||
}
|
||||
|
||||
/* ======================= destruction ======================= */
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
TheDestroyer.destroyQuietly(encryptionKey);
|
||||
TheDestroyer.destroyQuietly(macKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
return encryptionKey.isDestroyed() && macKey.isDestroyed();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* 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 org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
|
||||
@JsonPropertyOrder(value = {"version", "scryptSalt", "scryptCostParam", "scryptBlockSize", "primaryMasterKey", "hmacMasterKey"})
|
||||
class KeyFile implements Serializable {
|
||||
|
||||
static final Integer CURRENT_VERSION = 3;
|
||||
private static final long serialVersionUID = 8578363158959619885L;
|
||||
|
||||
@JsonProperty("version")
|
||||
private Integer version;
|
||||
|
||||
@JsonProperty("scryptSalt")
|
||||
private byte[] scryptSalt;
|
||||
|
||||
@JsonProperty("scryptCostParam")
|
||||
private int scryptCostParam;
|
||||
|
||||
@JsonProperty("scryptBlockSize")
|
||||
private int scryptBlockSize;
|
||||
|
||||
@JsonProperty("primaryMasterKey")
|
||||
private byte[] encryptionMasterKey;
|
||||
|
||||
@JsonProperty("hmacMasterKey")
|
||||
private byte[] macMasterKey;
|
||||
|
||||
public Integer getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(Integer version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public byte[] getScryptSalt() {
|
||||
return scryptSalt;
|
||||
}
|
||||
|
||||
public void setScryptSalt(byte[] scryptSalt) {
|
||||
this.scryptSalt = scryptSalt;
|
||||
}
|
||||
|
||||
public int getScryptCostParam() {
|
||||
return scryptCostParam;
|
||||
}
|
||||
|
||||
public void setScryptCostParam(int scryptCostParam) {
|
||||
this.scryptCostParam = scryptCostParam;
|
||||
}
|
||||
|
||||
public int getScryptBlockSize() {
|
||||
return scryptBlockSize;
|
||||
}
|
||||
|
||||
public void setScryptBlockSize(int scryptBlockSize) {
|
||||
this.scryptBlockSize = scryptBlockSize;
|
||||
}
|
||||
|
||||
public byte[] getEncryptionMasterKey() {
|
||||
return encryptionMasterKey;
|
||||
}
|
||||
|
||||
public void setEncryptionMasterKey(byte[] encryptionMasterKey) {
|
||||
this.encryptionMasterKey = encryptionMasterKey;
|
||||
}
|
||||
|
||||
public byte[] getMacMasterKey() {
|
||||
return macMasterKey;
|
||||
}
|
||||
|
||||
public void setMacMasterKey(byte[] macMasterKey) {
|
||||
this.macMasterKey = macMasterKey;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* 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 org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.bouncycastle.crypto.generators.SCrypt;
|
||||
|
||||
final class Scrypt {
|
||||
|
||||
private Scrypt() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives a key from the given passphrase.
|
||||
* This implementation makes sure, any copies of the passphrase used during key derivation are overwritten in memory asap (before next GC cycle).
|
||||
*
|
||||
* @param passphrase The passphrase
|
||||
* @param salt Salt, ideally randomly generated
|
||||
* @param costParam Cost parameter <code>N</code>, larger than 1, a power of 2 and less than <code>2^(128 * costParam / 8)</code>
|
||||
* @param blockSize Block size <code>r</code>
|
||||
* @param keyLengthInBytes Key output length <code>dkLen</code>
|
||||
* @return Derived key
|
||||
* @see <a href="https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-04#section-2">RFC Draft</a>
|
||||
*/
|
||||
public static byte[] scrypt(CharSequence passphrase, byte[] salt, int costParam, int blockSize, int keyLengthInBytes) {
|
||||
// This is an attempt to get the password bytes without copies of the password being created in some dark places inside the JVM:
|
||||
final ByteBuffer buf = StandardCharsets.UTF_8.encode(CharBuffer.wrap(passphrase));
|
||||
final byte[] pw = new byte[buf.remaining()];
|
||||
buf.get(pw);
|
||||
try {
|
||||
return SCrypt.generate(pw, salt, costParam, blockSize, 1, keyLengthInBytes);
|
||||
} finally {
|
||||
Arrays.fill(pw, (byte) 0); // overwrite bytes
|
||||
buf.rewind(); // just resets markers
|
||||
buf.put(pw); // this is where we overwrite the actual bytes
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* 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 org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
final class TheDestroyer {
|
||||
|
||||
private TheDestroyer() {
|
||||
|
||||
}
|
||||
|
||||
public static void destroyQuietly(Destroyable d) {
|
||||
try {
|
||||
d.destroy();
|
||||
} catch (DestroyFailedException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* 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
|
||||
*******************************************************************************/
|
||||
/**
|
||||
* This is where the actual encryption, decryption, hashing and authenticating takes place.
|
||||
*/
|
||||
package org.cryptomator.crypto.engine;
|
||||
@@ -0,0 +1,59 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* 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 org.cryptomator.crypto.fs;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.cryptomator.crypto.engine.Cryptor;
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.ReadableFile;
|
||||
import org.cryptomator.filesystem.WritableFile;
|
||||
|
||||
public class CryptoFile extends CryptoNode implements File {
|
||||
|
||||
static final String FILE_EXT = ".file";
|
||||
|
||||
public CryptoFile(CryptoFolder parent, String name, Cryptor cryptor) {
|
||||
super(parent, name, cryptor);
|
||||
}
|
||||
|
||||
String encryptedName() {
|
||||
return cryptor.getFilenameCryptor().encryptFilename(name()) + FILE_EXT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant lastModified() throws UncheckedIOException {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadableFile openReadable() {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WritableFile openWritable() {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return parent.toString() + name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(File o) {
|
||||
return toString().compareTo(o.toString());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* 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 org.cryptomator.crypto.fs;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.cryptomator.crypto.engine.Cryptor;
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.FileSystem;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.FolderCreateMode;
|
||||
import org.cryptomator.filesystem.ReadableFile;
|
||||
import org.cryptomator.filesystem.WritableFile;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class CryptoFileSystem extends CryptoFolder implements FileSystem {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CryptoFileSystem.class);
|
||||
private static final String DATA_ROOT_DIR = "d";
|
||||
private static final String ROOT_DIR_FILE = "root";
|
||||
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator";
|
||||
private static final String MASTERKEY_BACKUP_FILENAME = "masterkey.cryptomator.bkup";
|
||||
|
||||
private final Folder physicalRoot;
|
||||
|
||||
public CryptoFileSystem(Folder physicalRoot, Cryptor cryptor, CharSequence passphrase) {
|
||||
super(null, "", cryptor);
|
||||
this.physicalRoot = physicalRoot;
|
||||
final File masterkeyFile = physicalRoot.file(MASTERKEY_FILENAME);
|
||||
if (masterkeyFile.exists()) {
|
||||
final boolean unlocked = decryptMasterKeyFile(cryptor, masterkeyFile, passphrase);
|
||||
if (!unlocked) {
|
||||
// TODO new InvalidPassphraseException() ?
|
||||
throw new IllegalArgumentException("Wrong passphrase.");
|
||||
}
|
||||
} else {
|
||||
encryptMasterKeyFile(cryptor, masterkeyFile, passphrase);
|
||||
}
|
||||
assert masterkeyFile.exists() : "A CryptoFileSystem can not exist without a masterkey file.";
|
||||
final File backupFile = physicalRoot.file(MASTERKEY_BACKUP_FILENAME);
|
||||
masterkeyFile.copyTo(backupFile);
|
||||
}
|
||||
|
||||
private static boolean decryptMasterKeyFile(Cryptor cryptor, File masterkeyFile, CharSequence passphrase) {
|
||||
try (ReadableFile file = masterkeyFile.openReadable()) {
|
||||
// TODO we need to read the whole file but can not be sure about the
|
||||
// buffer size:
|
||||
final ByteBuffer bigEnoughBuffer = ByteBuffer.allocate(500);
|
||||
file.read(bigEnoughBuffer);
|
||||
bigEnoughBuffer.flip();
|
||||
assert bigEnoughBuffer.remaining() < bigEnoughBuffer.capacity() : "The buffer wasn't big enough.";
|
||||
final byte[] fileContents = new byte[bigEnoughBuffer.remaining()];
|
||||
bigEnoughBuffer.get(fileContents);
|
||||
return cryptor.readKeysFromMasterkeyFile(fileContents, passphrase);
|
||||
}
|
||||
}
|
||||
|
||||
private static void encryptMasterKeyFile(Cryptor cryptor, File masterkeyFile, CharSequence passphrase) {
|
||||
try (WritableFile file = masterkeyFile.openWritable()) {
|
||||
final byte[] fileContents = cryptor.writeKeysToMasterkeyFile(passphrase);
|
||||
file.write(ByteBuffer.wrap(fileContents));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
File physicalFile() {
|
||||
return physicalDataRoot().file(ROOT_DIR_FILE);
|
||||
}
|
||||
|
||||
@Override
|
||||
Folder physicalDataRoot() {
|
||||
return physicalRoot.folder(DATA_ROOT_DIR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<CryptoFolder> parent() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return physicalRoot.exists();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete() {
|
||||
// no-op.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void create(FolderCreateMode mode) {
|
||||
physicalDataRoot().create(mode);
|
||||
final File dirFile = physicalFile();
|
||||
final String directoryId = getDirectoryId();
|
||||
try (WritableFile writable = dirFile.openWritable()) {
|
||||
final ByteBuffer buf = ByteBuffer.wrap(directoryId.getBytes());
|
||||
writable.write(buf);
|
||||
}
|
||||
physicalFolder().create(FolderCreateMode.INCLUDING_PARENTS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return physicalRoot + ":::/";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* 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 org.cryptomator.crypto.fs;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.cryptomator.crypto.engine.Cryptor;
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.FolderCreateMode;
|
||||
import org.cryptomator.filesystem.Node;
|
||||
import org.cryptomator.filesystem.ReadableFile;
|
||||
import org.cryptomator.filesystem.WritableFile;
|
||||
|
||||
class CryptoFolder extends CryptoNode implements Folder {
|
||||
|
||||
static final String FILE_EXT = ".dir";
|
||||
|
||||
private final AtomicReference<String> directoryId = new AtomicReference<>();
|
||||
|
||||
public CryptoFolder(CryptoFolder parent, String name, Cryptor cryptor) {
|
||||
super(parent, name, cryptor);
|
||||
}
|
||||
|
||||
String encryptedName() {
|
||||
return cryptor.getFilenameCryptor().encryptFilename(name()) + FILE_EXT;
|
||||
}
|
||||
|
||||
protected String getDirectoryId() {
|
||||
if (directoryId.get() == null) {
|
||||
File dirFile = physicalFile();
|
||||
if (dirFile.exists()) {
|
||||
try (ReadableFile readable = dirFile.openReadable()) {
|
||||
final ByteBuffer buf = ByteBuffer.allocate(64);
|
||||
readable.read(buf);
|
||||
buf.flip();
|
||||
byte[] bytes = new byte[buf.remaining()];
|
||||
buf.get(bytes);
|
||||
directoryId.set(new String(bytes));
|
||||
}
|
||||
} else {
|
||||
directoryId.compareAndSet(null, UUID.randomUUID().toString());
|
||||
}
|
||||
}
|
||||
return directoryId.get();
|
||||
}
|
||||
|
||||
File physicalFile() {
|
||||
return parent.physicalFolder().file(encryptedName());
|
||||
}
|
||||
|
||||
Folder physicalFolder() {
|
||||
final String encryptedThenHashedDirId = cryptor.getFilenameCryptor().hashDirectoryId(getDirectoryId());
|
||||
return physicalDataRoot().folder(encryptedThenHashedDirId.substring(0, 2)).folder(encryptedThenHashedDirId.substring(2));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant lastModified() {
|
||||
return physicalFile().lastModified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<? extends Node> children() {
|
||||
return Stream.concat(files(), folders());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<CryptoFile> files() {
|
||||
return physicalFolder().files().map(File::name).filter(s -> s.endsWith(CryptoFile.FILE_EXT)).map(this::decryptFileName).map(this::file);
|
||||
}
|
||||
|
||||
private String decryptFileName(String encryptedFileName) {
|
||||
final String ciphertext = StringUtils.removeEnd(encryptedFileName, CryptoFile.FILE_EXT);
|
||||
return cryptor.getFilenameCryptor().decryptFilename(ciphertext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CryptoFile file(String name) {
|
||||
return new CryptoFile(this, name, cryptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<CryptoFolder> folders() {
|
||||
return physicalFolder().files().map(File::name).filter(s -> s.endsWith(CryptoFolder.FILE_EXT)).map(this::decryptFolderName).map(this::folder);
|
||||
}
|
||||
|
||||
private String decryptFolderName(String encryptedFolderName) {
|
||||
final String ciphertext = StringUtils.removeEnd(encryptedFolderName, CryptoFolder.FILE_EXT);
|
||||
return cryptor.getFilenameCryptor().decryptFilename(ciphertext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CryptoFolder folder(String name) {
|
||||
return new CryptoFolder(this, name, cryptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void create(FolderCreateMode mode) {
|
||||
final File dirFile = physicalFile();
|
||||
if (dirFile.exists()) {
|
||||
return;
|
||||
}
|
||||
if (!parent.exists() && FolderCreateMode.FAIL_IF_PARENT_IS_MISSING.equals(mode)) {
|
||||
throw new UncheckedIOException(new FileNotFoundException(parent.name));
|
||||
} else if (!parent.exists() && FolderCreateMode.INCLUDING_PARENTS.equals(mode)) {
|
||||
parent.create(mode);
|
||||
}
|
||||
assert parent.exists();
|
||||
final String directoryId = getDirectoryId();
|
||||
try (WritableFile writable = dirFile.openWritable()) {
|
||||
final ByteBuffer buf = ByteBuffer.wrap(directoryId.getBytes());
|
||||
writable.write(buf);
|
||||
}
|
||||
physicalFolder().create(FolderCreateMode.INCLUDING_PARENTS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveTo(Folder target) {
|
||||
if (target instanceof CryptoFolder) {
|
||||
moveToInternal((CryptoFolder) target);
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Can not move CryptoFolder to conventional folder.");
|
||||
}
|
||||
}
|
||||
|
||||
private void moveToInternal(CryptoFolder target) {
|
||||
if (this.isAncestorOf(target) || target.isAncestorOf(this)) {
|
||||
throw new IllegalArgumentException("Can not move directories containing one another (src: " + this + ", dst: " + target + ")");
|
||||
}
|
||||
|
||||
target.physicalFile().parent().get().create(FolderCreateMode.INCLUDING_PARENTS);
|
||||
assert target.physicalFile().parent().get().exists();
|
||||
try (WritableFile src = this.physicalFile().openWritable(); WritableFile dst = target.physicalFile().openWritable()) {
|
||||
src.moveTo(dst);
|
||||
}
|
||||
// directoryId is now used by target, we must no longer use the same id
|
||||
// (we'll generate a new one when needed)
|
||||
directoryId.set(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete() {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return parent.toString() + name + "/";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* 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 org.cryptomator.crypto.fs;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.cryptomator.crypto.engine.Cryptor;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.Node;
|
||||
|
||||
abstract class CryptoNode implements Node {
|
||||
|
||||
protected final CryptoFolder parent;
|
||||
protected final String name;
|
||||
protected final Cryptor cryptor;
|
||||
|
||||
public CryptoNode(CryptoFolder parent, String name, Cryptor cryptor) {
|
||||
this.parent = parent;
|
||||
this.name = name;
|
||||
this.cryptor = cryptor;
|
||||
}
|
||||
|
||||
Folder physicalDataRoot() {
|
||||
return parent.physicalDataRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<CryptoFolder> parent() {
|
||||
return Optional.of(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return parent.children().anyMatch(node -> node.equals(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((name == null) ? 0 : name.hashCode());
|
||||
result = prime * result + ((parent == null) ? 0 : parent.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof CryptoNode) {
|
||||
CryptoNode other = (CryptoNode) obj;
|
||||
return this.getClass() == other.getClass() //
|
||||
&& (this.parent == null && other.parent == null || this.parent.equals(other.parent)) //
|
||||
&& (this.name == null && other.name == null || this.name.equals(other.name));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* 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
|
||||
*******************************************************************************/
|
||||
/**
|
||||
* Provides a decoration layer for the {@link org.cryptomator.filesystem Filesystem API}, consuming an encrypted file system and providing access to a cleartext filesystem.
|
||||
* While the implementation in this package dictates the Vault directory layout, no encryption code can be found here.
|
||||
* All cryptographic operations are delegated to the {@link org.cryptomator.crypto.engine CryptoEngine}.
|
||||
*/
|
||||
package org.cryptomator.crypto.fs;
|
||||
@@ -0,0 +1,37 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* 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 org.cryptomator.crypto.engine;
|
||||
|
||||
public class NoCryptor implements Cryptor {
|
||||
|
||||
private final FilenameCryptor filenameCryptor = new NoFilenameCryptor();
|
||||
|
||||
@Override
|
||||
public FilenameCryptor getFilenameCryptor() {
|
||||
return filenameCryptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void randomizeMasterkey() {
|
||||
// like this? https://xkcd.com/221/
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readKeysFromMasterkeyFile(byte[] masterkeyFileContents, CharSequence passphrase) {
|
||||
// thanks, but I don't need a key, if I'm not encryption anything...
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] writeKeysToMasterkeyFile(CharSequence passphrase) {
|
||||
// ok, if you insist to get my non-existing key data... here you go:
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* 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 org.cryptomator.crypto.engine;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import org.apache.commons.codec.binary.Base32;
|
||||
import org.apache.commons.codec.binary.BaseNCodec;
|
||||
|
||||
class NoFilenameCryptor implements FilenameCryptor {
|
||||
|
||||
private static final BaseNCodec BASE32 = new Base32();
|
||||
private static final ThreadLocal<MessageDigest> SHA1 = new ThreadLocalSha1();
|
||||
|
||||
@Override
|
||||
public String hashDirectoryId(String cleartextDirectoryId) {
|
||||
final byte[] cleartextBytes = cleartextDirectoryId.getBytes(StandardCharsets.UTF_8);
|
||||
final byte[] hashedBytes = SHA1.get().digest(cleartextBytes);
|
||||
return BASE32.encodeAsString(hashedBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encryptFilename(String cleartextName) {
|
||||
return cleartextName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decryptFilename(String ciphertextName) {
|
||||
return ciphertextName;
|
||||
}
|
||||
|
||||
private static class ThreadLocalSha1 extends ThreadLocal<MessageDigest> {
|
||||
|
||||
@Override
|
||||
protected MessageDigest initialValue() {
|
||||
try {
|
||||
return MessageDigest.getInstance("SHA-1");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError("SHA-1 exists in every JVM");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageDigest get() {
|
||||
final MessageDigest sha1 = super.get();
|
||||
sha1.reset();
|
||||
return sha1;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* 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 org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.cryptomator.crypto.engine.Cryptor;
|
||||
import org.cryptomator.crypto.engine.FilenameCryptor;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class CryptorImplTest {
|
||||
|
||||
private static final SecureRandom RANDOM_MOCK = new SecureRandom() {
|
||||
|
||||
private static final long serialVersionUID = 1505563778398085504L;
|
||||
|
||||
@Override
|
||||
public void nextBytes(byte[] bytes) {
|
||||
Arrays.fill(bytes, (byte) 0x00);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@Test
|
||||
public void testMasterkeyDecryption() throws IOException {
|
||||
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"}";
|
||||
final Cryptor cryptor = new CryptorImpl(RANDOM_MOCK);
|
||||
Assert.assertFalse(cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "qwe"));
|
||||
Assert.assertTrue(cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMasterkeyEncryption() throws IOException {
|
||||
final String expectedMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":16384,\"scryptBlockSize\":8," //
|
||||
+ "\"primaryMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," //
|
||||
+ "\"hmacMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"}";
|
||||
final Cryptor cryptor = new CryptorImpl(RANDOM_MOCK);
|
||||
cryptor.randomizeMasterkey();
|
||||
final byte[] masterkeyFile = cryptor.writeKeysToMasterkeyFile("asd");
|
||||
Assert.assertArrayEquals(expectedMasterKey.getBytes(), masterkeyFile);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFilenameCryptorAfterUnlocking() {
|
||||
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"}";
|
||||
final Cryptor cryptor = new CryptorImpl(RANDOM_MOCK);
|
||||
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
|
||||
Assert.assertNotNull(cryptor.getFilenameCryptor());
|
||||
}
|
||||
|
||||
@Test(expected = RuntimeException.class)
|
||||
public void testGetFilenameCryptorBeforeUnlocking() {
|
||||
final Cryptor cryptor = new CryptorImpl(RANDOM_MOCK);
|
||||
cryptor.getFilenameCryptor();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConcurrentGetFilenameCryptor() throws InterruptedException {
|
||||
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"}";
|
||||
final Cryptor cryptor = new CryptorImpl(RANDOM_MOCK);
|
||||
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
|
||||
|
||||
final AtomicReference<FilenameCryptor> receivedByT1 = new AtomicReference<>();
|
||||
final Thread t1 = new Thread(() -> {
|
||||
receivedByT1.set(cryptor.getFilenameCryptor());
|
||||
});
|
||||
|
||||
final AtomicReference<FilenameCryptor> receivedByT2 = new AtomicReference<>();
|
||||
final Thread t2 = new Thread(() -> {
|
||||
receivedByT2.set(cryptor.getFilenameCryptor());
|
||||
});
|
||||
t1.start();
|
||||
t2.start();
|
||||
t1.join();
|
||||
t2.join();
|
||||
// It is not guaranteed, both threads will enter getFilenameCryptor() exactly simultaneously. (But logging shows it is very likely)
|
||||
// In any case both threads should receive the same FilenameCryptor
|
||||
Assert.assertSame(receivedByT1.get(), receivedByT2.get());
|
||||
}
|
||||
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* 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 org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.cryptomator.crypto.engine.FilenameCryptor;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class FilenameCryptorImplTest {
|
||||
|
||||
@Test
|
||||
public void testDeterministicEncryptionOfFilenames() throws IOException {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final FilenameCryptor filenameCryptor = new FilenameCryptorImpl(encryptionKey, macKey);
|
||||
|
||||
// some random
|
||||
for (int i = 0; i < 2000; i++) {
|
||||
final String origName = UUID.randomUUID().toString();
|
||||
final String encrypted1 = filenameCryptor.encryptFilename(origName);
|
||||
final String encrypted2 = filenameCryptor.encryptFilename(origName);
|
||||
Assert.assertEquals(encrypted1, encrypted2);
|
||||
final String decrypted = filenameCryptor.decryptFilename(encrypted1);
|
||||
Assert.assertEquals(origName, decrypted);
|
||||
}
|
||||
|
||||
// block size length file names
|
||||
final String originalPath3 = "aaaabbbbccccdddd"; // 128 bit ascii
|
||||
final String encryptedPath3a = filenameCryptor.encryptFilename(originalPath3);
|
||||
final String encryptedPath3b = filenameCryptor.encryptFilename(originalPath3);
|
||||
Assert.assertEquals(encryptedPath3a, encryptedPath3b);
|
||||
final String decryptedPath3 = filenameCryptor.decryptFilename(encryptedPath3a);
|
||||
Assert.assertEquals(originalPath3, decryptedPath3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeterministicHashingOfDirectoryIds() throws IOException {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final FilenameCryptor filenameCryptor = new FilenameCryptorImpl(encryptionKey, macKey);
|
||||
|
||||
// some random
|
||||
for (int i = 0; i < 2000; i++) {
|
||||
final String originalDirectoryId = UUID.randomUUID().toString();
|
||||
final String hashedDirectory1 = filenameCryptor.hashDirectoryId(originalDirectoryId);
|
||||
final String hashedDirectory2 = filenameCryptor.hashDirectoryId(originalDirectoryId);
|
||||
Assert.assertEquals(hashedDirectory1, hashedDirectory2);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* 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 org.cryptomator.crypto.fs;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.cryptomator.crypto.engine.Cryptor;
|
||||
import org.cryptomator.crypto.engine.NoCryptor;
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.FileSystem;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.FolderCreateMode;
|
||||
import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class CryptoFileSystemTest {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CryptoFileSystemTest.class);
|
||||
|
||||
@Test
|
||||
public void testVaultStructureInitialization() throws UncheckedIOException, IOException {
|
||||
// mock cryptor:
|
||||
final Cryptor cryptor = new NoCryptor();
|
||||
|
||||
// some mock fs:
|
||||
final FileSystem physicalFs = new InMemoryFileSystem();
|
||||
final File masterkeyFile = physicalFs.file("masterkey.cryptomator");
|
||||
final File masterkeyBkupFile = physicalFs.file("masterkey.cryptomator.bkup");
|
||||
final Folder physicalDataRoot = physicalFs.folder("d");
|
||||
Assert.assertFalse(masterkeyFile.exists());
|
||||
Assert.assertFalse(masterkeyBkupFile.exists());
|
||||
Assert.assertFalse(physicalDataRoot.exists());
|
||||
|
||||
// init crypto fs:
|
||||
final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor, "foo");
|
||||
Assert.assertTrue(masterkeyFile.exists());
|
||||
Assert.assertTrue(masterkeyBkupFile.exists());
|
||||
fs.create(FolderCreateMode.INCLUDING_PARENTS);
|
||||
Assert.assertTrue(physicalDataRoot.exists());
|
||||
Assert.assertEquals(3, physicalFs.children().count()); // d + masterkey.cryptomator + masterkey.cryptomator.bkup
|
||||
Assert.assertEquals(1, physicalDataRoot.files().count()); // ROOT file
|
||||
Assert.assertEquals(1, physicalDataRoot.folders().count()); // ROOT directory
|
||||
|
||||
LOG.debug(DirectoryPrinter.print(physicalFs));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMasterkeyBackupBehaviour() throws InterruptedException {
|
||||
// mock cryptor:
|
||||
final Cryptor cryptor = new NoCryptor();
|
||||
|
||||
// some mock fs:
|
||||
final FileSystem physicalFs = new InMemoryFileSystem();
|
||||
final File masterkeyBkupFile = physicalFs.file("masterkey.cryptomator.bkup");
|
||||
Assert.assertFalse(masterkeyBkupFile.exists());
|
||||
|
||||
// first initialization:
|
||||
new CryptoFileSystem(physicalFs, cryptor, "foo");
|
||||
Assert.assertTrue(masterkeyBkupFile.exists());
|
||||
final Instant bkupDateT0 = masterkeyBkupFile.lastModified();
|
||||
|
||||
// make sure some time passes, as the resolution of last modified date is not in nanos:
|
||||
Thread.sleep(1);
|
||||
|
||||
// second initialization:
|
||||
new CryptoFileSystem(physicalFs, cryptor, "foo");
|
||||
Assert.assertTrue(masterkeyBkupFile.exists());
|
||||
final Instant bkupDateT1 = masterkeyBkupFile.lastModified();
|
||||
|
||||
Assert.assertTrue(bkupDateT1.isAfter(bkupDateT0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDirectoryCreation() throws UncheckedIOException, IOException {
|
||||
// mock stuff and prepare crypto FS:
|
||||
final Cryptor cryptor = new NoCryptor();
|
||||
final FileSystem physicalFs = new InMemoryFileSystem();
|
||||
final Folder physicalDataRoot = physicalFs.folder("d");
|
||||
final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor, "foo");
|
||||
fs.create(FolderCreateMode.INCLUDING_PARENTS);
|
||||
|
||||
// add another encrypted folder:
|
||||
final Folder fooFolder = fs.folder("foo");
|
||||
final Folder fooBarFolder = fooFolder.folder("bar");
|
||||
Assert.assertFalse(fooFolder.exists());
|
||||
Assert.assertFalse(fooBarFolder.exists());
|
||||
fooBarFolder.create(FolderCreateMode.INCLUDING_PARENTS);
|
||||
Assert.assertTrue(fooFolder.exists());
|
||||
Assert.assertTrue(fooBarFolder.exists());
|
||||
Assert.assertEquals(3, countDataFolders(physicalDataRoot)); // parent + foo + bar
|
||||
|
||||
LOG.debug(DirectoryPrinter.print(fs));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDirectoryMoving() throws UncheckedIOException, IOException {
|
||||
// mock stuff and prepare crypto FS:
|
||||
final Cryptor cryptor = new NoCryptor();
|
||||
final FileSystem physicalFs = new InMemoryFileSystem();
|
||||
final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor, "foo");
|
||||
fs.create(FolderCreateMode.INCLUDING_PARENTS);
|
||||
|
||||
// create foo/bar/ and then move foo/ to baz/:
|
||||
final Folder fooFolder = fs.folder("foo");
|
||||
final Folder fooBarFolder = fooFolder.folder("bar");
|
||||
final Folder bazFolder = fs.folder("baz");
|
||||
final Folder bazBarFolder = bazFolder.folder("bar");
|
||||
fooBarFolder.create(FolderCreateMode.INCLUDING_PARENTS);
|
||||
Assert.assertTrue(fooBarFolder.exists());
|
||||
Assert.assertFalse(bazFolder.exists());
|
||||
fooFolder.moveTo(bazFolder);
|
||||
// foo/bar/ should no longer exist, but baz/bar/ should:
|
||||
Assert.assertFalse(fooBarFolder.exists());
|
||||
Assert.assertTrue(bazFolder.exists());
|
||||
Assert.assertTrue(bazBarFolder.exists());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testDirectoryMovingWithinBloodline() throws UncheckedIOException, IOException {
|
||||
// mock stuff and prepare crypto FS:
|
||||
final Cryptor cryptor = new NoCryptor();
|
||||
final FileSystem physicalFs = new InMemoryFileSystem();
|
||||
final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor, "foo");
|
||||
fs.create(FolderCreateMode.INCLUDING_PARENTS);
|
||||
|
||||
// create foo/bar/ and then try to move foo/bar/ to foo/
|
||||
final Folder fooFolder = fs.folder("foo");
|
||||
final Folder fooBarFolder = fooFolder.folder("bar");
|
||||
fooBarFolder.create(FolderCreateMode.INCLUDING_PARENTS);
|
||||
fooBarFolder.moveTo(fooFolder);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return number of folders on second level inside the given dataRoot folder.
|
||||
*/
|
||||
private static int countDataFolders(Folder dataRoot) {
|
||||
final AtomicInteger num = new AtomicInteger();
|
||||
DirectoryWalker.walk(dataRoot, 0, 2, (node) -> {
|
||||
if (node instanceof Folder) {
|
||||
final Folder nodeParent = node.parent().get();
|
||||
final Folder nodeParentParent = nodeParent.parent().orElse(null);
|
||||
if (nodeParentParent != null && nodeParentParent.equals(dataRoot)) {
|
||||
num.incrementAndGet();
|
||||
}
|
||||
}
|
||||
});
|
||||
return num.get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* 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 org.cryptomator.crypto.fs;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
|
||||
public final class DirectoryPrinter {
|
||||
|
||||
private DirectoryPrinter() {
|
||||
}
|
||||
|
||||
public static String print(Folder folder) {
|
||||
StringBuilder sb = new StringBuilder(folder.name()).append('\n');
|
||||
|
||||
DirectoryWalker.walk(folder, (node) -> {
|
||||
Optional<? extends Folder> parent = node.parent();
|
||||
while (parent.isPresent()) {
|
||||
sb.append(" ");
|
||||
parent = parent.get().parent();
|
||||
}
|
||||
if (node instanceof Folder) {
|
||||
sb.append(node.name()).append('/').append('\n');
|
||||
} else if (node instanceof File) {
|
||||
sb.append(node.name()).append('\n');
|
||||
}
|
||||
});
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* 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 org.cryptomator.crypto.fs;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.Node;
|
||||
|
||||
final class DirectoryWalker {
|
||||
|
||||
private DirectoryWalker() {
|
||||
}
|
||||
|
||||
public static void walk(Folder folder, Consumer<Node> visitor) {
|
||||
walk(folder, 0, Integer.MAX_VALUE, visitor);
|
||||
}
|
||||
|
||||
public static void walk(Folder folder, int depth, int maxDepth, Consumer<Node> visitor) {
|
||||
folder.files().forEach(visitor);
|
||||
if (depth == maxDepth) {
|
||||
return;
|
||||
} else {
|
||||
folder.folders().forEach(childFolder -> {
|
||||
visitor.accept(childFolder);
|
||||
walk(childFolder, depth + 1, maxDepth, visitor);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package org.cryptomator.crypto.fs;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.cryptomator.crypto.engine.Cryptor;
|
||||
import org.cryptomator.crypto.engine.impl.CryptorImpl;
|
||||
import org.cryptomator.filesystem.FileSystem;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.FolderCreateMode;
|
||||
import org.cryptomator.filesystem.Node;
|
||||
import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
|
||||
import org.cryptomator.shortening.ShorteningFileSystem;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class EncryptAndShortenIntegrationTest {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EncryptAndShortenIntegrationTest.class);
|
||||
|
||||
private static final SecureRandom RANDOM_MOCK = new SecureRandom() {
|
||||
|
||||
private static final long serialVersionUID = 1505563778398085504L;
|
||||
|
||||
@Override
|
||||
public void nextBytes(byte[] bytes) {
|
||||
Arrays.fill(bytes, (byte) 0x00);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@Test
|
||||
public void testEncryptionOfLongFolderNames() {
|
||||
final FileSystem physicalFs = new InMemoryFileSystem();
|
||||
final FileSystem shorteningFs = new ShorteningFileSystem(physicalFs, physicalFs.folder("m"), 70);
|
||||
final Cryptor cryptor = new CryptorImpl(RANDOM_MOCK);
|
||||
cryptor.randomizeMasterkey();
|
||||
final FileSystem fs = new CryptoFileSystem(shorteningFs, cryptor, "foo");
|
||||
fs.create(FolderCreateMode.FAIL_IF_PARENT_IS_MISSING);
|
||||
final Folder shortFolder = fs.folder("normal folder name");
|
||||
shortFolder.create(FolderCreateMode.FAIL_IF_PARENT_IS_MISSING);
|
||||
final Folder longFolder = fs.folder("this will be a long filename after encryption");
|
||||
longFolder.create(FolderCreateMode.FAIL_IF_PARENT_IS_MISSING);
|
||||
|
||||
// the long name will produce a metadata file on the physical layer:
|
||||
LOG.debug("Physical file system:\n" + DirectoryPrinter.print(physicalFs));
|
||||
Assert.assertEquals(1, physicalFs.folder("m").folders().count());
|
||||
|
||||
// on the second layer all .lng files are resolved to their actual names:
|
||||
LOG.debug("Unlimited filename length:\n" + DirectoryPrinter.print(shorteningFs));
|
||||
DirectoryWalker.walk(shorteningFs, node -> {
|
||||
Assert.assertFalse(node.name().endsWith(".lng"));
|
||||
});
|
||||
|
||||
// on the third (cleartext layer) we have cleartext names on the root level:
|
||||
LOG.debug("Cleartext files:\n" + DirectoryPrinter.print(fs));
|
||||
Assert.assertArrayEquals(new String[] {"normal folder name", "this will be a long filename after encryption"}, fs.folders().map(Node::name).sorted().toArray());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Configuration status="WARN">
|
||||
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
|
||||
</Console>
|
||||
<Console name="StdErr" target="SYSTEM_ERR">
|
||||
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
|
||||
</Console>
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<Root level="DEBUG">
|
||||
<AppenderRef ref="Console" />
|
||||
<AppenderRef ref="StdErr" />
|
||||
</Root>
|
||||
</Loggers>
|
||||
|
||||
</Configuration>
|
||||
Reference in New Issue
Block a user