- Added throuput statistics

This commit is contained in:
Sebastian Stenzel
2014-12-11 19:46:57 +01:00
parent d0a420d6c0
commit 863b2ec423
19 changed files with 332 additions and 32 deletions

View File

@@ -21,7 +21,7 @@ public final class FileTimeUtils {
}
public static String toRfc1123String(FileTime time) {
final Temporal date = OffsetDateTime.ofInstant(time.toInstant(), ZoneOffset.UTC.normalized());
final Temporal date = OffsetDateTime.ofInstant(time.toInstant(), ZoneOffset.UTC);
return DateTimeFormatter.RFC_1123_DATE_TIME.format(date);
}

View File

@@ -42,7 +42,6 @@ import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.io.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.AbstractCryptor;
import org.cryptomator.crypto.CryptorIOSupport;
@@ -78,11 +77,6 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
*/
private static final int AES_KEY_LENGTH;
/**
*
*/
private static final byte[] EMPTY_MASTER_KEY = new byte[MASTER_KEY_LENGTH];
/**
* Jackson JSON-Mapper.
*/
@@ -92,7 +86,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
* The decrypted master key. Its lifecycle starts with {@link #randomData(int)} or {@link #encryptMasterKey(Path, CharSequence)}. Its
* lifecycle ends with {@link #swipeSensitiveData()}.
*/
private final byte[] masterKey = Arrays.copyOf(EMPTY_MASTER_KEY, MASTER_KEY_LENGTH);
private final byte[] masterKey = new byte[MASTER_KEY_LENGTH];
private static final int SIZE_OF_LONG = Long.SIZE / Byte.SIZE;
private static final int SIZE_OF_INT = Integer.SIZE / Byte.SIZE;
@@ -108,10 +102,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
}
}
/**
* Fills the masterkey with new random bytes.
*/
public void randomizeMasterKey() {
public Aes256Cryptor() {
SECURE_PRNG.setSeed(SECURE_PRNG.generateSeed(PRNG_SEED_LENGTH));
SECURE_PRNG.nextBytes(this.masterKey);
}
@@ -119,10 +110,8 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
/**
* Encrypts the current masterKey with the given password and writes the result to the given output stream.
*/
@Override
public void encryptMasterKey(OutputStream out, CharSequence password) throws IOException {
if (ArrayUtils.isEquals(this.masterKey, EMPTY_MASTER_KEY)) {
throw new IllegalStateException("Masterkey not yet initialized.");
}
try {
// derive key:
final byte[] userSalt = randomData(SALT_LENGTH);
@@ -157,6 +146,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
* @throws UnsupportedKeyLengthException If the masterkey has been encrypted with a higher key length than supported by the system. In
* this case Java JCE needs to be installed.
*/
@Override
public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException {
byte[] decrypted = new byte[0];
try {

View File

@@ -50,20 +50,11 @@ public class Aes256CryptorTest {
/* ------------------------------------------------------------------------------- */
@Test(expected = IllegalStateException.class)
public void testUninitializedMasterKey() throws IOException {
final String pw = "asd";
final Aes256Cryptor cryptor = new Aes256Cryptor();
final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
cryptor.encryptMasterKey(out, pw);
}
@Test
public void testCorrectPassword() throws IOException, WrongPasswordException, DecryptFailedException, UnsupportedKeyLengthException {
final String pw = "asd";
final Aes256Cryptor cryptor = new Aes256Cryptor();
final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
cryptor.randomizeMasterKey();
cryptor.encryptMasterKey(out, pw);
cryptor.swipeSensitiveData();
@@ -77,7 +68,6 @@ public class Aes256CryptorTest {
final String pw = "asd";
final Aes256Cryptor cryptor = new Aes256Cryptor();
final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
cryptor.randomizeMasterKey();
cryptor.encryptMasterKey(out, pw);
cryptor.swipeSensitiveData();
@@ -92,7 +82,6 @@ public class Aes256CryptorTest {
final String pw = "asd";
final Aes256Cryptor cryptor = new Aes256Cryptor();
final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
cryptor.randomizeMasterKey();
cryptor.encryptMasterKey(out, pw);
cryptor.swipeSensitiveData();
@@ -107,7 +96,6 @@ public class Aes256CryptorTest {
final String pw = "asd";
final Aes256Cryptor cryptor = new Aes256Cryptor();
final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
cryptor.randomizeMasterKey();
cryptor.encryptMasterKey(out, pw);
cryptor.swipeSensitiveData();
@@ -120,7 +108,6 @@ public class Aes256CryptorTest {
public void testEncryptionOfFilenames() throws IOException {
final CryptorIOSupport ioSupportMock = new CryptoIOSupportMock();
final Aes256Cryptor cryptor = new Aes256Cryptor();
cryptor.randomizeMasterKey();
// short path components
final String originalPath1 = "foo/bar/baz";

View File

@@ -22,5 +22,9 @@
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -1,3 +1,11 @@
/*******************************************************************************
* 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 org.cryptomator.crypto;
import java.util.HashSet;

View File

@@ -15,11 +15,31 @@ import java.nio.channels.SeekableByteChannel;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.Path;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
/**
* Provides access to cryptographic functions. All methods are threadsafe.
*/
public interface Cryptor extends SensitiveDataSwipeListener {
/**
* Encrypts the current masterKey with the given password and writes the result to the given output stream.
*/
void encryptMasterKey(OutputStream out, CharSequence password) throws IOException;
/**
* Reads the encrypted masterkey from the given input stream and decrypts it with the given password.
*
* @throws DecryptFailedException If the decryption failed for various reasons (including wrong password).
* @throws WrongPasswordException If the provided password was wrong. Note: Sometimes the algorithm itself fails due to a wrong
* password. In this case a DecryptFailedException will be thrown.
* @throws UnsupportedKeyLengthException If the masterkey has been encrypted with a higher key length than supported by the system. In
* this case Java JCE needs to be installed.
*/
void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException;
/**
* Encrypts each plaintext path component for its own.
*

View File

@@ -0,0 +1,26 @@
/*******************************************************************************
* 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 org.cryptomator.crypto;
/**
* Optional monitoring interface. If a cryptor implements this interface, it counts bytes de- and encrypted in a thread-safe manner.
*/
public interface CryptorIOSampling {
/**
* @return Number of encrypted bytes since the last reset.
*/
Long pollEncryptedBytes(boolean resetCounter);
/**
* @return Number of decrypted bytes since the last reset.
*/
Long pollDecryptedBytes(boolean resetCounter);
}

View File

@@ -1,3 +1,11 @@
/*******************************************************************************
* 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 org.cryptomator.crypto;
import java.io.IOException;

View File

@@ -0,0 +1,161 @@
package org.cryptomator.crypto;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
public class SamplingDecorator implements Cryptor, CryptorIOSampling {
private final Cryptor cryptor;
private final AtomicLong encryptedBytes;
private final AtomicLong decryptedBytes;
private SamplingDecorator(Cryptor cryptor) {
this.cryptor = cryptor;
encryptedBytes = new AtomicLong();
decryptedBytes = new AtomicLong();
}
public static Cryptor decorate(Cryptor cryptor) {
return new SamplingDecorator(cryptor);
}
@Override
public void swipeSensitiveData() {
cryptor.swipeSensitiveData();
}
@Override
public Long pollEncryptedBytes(boolean resetCounter) {
if (resetCounter) {
return encryptedBytes.getAndSet(0);
} else {
return encryptedBytes.get();
}
}
@Override
public Long pollDecryptedBytes(boolean resetCounter) {
if (resetCounter) {
return decryptedBytes.getAndSet(0);
} else {
return decryptedBytes.get();
}
}
/* Cryptor */
@Override
public void encryptMasterKey(OutputStream out, CharSequence password) throws IOException {
cryptor.encryptMasterKey(out, password);
}
@Override
public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException {
cryptor.decryptMasterKey(in, password);
}
@Override
public String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
encryptedBytes.addAndGet(StringUtils.length(cleartextPath));
return cryptor.encryptPath(cleartextPath, encryptedPathSep, cleartextPathSep, ioSupport);
}
@Override
public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
decryptedBytes.addAndGet(StringUtils.length(encryptedPath));
return cryptor.decryptPath(encryptedPath, encryptedPathSep, cleartextPathSep, ioSupport);
}
@Override
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException {
return cryptor.decryptedContentLength(encryptedFile);
}
@Override
public Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException {
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
return cryptor.decryptedFile(encryptedFile, countingInputStream);
}
@Override
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException {
final InputStream countingInputStream = new CountingInputStream(encryptedBytes, plaintextFile);
return cryptor.encryptFile(countingInputStream, encryptedFile);
}
@Override
public Filter<Path> getPayloadFilesFilter() {
return cryptor.getPayloadFilesFilter();
}
@Override
public void addSensitiveDataSwipeListener(SensitiveDataSwipeListener listener) {
cryptor.addSensitiveDataSwipeListener(listener);
}
@Override
public void removeSensitiveDataSwipeListener(SensitiveDataSwipeListener listener) {
cryptor.removeSensitiveDataSwipeListener(listener);
}
private class CountingInputStream extends InputStream {
private final InputStream in;
private final AtomicLong counter;
private CountingInputStream(AtomicLong counter, InputStream in) {
this.in = in;
this.counter = counter;
}
@Override
public int read() throws IOException {
int count = in.read();
counter.addAndGet(count);
return count;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int count = in.read(b, off, len);
counter.addAndGet(count);
return count;
}
}
private class CountingOutputStream extends OutputStream {
private final OutputStream out;
private final AtomicLong counter;
private CountingOutputStream(AtomicLong counter, OutputStream out) {
this.out = out;
this.counter = counter;
}
@Override
public void write(int b) throws IOException {
counter.incrementAndGet();
out.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
counter.addAndGet(len);
out.write(b, off, len);
}
}
}

View File

@@ -1,3 +1,11 @@
/*******************************************************************************
* 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 org.cryptomator.crypto;
public interface SensitiveDataSwipeListener {

View File

@@ -132,7 +132,6 @@ public class InitializeController implements Initializable {
OutputStream masterKeyOutputStream = null;
try {
masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
directory.getCryptor().randomizeMasterKey();
directory.getCryptor().encryptMasterKey(masterKeyOutputStream, password);
encryptExistingContents();
directory.getCryptor().swipeSensitiveData();

View File

@@ -11,26 +11,44 @@ package org.cryptomator.ui;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Label;
import javafx.util.Duration;
import org.cryptomator.crypto.CryptorIOSampling;
import org.cryptomator.ui.model.Directory;
public class UnlockedController implements Initializable {
private static final int IO_SAMPLING_STEPS = 100;
private static final double IO_SAMPLING_INTERVAL = 0.25;
private ResourceBundle rb;
private LockListener listener;
private Directory directory;
private Timeline ioAnimation;
@FXML
private Label messageLabel;
@FXML
private LineChart<Number, Number> ioGraph;
@FXML
private NumberAxis xAxis;
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
}
@FXML
@@ -43,6 +61,60 @@ public class UnlockedController implements Initializable {
}
}
// ****************************************
// IO Graph
// ****************************************
private void startIoSampling(final CryptorIOSampling sampler) {
final Series<Number, Number> decryptedBytes = new Series<>();
decryptedBytes.setName("decrypted");
final Series<Number, Number> encryptedBytes = new Series<>();
encryptedBytes.setName("encrypted");
ioGraph.getData().add(decryptedBytes);
ioGraph.getData().add(encryptedBytes);
ioAnimation = new Timeline();
ioAnimation.getKeyFrames().add(new KeyFrame(Duration.seconds(IO_SAMPLING_INTERVAL), new IoSamplingAnimationHandler(sampler, decryptedBytes, encryptedBytes)));
ioAnimation.setCycleCount(Animation.INDEFINITE);
ioAnimation.play();
}
private class IoSamplingAnimationHandler implements EventHandler<ActionEvent> {
private static final double BYTES_TO_MEGABYTES_FACTOR = IO_SAMPLING_INTERVAL / 1024.0 / 1024.0;
private final CryptorIOSampling sampler;
private final Series<Number, Number> decryptedBytes;
private final Series<Number, Number> encryptedBytes;
private int step = 0;
public IoSamplingAnimationHandler(CryptorIOSampling sampler, Series<Number, Number> decryptedBytes, Series<Number, Number> encryptedBytes) {
this.sampler = sampler;
this.decryptedBytes = decryptedBytes;
this.encryptedBytes = encryptedBytes;
}
@Override
public void handle(ActionEvent event) {
step++;
final double decryptedMb = sampler.pollDecryptedBytes(true) * BYTES_TO_MEGABYTES_FACTOR;
decryptedBytes.getData().add(new Data<Number, Number>(step, decryptedMb));
if (decryptedBytes.getData().size() > IO_SAMPLING_STEPS) {
decryptedBytes.getData().remove(0);
}
final double encrypteddMb = sampler.pollEncryptedBytes(true) * BYTES_TO_MEGABYTES_FACTOR;
encryptedBytes.getData().add(new Data<Number, Number>(step, encrypteddMb));
if (encryptedBytes.getData().size() > IO_SAMPLING_STEPS) {
encryptedBytes.getData().remove(0);
}
xAxis.setLowerBound(step - IO_SAMPLING_STEPS);
xAxis.setUpperBound(step);
}
}
/* Getter/Setter */
public Directory getDirectory() {
@@ -53,6 +125,12 @@ public class UnlockedController implements Initializable {
this.directory = directory;
final String msg = String.format(rb.getString("unlocked.messageLabel.runningOnPort"), directory.getServer().getPort());
messageLabel.setText(msg);
if (directory.getCryptor() instanceof CryptorIOSampling) {
startIoSampling((CryptorIOSampling) directory.getCryptor());
} else {
ioGraph.setVisible(false);
}
}
public LockListener getListener() {

View File

@@ -6,6 +6,8 @@ import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.SamplingDecorator;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.cryptomator.ui.MainApplication;
import org.cryptomator.ui.util.MasterKeyFilter;
@@ -26,7 +28,7 @@ public class Directory implements Serializable {
private static final Logger LOG = LoggerFactory.getLogger(Directory.class);
private final WebDAVServer server = new WebDAVServer();
private final Aes256Cryptor cryptor = new Aes256Cryptor();
private final Cryptor cryptor = SamplingDecorator.decorate(new Aes256Cryptor());
private final Path path;
private boolean unlocked;
private String unmountCommand;
@@ -97,7 +99,7 @@ public class Directory implements Serializable {
return path.getFileName().toString();
}
public Aes256Cryptor getCryptor() {
public Cryptor getCryptor() {
return cryptor;
}

View File

@@ -36,6 +36,7 @@ unlock.messageLabel.startServerFailed=Starting WebDAV server failed.
# unlocked.fxml
unlocked.messageLabel.runningOnPort=Vault is accessible via WebDAV on local port %d.
unlocked.button.lock=Lock vault
unlocked.ioGraph.yAxis.label=Throughput (MiB/s)
# tray icon

View File

@@ -14,6 +14,8 @@
<?import javafx.scene.text.*?>
<?import java.lang.String?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.chart.LineChart?>
<?import javafx.scene.chart.NumberAxis?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.UnlockedController" xmlns:fx="http://javafx.com/fxml">
@@ -32,6 +34,12 @@
<!-- Row 1 -->
<Button text="%unlocked.button.lock" defaultButton="true" GridPane.rowIndex="1" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#closeVault" focusTraversable="false"/>
<!-- Row 2 -->
<LineChart fx:id="ioGraph" GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" animated="false" createSymbols="false" prefHeight="300.0" legendVisible="true" legendSide="BOTTOM" verticalZeroLineVisible="false" verticalGridLinesVisible="false" horizontalGridLinesVisible="true">
<xAxis><NumberAxis fx:id="xAxis" forceZeroInRange="false" tickMarkVisible="false" minorTickVisible="false" tickLabelsVisible="false" autoRanging="false"/></xAxis>
<yAxis><NumberAxis label="%unlocked.ioGraph.yAxis.label" autoRanging="true" forceZeroInRange="true" /></yAxis>
</LineChart>
</children>
</GridPane>