Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for encrypted UserDefinedAttributeView #156

Draft
wants to merge 6 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,43 +13,34 @@
import org.cryptomator.cryptofs.Symlinks;
import org.cryptomator.cryptofs.common.ArrayUtils;
import org.cryptomator.cryptofs.common.CiphertextFileType;
import org.cryptomator.cryptofs.fh.OpenCryptoFile;
import org.cryptomator.cryptofs.fh.OpenCryptoFiles;

import java.io.IOException;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.FileAttributeView;
import java.util.Optional;

abstract sealed class AbstractCryptoFileAttributeView implements FileAttributeView
permits CryptoBasicFileAttributeView, CryptoFileOwnerAttributeView {
permits CryptoBasicFileAttributeView, CryptoFileOwnerAttributeView, CryptoUserDefinedFileAttributeView {

protected final CryptoPath cleartextPath;
private final CryptoPathMapper pathMapper;
protected final LinkOption[] linkOptions;
private final Symlinks symlinks;
private final OpenCryptoFiles openCryptoFiles;

protected AbstractCryptoFileAttributeView(CryptoPath cleartextPath, CryptoPathMapper pathMapper, LinkOption[] linkOptions, Symlinks symlinks, OpenCryptoFiles openCryptoFiles) {

protected AbstractCryptoFileAttributeView(CryptoPath cleartextPath, CryptoPathMapper pathMapper, LinkOption[] linkOptions, Symlinks symlinks) {
this.cleartextPath = cleartextPath;
this.pathMapper = pathMapper;
this.linkOptions = linkOptions;
this.symlinks = symlinks;
this.openCryptoFiles = openCryptoFiles;
}

protected <T extends FileAttributeView> T getCiphertextAttributeView(Class<T> delegateType) throws IOException {
Path ciphertextPath = getCiphertextPath(cleartextPath);
return ciphertextPath.getFileSystem().provider().getFileAttributeView(ciphertextPath, delegateType);
}

protected Optional<OpenCryptoFile> getOpenCryptoFile() throws IOException {
Path ciphertextPath = getCiphertextPath(cleartextPath);
return openCryptoFiles.get(ciphertextPath);
}

private Path getCiphertextPath(CryptoPath path) throws IOException {
protected Path getCiphertextPath(CryptoPath path) throws IOException {
CiphertextFileType type = pathMapper.getCiphertextFileType(path);
return switch (type) {
case SYMLINK:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.nio.file.attribute.FileAttributeView;
import java.nio.file.attribute.FileOwnerAttributeView;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.UserDefinedFileAttributeView;
import java.util.Map;
import java.util.Optional;

Expand Down Expand Up @@ -42,6 +43,12 @@ abstract class AttributeViewModule {
@AttributeViewScoped
public abstract FileAttributeView provideFileOwnerAttributeView(CryptoFileOwnerAttributeView view);

@Binds
@IntoMap
@ClassKey(UserDefinedFileAttributeView.class)
@AttributeViewScoped
public abstract FileAttributeView provideUserDefinedAttributeView(CryptoUserDefinedFileAttributeView view);

@Provides
@AttributeViewScoped
public static Optional<FileAttributeView> provideAttributeView(Map<Class<?>, Provider<FileAttributeView>> providers, Class<? extends FileAttributeView> requestedType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
import java.nio.file.attribute.FileAttributeView;
import java.nio.file.attribute.FileOwnerAttributeView;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.UserDefinedFileAttributeView;
import java.util.Arrays;
import java.util.Optional;

public enum AttributeViewType {
BASIC(BasicFileAttributeView.class, "basic"),
OWNER(FileOwnerAttributeView.class, "owner"),
POSIX(PosixFileAttributeView.class, "posix"),
DOS(DosFileAttributeView.class, "dos");
DOS(DosFileAttributeView.class, "dos"),
USER(UserDefinedFileAttributeView.class, "user");

private final Class<? extends FileAttributeView> type;
private final String viewName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,30 @@
import org.cryptomator.cryptofs.CryptoPathMapper;
import org.cryptomator.cryptofs.ReadonlyFlag;
import org.cryptomator.cryptofs.Symlinks;
import org.cryptomator.cryptofs.fh.OpenCryptoFile;
import org.cryptomator.cryptofs.fh.OpenCryptoFiles;

import javax.inject.Inject;
import java.io.IOException;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.Optional;

@AttributeViewScoped
sealed class CryptoBasicFileAttributeView extends AbstractCryptoFileAttributeView implements BasicFileAttributeView
permits CryptoDosFileAttributeView, CryptoPosixFileAttributeView {
sealed class CryptoBasicFileAttributeView extends AbstractCryptoFileAttributeView implements BasicFileAttributeView permits CryptoDosFileAttributeView, CryptoPosixFileAttributeView {

private final OpenCryptoFiles openCryptoFiles;
protected final AttributeProvider fileAttributeProvider;
protected final ReadonlyFlag readonlyFlag;


@Inject
public CryptoBasicFileAttributeView(CryptoPath cleartextPath, CryptoPathMapper pathMapper, LinkOption[] linkOptions, Symlinks symlinks, OpenCryptoFiles openCryptoFiles, AttributeProvider fileAttributeProvider, ReadonlyFlag readonlyFlag) {
super(cleartextPath, pathMapper, linkOptions, symlinks, openCryptoFiles);
super(cleartextPath, pathMapper, linkOptions, symlinks);
this.openCryptoFiles = openCryptoFiles;
this.fileAttributeProvider = fileAttributeProvider;
this.readonlyFlag = readonlyFlag;
}
Expand All @@ -45,6 +50,11 @@ public BasicFileAttributes readAttributes() throws IOException {
return fileAttributeProvider.readAttributes(cleartextPath, BasicFileAttributes.class, linkOptions);
}

private Optional<OpenCryptoFile> getOpenCryptoFile() throws IOException {
Path ciphertextPath = getCiphertextPath(cleartextPath);
return openCryptoFiles.get(ciphertextPath);
}

@Override
public void setTimes(FileTime lastModifiedTime, FileTime lastAccessTime, FileTime createTime) throws IOException {
readonlyFlag.assertWritable();
Expand All @@ -53,5 +63,4 @@ public void setTimes(FileTime lastModifiedTime, FileTime lastAccessTime, FileTim
getOpenCryptoFile().ifPresent(file -> file.setLastModifiedTime(lastModifiedTime));
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ final class CryptoFileOwnerAttributeView extends AbstractCryptoFileAttributeView
private final ReadonlyFlag readonlyFlag;

@Inject
public CryptoFileOwnerAttributeView(CryptoPath cleartextPath, CryptoPathMapper pathMapper, LinkOption[] linkOptions, Symlinks symlinks, OpenCryptoFiles openCryptoFiles, ReadonlyFlag readonlyFlag) {
super(cleartextPath, pathMapper, linkOptions, symlinks, openCryptoFiles);
public CryptoFileOwnerAttributeView(CryptoPath cleartextPath, CryptoPathMapper pathMapper, LinkOption[] linkOptions, Symlinks symlinks, ReadonlyFlag readonlyFlag) {
super(cleartextPath, pathMapper, linkOptions, symlinks);
this.readonlyFlag = readonlyFlag;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package org.cryptomator.cryptofs.attr;

import com.google.common.io.BaseEncoding;
import org.cryptomator.cryptofs.CryptoPath;
import org.cryptomator.cryptofs.CryptoPathMapper;
import org.cryptomator.cryptofs.Symlinks;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel;
import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel;

import javax.inject.Inject;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.file.LinkOption;
import java.nio.file.attribute.UserDefinedFileAttributeView;
import java.util.List;

@AttributeViewScoped
final class CryptoUserDefinedFileAttributeView extends AbstractCryptoFileAttributeView implements UserDefinedFileAttributeView {

private static final String PREFIX = "c9r.";

private final Cryptor cryptor;

@Inject
public CryptoUserDefinedFileAttributeView(CryptoPath cleartextPath, CryptoPathMapper pathMapper, LinkOption[] linkOptions, Symlinks symlinks, Cryptor cryptor) {
super(cleartextPath, pathMapper, linkOptions, symlinks);
this.cryptor = cryptor;
}

@Override
public String name() {
return "user"; // as per contract
}

@Override
public List<String> list() throws IOException {
var ciphertextNames = getCiphertextAttributeView(UserDefinedFileAttributeView.class).list();
return ciphertextNames.stream().filter(s -> s.startsWith(PREFIX)).map(this::decryptName).toList();
}

@Override
public int size(String cleartextName) throws IOException {
var ciphertextName = encryptName(cleartextName);
var totalCiphertextSize = getCiphertextAttributeView(UserDefinedFileAttributeView.class).size(ciphertextName);
var ciphertextBodySize = totalCiphertextSize - cryptor.fileHeaderCryptor().headerSize();
return (int) cryptor.fileContentCryptor().cleartextSize(ciphertextBodySize);
}

@Override
public int read(String cleartextName, ByteBuffer dst) throws IOException {
var ciphertextName = encryptName(cleartextName);
var view = getCiphertextAttributeView(UserDefinedFileAttributeView.class);
int size = view.size(ciphertextName);
var buf = ByteBuffer.allocate(size);
view.read(ciphertextName, buf);
buf.flip();

try (var in = new ByteBufferInputStream(buf); //
var ciphertextChannel = Channels.newChannel(in); //
var cleartextChannel = new DecryptingReadableByteChannel(ciphertextChannel, cryptor, true)) {
return cleartextChannel.read(dst);
}
}

@Override
public int write(String cleartextName, ByteBuffer src) throws IOException {
var ciphertextName = encryptName(cleartextName);
var out = new ByteArrayOutputStream();
final int size;
try (var ciphertextChannel = Channels.newChannel(out); //
var cleartextChannel = new EncryptingWritableByteChannel(ciphertextChannel, cryptor)) {
size = cleartextChannel.write(src);
} // close to flush cached ciphertext
var buf = ByteBuffer.wrap(out.toByteArray());
getCiphertextAttributeView(UserDefinedFileAttributeView.class).write(ciphertextName, buf);
return size;
}

@Override
public void delete(String cleartextName) throws IOException {
var ciphertextName = encryptName(cleartextName);
getCiphertextAttributeView(UserDefinedFileAttributeView.class).delete(ciphertextName);
}

private String encryptName(String cleartextName) {
return PREFIX + cryptor.fileNameCryptor().encryptFilename(BaseEncoding.base64Url(), cleartextName);
}

private String decryptName(String ciphertextName) {
assert ciphertextName.startsWith(PREFIX);
return cryptor.fileNameCryptor().decryptFilename(BaseEncoding.base64Url(), ciphertextName.substring(PREFIX.length()));
}

// taken from https://stackoverflow.com/a/6603018/4014509
private static class ByteBufferInputStream extends InputStream {

ByteBuffer buf;

public ByteBufferInputStream(ByteBuffer buf) {
this.buf = buf;
}

public int read() throws IOException {
if (!buf.hasRemaining()) {
return -1;
}
return buf.get() & 0xFF;
}

public int read(byte[] bytes, int off, int len) throws IOException {
if (!buf.hasRemaining()) {
return -1;
}

len = Math.min(len, buf.remaining());
buf.get(bytes, off, len);
return len;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ public class CryptoFileOwnerAttributeViewTest {
private CryptoPath cleartextPath = mock(CryptoPath.class);
private CryptoPathMapper pathMapper = mock(CryptoPathMapper.class);
private Symlinks symlinks = mock(Symlinks.class);
private OpenCryptoFiles openCryptoFiles = mock(OpenCryptoFiles.class);
private ReadonlyFlag readonlyFlag = mock(ReadonlyFlag.class);

private CryptoFileOwnerAttributeView inTest;
Expand All @@ -61,7 +60,7 @@ public void setup() throws IOException {
when(linkCiphertextPath.getSymlinkFilePath()).thenReturn(linkCiphertextRawPath);
when(ciphertextPath.getFilePath()).thenReturn(ciphertextRawPath);

inTest = new CryptoFileOwnerAttributeView(cleartextPath, pathMapper, new LinkOption[]{}, symlinks, openCryptoFiles, readonlyFlag);
inTest = new CryptoFileOwnerAttributeView(cleartextPath, pathMapper, new LinkOption[]{}, symlinks, readonlyFlag);
}

@Test
Expand Down Expand Up @@ -91,7 +90,7 @@ public void testSetOwnerDelegates() throws IOException {
public void testSetOwnerOfSymlinkNoFollow() throws IOException {
UserPrincipal principal = mock(UserPrincipal.class);

CryptoFileOwnerAttributeView view = new CryptoFileOwnerAttributeView(link, pathMapper, new LinkOption[]{LinkOption.NOFOLLOW_LINKS}, symlinks, openCryptoFiles, readonlyFlag);
CryptoFileOwnerAttributeView view = new CryptoFileOwnerAttributeView(link, pathMapper, new LinkOption[]{LinkOption.NOFOLLOW_LINKS}, symlinks, readonlyFlag);
view.setOwner(principal);

verify(linkDelegate).setOwner(principal);
Expand All @@ -101,7 +100,7 @@ public void testSetOwnerOfSymlinkNoFollow() throws IOException {
public void testSetOwnerOfSymlinkFollow() throws IOException {
UserPrincipal principal = mock(UserPrincipal.class);

CryptoFileOwnerAttributeView view = new CryptoFileOwnerAttributeView(link, pathMapper, new LinkOption[]{}, symlinks, openCryptoFiles, readonlyFlag);
CryptoFileOwnerAttributeView view = new CryptoFileOwnerAttributeView(link, pathMapper, new LinkOption[]{}, symlinks, readonlyFlag);
view.setOwner(principal);

verify(delegate).setOwner(principal);
Expand Down
Loading