- Allows the user to configure optional MAC verification before decrypting content (Fixes #17)

This commit is contained in:
Sebastian Stenzel
2015-01-07 20:00:09 +01:00
parent b6b3360325
commit deb10c1256
15 changed files with 117 additions and 49 deletions

View File

@@ -45,7 +45,7 @@ public final class WebDavServer {
* @param cryptor A fully initialized cryptor instance ready to en- or decrypt streams.
* @return <code>true</code> upon success
*/
public synchronized boolean start(final String workDir, final Cryptor cryptor) {
public synchronized boolean start(final String workDir, final boolean checkFileIntegrity, final Cryptor cryptor) {
final ServerConnector connector = new ServerConnector(server);
connector.setHost(LOCALHOST);
@@ -53,7 +53,7 @@ public final class WebDavServer {
final String servletPathSpec = "/*";
final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.addServlet(getWebDavServletHolder(workDir, contextPath, cryptor), servletPathSpec);
context.addServlet(getWebDavServletHolder(workDir, contextPath, checkFileIntegrity, cryptor), servletPathSpec);
context.setContextPath(contextPath);
server.setHandler(context);
@@ -82,10 +82,11 @@ public final class WebDavServer {
return server.isStopped();
}
private ServletHolder getWebDavServletHolder(final String workDir, final String contextPath, final Cryptor cryptor) {
private ServletHolder getWebDavServletHolder(final String workDir, final String contextPath, final boolean checkFileIntegrity, final Cryptor cryptor) {
final ServletHolder result = new ServletHolder("Cryptomator-WebDAV-Servlet", new WebDavServlet(cryptor));
result.setInitParameter(WebDavServlet.CFG_FS_ROOT, workDir);
result.setInitParameter(WebDavServlet.CFG_HTTP_ROOT, contextPath);
result.setInitParameter(WebDavServlet.CFG_CHECK_FILE_INTEGRITY, Boolean.toString(checkFileIntegrity));
return result;
}

View File

@@ -34,9 +34,11 @@ class WebDavResourceFactory implements DavResourceFactory {
private final LockManager lockManager = new SimpleLockManager();
private final Cryptor cryptor;
private final boolean checkFileIntegrity;
WebDavResourceFactory(Cryptor cryptor) {
WebDavResourceFactory(Cryptor cryptor, boolean checkFileIntegrity) {
this.cryptor = cryptor;
this.checkFileIntegrity = checkFileIntegrity;
}
@Override
@@ -70,11 +72,11 @@ class WebDavResourceFactory implements DavResourceFactory {
}
private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, DavServletRequest request) {
return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor);
return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor, checkFileIntegrity);
}
private EncryptedFile createFile(DavResourceLocator locator, DavSession session) {
return new EncryptedFile(this, locator, session, lockManager, cryptor);
return new EncryptedFile(this, locator, session, lockManager, cryptor, checkFileIntegrity);
}
private EncryptedDir createDirectory(DavResourceLocator locator, DavSession session) {

View File

@@ -22,8 +22,9 @@ import org.cryptomator.crypto.Cryptor;
public class WebDavServlet extends AbstractWebdavServlet {
private static final long serialVersionUID = 7965170007048673022L;
public static final String CFG_FS_ROOT = "oce.fs.root";
public static final String CFG_HTTP_ROOT = "oce.http.root";
public static final String CFG_FS_ROOT = "cfg.fs.root";
public static final String CFG_HTTP_ROOT = "cfg.http.root";
public static final String CFG_CHECK_FILE_INTEGRITY = "cfg.checkFileIntegrity";
private DavSessionProvider davSessionProvider;
private DavLocatorFactory davLocatorFactory;
private DavResourceFactory davResourceFactory;
@@ -42,9 +43,10 @@ public class WebDavServlet extends AbstractWebdavServlet {
final String fsRoot = config.getInitParameter(CFG_FS_ROOT);
final String httpRoot = config.getInitParameter(CFG_HTTP_ROOT);
final boolean checkFileIntegrity = Boolean.parseBoolean(config.getInitParameter(CFG_CHECK_FILE_INTEGRITY));
this.davLocatorFactory = new WebDavLocatorFactory(fsRoot, httpRoot, cryptor);
this.davResourceFactory = new WebDavResourceFactory(cryptor);
this.davResourceFactory = new WebDavResourceFactory(cryptor, checkFileIntegrity);
}
@Override

View File

@@ -40,8 +40,11 @@ public class EncryptedFile extends AbstractEncryptedNode {
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFile.class);
public EncryptedFile(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
protected final boolean checkIntegrity;
public EncryptedFile(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, boolean checkIntegrity) {
super(factory, locator, session, lockManager, cryptor);
this.checkIntegrity = checkIntegrity;
}
@Override
@@ -73,6 +76,9 @@ public class EncryptedFile extends AbstractEncryptedNode {
SeekableByteChannel channel = null;
try {
channel = Files.newByteChannel(path, StandardOpenOption.READ);
if (checkIntegrity && !cryptor.authenticateContent(channel)) {
throw new DecryptFailedException("File content compromised: " + path.toString());
}
outputContext.setContentLength(cryptor.decryptedContentLength(channel));
if (outputContext.hasStream()) {
cryptor.decryptedFile(channel, outputContext.getOutputStream());

View File

@@ -50,8 +50,8 @@ public class EncryptedFilePart extends EncryptedFile {
private final Set<Pair<Long, Long>> requestedContentRanges = new HashSet<Pair<Long, Long>>();
public EncryptedFilePart(DavResourceFactory factory, DavResourceLocator locator, DavSession session, DavServletRequest request, LockManager lockManager, Cryptor cryptor) {
super(factory, locator, session, lockManager, cryptor);
public EncryptedFilePart(DavResourceFactory factory, DavResourceLocator locator, DavSession session, DavServletRequest request, LockManager lockManager, Cryptor cryptor, boolean checkIntegrity) {
super(factory, locator, session, lockManager, cryptor, checkIntegrity);
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
if (rangeHeader == null) {
throw new IllegalArgumentException("HTTP request doesn't contain a range header");
@@ -116,6 +116,9 @@ public class EncryptedFilePart extends EncryptedFile {
SeekableByteChannel channel = null;
try {
channel = Files.newByteChannel(path, StandardOpenOption.READ);
if (checkIntegrity && !cryptor.authenticateContent(channel)) {
throw new DecryptFailedException("File content compromised: " + path.toString());
}
final Long fileSize = cryptor.decryptedContentLength(channel);
final Pair<Long, Long> range = getUnionRange(fileSize);
final Long rangeLength = range.getRight() - range.getLeft() + 1;

View File

@@ -7,7 +7,7 @@ public class DecryptFailedException extends StorageCryptingException {
super("Decryption failed.", t);
}
protected DecryptFailedException(String msg) {
public DecryptFailedException(String msg) {
super(msg);
}
}

View File

@@ -24,6 +24,7 @@ import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
@@ -55,6 +56,9 @@ public class UnlockController implements Initializable {
@FXML
private SecPasswordField passwordField;
@FXML
private CheckBox checkIntegrity;
@FXML
private Button unlockButton;
@@ -96,6 +100,7 @@ public class UnlockController implements Initializable {
try {
progressIndicator.setVisible(true);
masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ);
directory.setVerifyFileIntegrity(checkIntegrity.isSelected());
directory.getCryptor().decryptMasterKey(masterKeyInputStream, password);
if (!directory.startServer()) {
messageLabel.setText(rb.getString("unlock.messageLabel.startServerFailed"));
@@ -170,6 +175,7 @@ public class UnlockController implements Initializable {
public void setDirectory(Directory directory) {
this.directory = directory;
this.findExistingUsernames();
this.checkIntegrity.setSelected(directory.shouldVerifyFileIntegrity());
}
public UnlockListener getListener() {

View File

@@ -34,7 +34,7 @@ public class Directory implements Serializable {
private final Cryptor cryptor = SamplingDecorator.decorate(new Aes256Cryptor());
private final ObjectProperty<Boolean> unlocked = new SimpleObjectProperty<Boolean>(this, "unlocked", Boolean.FALSE);
private final Path path;
// private boolean unlocked;
private boolean verifyFileIntegrity;
private WebDavMount webDavMount;
private final Runnable shutdownTask = new ShutdownTask();
@@ -50,7 +50,7 @@ public class Directory implements Serializable {
}
public synchronized boolean startServer() {
if (server.start(path.toString(), cryptor)) {
if (server.start(path.toString(), verifyFileIntegrity, cryptor)) {
MainApplication.addShutdownTask(shutdownTask);
return true;
} else {
@@ -96,6 +96,14 @@ public class Directory implements Serializable {
return path;
}
public boolean shouldVerifyFileIntegrity() {
return verifyFileIntegrity;
}
public void setVerifyFileIntegrity(boolean verifyFileIntegrity) {
this.verifyFileIntegrity = verifyFileIntegrity;
}
/**
* @return Directory name without preceeding path components
*/

View File

@@ -17,7 +17,10 @@ public class DirectoryDeserializer extends JsonDeserializer<Directory> {
final JsonNode node = jp.readValueAsTree();
final String pathStr = node.get("path").asText();
final Path path = FileSystems.getDefault().getPath(pathStr);
return new Directory(path);
final Directory dir = new Directory(path);
final boolean verifyFileIntegrity = node.has("checkIntegrity") ? node.get("checkIntegrity").asBoolean() : false;
dir.setVerifyFileIntegrity(verifyFileIntegrity);
return dir;
}
}

View File

@@ -13,6 +13,7 @@ public class DirectorySerializer extends JsonSerializer<Directory> {
public void serialize(Directory value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
jgen.writeStartObject();
jgen.writeStringField("path", value.getPath().toString());
jgen.writeBooleanField("checkIntegrity", value.shouldVerifyFileIntegrity());
jgen.writeEndObject();
}

View File

@@ -28,7 +28,7 @@ import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.databind.ObjectMapper;
@JsonPropertyOrder(value = {"webdavWorkDir"})
@JsonPropertyOrder(value = {"directories"})
public class Settings implements Serializable {
private static final long serialVersionUID = 7609959894417878744L;
@@ -55,7 +55,6 @@ public class Settings implements Serializable {
}
private List<Directory> directories;
private String username;
private Settings() {
// private constructor
@@ -107,12 +106,4 @@ public class Settings implements Serializable {
this.directories = directories;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}

View File

@@ -175,8 +175,6 @@
.button,
.toggle-button,
.radio-button > .radio,
.check-box > .box,
.menu-button,
.choice-box,
.color-picker.split-button > .color-picker-label,
@@ -194,7 +192,6 @@
.button:hover,
.toggle-button:hover,
.radio-button:hover > .radio,
.check-box:hover > .box,
.menu-button:hover,
.split-menu-button > .label:hover,
.split-menu-button > .arrow-button:hover,
@@ -212,8 +209,6 @@
.button:armed,
.button:default:armed,
.toggle-button:armed,
.radio-button:armed > .radio,
.check-box:armed .box,
.menu-button:armed,
.split-menu-button:armed > .label,
.split-menu-button > .arrow-button:pressed,
@@ -228,8 +223,6 @@
}
.button:focused,
.toggle-button:focused,
.radio-button:focused > .radio,
.check-box:focused > .box,
.menu-button:focused,
.choice-box:focused,
.color-picker.split-button:focused > .color-picker-label {
@@ -242,8 +235,6 @@
.button:disabled,
.toggle-button:disabled,
.radio-button:disabled,
.check-box:disabled,
.hyperlink:disabled,
.menu-button:disabled,
.split-menu-button:disabled,
@@ -270,8 +261,6 @@
.button:show-mnemonics .mnemonic-underline,
.toggle-button:show-mnemonics .mnemonic-underline,
.radio-button:show-mnemonics .mnemonic-underline,
.check-box:show-mnemonics .mnemonic-underline,
.hyperlink:show-mnemonics > .mnemonic-underline,
.split-menu-button:show-mnemonics > .mnemonic-underline,
.menu-button:show-mnemonics > .mnemonic-underline {
@@ -334,6 +323,35 @@
-fx-text-fill: -fx-mid-text-color;
}
/*******************************************************************************
* *
* CheckBox *
* *
******************************************************************************/
.check-box {
-fx-label-padding: 0 0 0 3px;
-fx-text-fill: -fx-text-background-color;
}
.check-box > .box {
-fx-padding: 3px;
-fx-background-color: linear-gradient(to bottom, #A5A5A5 0%, #B8B8B8 100%), #F3F3F3, #FFFFFF;
-fx-background-radius: 2.5, 2.5, 2.5;
-fx-background-insets: 0, 1, 2 1 1 1;
}
.check-box > .box > .mark {
-fx-background-color: transparent;
-fx-padding: 4px;
-fx-shape: "M-1,4, L-1,5.5 L3.5,8.5 L9,0 L9,-1 L7,-1 L3,6 L1,4 Z";
}
.check-box:selected > .box {
-fx-background-color: #2C90FC, #3B99FC;
-fx-background-insets: 0, 1;
}
.check-box:selected > .box > .mark {
-fx-background-color: white;
}
/*******************************************************************************
* *
* ToolBar *

View File

@@ -174,8 +174,6 @@
.button,
.toggle-button,
.radio-button > .radio,
.check-box > .box,
.menu-button,
.choice-box,
.color-picker.split-button > .color-picker-label,
@@ -192,8 +190,6 @@
}
.button:hover,
.toggle-button:hover,
.radio-button:hover > .radio,
.check-box:hover > .box,
.menu-button:hover,
.split-menu-button > .label:hover,
.split-menu-button > .arrow-button:hover,
@@ -208,8 +204,6 @@
.button:armed,
.button:default:armed,
.toggle-button:armed,
.radio-button:armed > .radio,
.check-box:armed .box,
.menu-button:armed,
.split-menu-button:armed > .label,
.split-menu-button > .arrow-button:pressed,
@@ -235,8 +229,6 @@
.button:disabled,
.toggle-button:disabled,
.radio-button:disabled,
.check-box:disabled,
.hyperlink:disabled,
.menu-button:disabled,
.split-menu-button:disabled,
@@ -261,8 +253,6 @@
.button:show-mnemonics .mnemonic-underline,
.toggle-button:show-mnemonics .mnemonic-underline,
.radio-button:show-mnemonics .mnemonic-underline,
.check-box:show-mnemonics .mnemonic-underline,
.hyperlink:show-mnemonics > .mnemonic-underline,
.split-menu-button:show-mnemonics > .mnemonic-underline,
.menu-button:show-mnemonics > .mnemonic-underline {
@@ -323,6 +313,36 @@
-fx-text-fill: -fx-mid-text-color;
}
/*******************************************************************************
* *
* CheckBox *
* *
******************************************************************************/
/* TODO win L&F */
.check-box {
-fx-label-padding: 0 0 0 3px;
-fx-text-fill: -fx-text-background-color;
}
.check-box > .box {
-fx-padding: 3px;
-fx-background-color: linear-gradient(to bottom, #A5A5A5 0%, #B8B8B8 100%), #F3F3F3, #FFFFFF;
-fx-background-radius: 2.5, 2.5, 2.5;
-fx-background-insets: 0, 1, 2 1 1 1;
}
.check-box > .box > .mark {
-fx-background-color: transparent;
-fx-padding: 4px;
-fx-shape: "M-1,4, L-1,5.5 L3.5,8.5 L9,0 L9,-1 L7,-1 L3,6 L1,4 Z";
}
.check-box:selected > .box {
-fx-background-color: #2C90FC, #3B99FC;
-fx-background-insets: 0, 1;
}
.check-box:selected > .box > .mark {
-fx-background-color: white;
}
/*******************************************************************************
* *
* ToolBar *

View File

@@ -16,6 +16,7 @@
<?import org.cryptomator.ui.controls.SecPasswordField?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ProgressIndicator?>
<?import javafx.scene.control.CheckBox?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.UnlockController" xmlns:fx="http://javafx.com/fxml">
@@ -38,10 +39,14 @@
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="1" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
<!-- Row 2 -->
<Button fx:id="unlockButton" text="%unlock.button.unlock" defaultButton="true" GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickUnlockButton"/>
<Label text="%unlock.label.checkIntegrity" GridPane.rowIndex="2" GridPane.columnIndex="0" />
<CheckBox fx:id="checkIntegrity" wrapText="true" text="%unlock.checkbox.checkIntegrity" GridPane.rowIndex="2" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
<!-- Row 3 -->
<ProgressIndicator progress="-1" fx:id="progressIndicator" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="CENTER" visible="false"/>
<Button fx:id="unlockButton" text="%unlock.button.unlock" defaultButton="true" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickUnlockButton"/>
<!-- Row 4-->
<ProgressIndicator progress="-1" fx:id="progressIndicator" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="CENTER" visible="false"/>
<!-- Row 5 -->
<Label fx:id="messageLabel" GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="2" />

View File

@@ -32,6 +32,8 @@ initialize.alert.directoryIsNotEmpty.content=All existing files inside this dire
# unlock.fxml
unlock.label.username=Username
unlock.label.password=Password
unlock.label.checkIntegrity=File integrity
unlock.checkbox.checkIntegrity=Verify checksums (slower, but detects manipulation)
unlock.button.unlock=Unlock vault
unlock.errorMessage.wrongPassword=Wrong password.
unlock.errorMessage.decryptionFailed=Decryption failed.