From ae7cd0692847daf984211f99dfe0ef6e71a1d0a7 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 4 Jan 2023 16:28:07 +0100 Subject: [PATCH 1/6] moving field to subclass (not required elsewhere) --- .../attr/AbstractCryptoFileAttributeView.java | 17 ++++------------- .../attr/CryptoBasicFileAttributeView.java | 17 +++++++++++++---- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java b/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java index 82563853..62e00e11 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java @@ -13,30 +13,26 @@ 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, CryptoUserDefinedAttributeView { 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 getCiphertextAttributeView(Class delegateType) throws IOException { @@ -44,12 +40,7 @@ protected T getCiphertextAttributeView(Class de return ciphertextPath.getFileSystem().provider().getFileAttributeView(ciphertextPath, delegateType); } - protected Optional 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: diff --git a/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributeView.java b/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributeView.java index 1e44bfa1..1a5337d6 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributeView.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributeView.java @@ -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; } @@ -45,6 +50,11 @@ public BasicFileAttributes readAttributes() throws IOException { return fileAttributeProvider.readAttributes(cleartextPath, BasicFileAttributes.class, linkOptions); } + private Optional 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(); @@ -53,5 +63,4 @@ public void setTimes(FileTime lastModifiedTime, FileTime lastAccessTime, FileTim getOpenCryptoFile().ifPresent(file -> file.setLastModifiedTime(lastModifiedTime)); } } - } From 52aaa8aba17427f5ea48487c9367034c767149da Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 4 Jan 2023 16:28:48 +0100 Subject: [PATCH 2/6] first draft for encryption of user-defined attributes --- .../cryptofs/attr/AttributeViewModule.java | 6 + .../cryptofs/attr/AttributeViewType.java | 4 +- .../attr/CryptoUserDefinedAttributeView.java | 122 ++++++++++++++++++ 3 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/cryptomator/cryptofs/attr/CryptoUserDefinedAttributeView.java diff --git a/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewModule.java b/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewModule.java index d41836c1..5490e119 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewModule.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewModule.java @@ -42,6 +42,12 @@ abstract class AttributeViewModule { @AttributeViewScoped public abstract FileAttributeView provideFileOwnerAttributeView(CryptoFileOwnerAttributeView view); + @Binds + @IntoMap + @ClassKey(CryptoUserDefinedAttributeView.class) + @AttributeViewScoped + public abstract FileAttributeView provideUserDefinedAttributeView(CryptoUserDefinedAttributeView view); + @Provides @AttributeViewScoped public static Optional provideAttributeView(Map, Provider> providers, Class requestedType) { diff --git a/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewType.java b/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewType.java index 7d7007b2..d3809cb3 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewType.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewType.java @@ -5,6 +5,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.Arrays; import java.util.Optional; @@ -12,7 +13,8 @@ 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 type; private final String viewName; diff --git a/src/main/java/org/cryptomator/cryptofs/attr/CryptoUserDefinedAttributeView.java b/src/main/java/org/cryptomator/cryptofs/attr/CryptoUserDefinedAttributeView.java new file mode 100644 index 00000000..0908c011 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/attr/CryptoUserDefinedAttributeView.java @@ -0,0 +1,122 @@ +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 CryptoUserDefinedAttributeView extends AbstractCryptoFileAttributeView implements UserDefinedFileAttributeView { + + private static final String PREFIX = "c9r."; + + private final Cryptor cryptor; + + @Inject + public CryptoUserDefinedAttributeView(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 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); + return getCiphertextAttributeView(UserDefinedFileAttributeView.class).size(ciphertextName); + } + + @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); + try (var out = new ByteArrayOutputStream(); + var ciphertextChannel = Channels.newChannel(out); // + var cleartextChannel = new EncryptingWritableByteChannel(ciphertextChannel, cryptor)) { + int size = cleartextChannel.write(src); + 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; + } + } + +} From 8d22b3656eae250949c59cffba5d4837b387833a Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 4 Jan 2023 16:47:32 +0100 Subject: [PATCH 3/6] fix for error in ae7cd06 --- .../cryptofs/attr/CryptoFileOwnerAttributeView.java | 4 ++-- .../cryptofs/attr/CryptoFileOwnerAttributeViewTest.java | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeView.java b/src/main/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeView.java index 42678036..21677180 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeView.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeView.java @@ -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; } diff --git a/src/test/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeViewTest.java b/src/test/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeViewTest.java index 1ad616ad..5ab35376 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeViewTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeViewTest.java @@ -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; @@ -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 @@ -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); @@ -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); From f5890d3a61dda84e4581763f7bca6ebef673592d Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 5 Jan 2023 13:10:23 +0100 Subject: [PATCH 4/6] renamed class --- .../cryptofs/attr/AbstractCryptoFileAttributeView.java | 2 +- .../org/cryptomator/cryptofs/attr/AttributeViewModule.java | 4 ++-- ...teView.java => CryptoUserDefinedFileAttributeView.java} | 7 ++++--- 3 files changed, 7 insertions(+), 6 deletions(-) rename src/main/java/org/cryptomator/cryptofs/attr/{CryptoUserDefinedAttributeView.java => CryptoUserDefinedFileAttributeView.java} (87%) diff --git a/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java b/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java index 62e00e11..3b89dabe 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java @@ -20,7 +20,7 @@ import java.nio.file.attribute.FileAttributeView; abstract sealed class AbstractCryptoFileAttributeView implements FileAttributeView - permits CryptoBasicFileAttributeView, CryptoFileOwnerAttributeView, CryptoUserDefinedAttributeView { + permits CryptoBasicFileAttributeView, CryptoFileOwnerAttributeView, CryptoUserDefinedFileAttributeView { protected final CryptoPath cleartextPath; private final CryptoPathMapper pathMapper; diff --git a/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewModule.java b/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewModule.java index 5490e119..1c04763e 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewModule.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewModule.java @@ -44,9 +44,9 @@ abstract class AttributeViewModule { @Binds @IntoMap - @ClassKey(CryptoUserDefinedAttributeView.class) + @ClassKey(CryptoUserDefinedFileAttributeView.class) @AttributeViewScoped - public abstract FileAttributeView provideUserDefinedAttributeView(CryptoUserDefinedAttributeView view); + public abstract FileAttributeView provideUserDefinedAttributeView(CryptoUserDefinedFileAttributeView view); @Provides @AttributeViewScoped diff --git a/src/main/java/org/cryptomator/cryptofs/attr/CryptoUserDefinedAttributeView.java b/src/main/java/org/cryptomator/cryptofs/attr/CryptoUserDefinedFileAttributeView.java similarity index 87% rename from src/main/java/org/cryptomator/cryptofs/attr/CryptoUserDefinedAttributeView.java rename to src/main/java/org/cryptomator/cryptofs/attr/CryptoUserDefinedFileAttributeView.java index 0908c011..655b5317 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/CryptoUserDefinedAttributeView.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/CryptoUserDefinedFileAttributeView.java @@ -19,14 +19,14 @@ import java.util.List; @AttributeViewScoped -final class CryptoUserDefinedAttributeView extends AbstractCryptoFileAttributeView implements UserDefinedFileAttributeView { +final class CryptoUserDefinedFileAttributeView extends AbstractCryptoFileAttributeView implements UserDefinedFileAttributeView { private static final String PREFIX = "c9r."; private final Cryptor cryptor; @Inject - public CryptoUserDefinedAttributeView(CryptoPath cleartextPath, CryptoPathMapper pathMapper, LinkOption[] linkOptions, Symlinks symlinks, Cryptor cryptor) { + public CryptoUserDefinedFileAttributeView(CryptoPath cleartextPath, CryptoPathMapper pathMapper, LinkOption[] linkOptions, Symlinks symlinks, Cryptor cryptor) { super(cleartextPath, pathMapper, linkOptions, symlinks); this.cryptor = cryptor; } @@ -45,7 +45,8 @@ public List list() throws IOException { @Override public int size(String cleartextName) throws IOException { var ciphertextName = encryptName(cleartextName); - return getCiphertextAttributeView(UserDefinedFileAttributeView.class).size(ciphertextName); + var ciphertextSize = getCiphertextAttributeView(UserDefinedFileAttributeView.class).size(ciphertextName); + return (int) cryptor.fileContentCryptor().cleartextSize(ciphertextSize) - cryptor.fileHeaderCryptor().headerSize(); } @Override From 409b54a31162bc27961930aa07b9b53589b32369 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 5 Jan 2023 13:10:45 +0100 Subject: [PATCH 5/6] added unit tests --- ...ryptoUserDefinedFileAttributeViewTest.java | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 src/test/java/org/cryptomator/cryptofs/attr/CryptoUserDefinedFileAttributeViewTest.java diff --git a/src/test/java/org/cryptomator/cryptofs/attr/CryptoUserDefinedFileAttributeViewTest.java b/src/test/java/org/cryptomator/cryptofs/attr/CryptoUserDefinedFileAttributeViewTest.java new file mode 100644 index 00000000..a3f65c6e --- /dev/null +++ b/src/test/java/org/cryptomator/cryptofs/attr/CryptoUserDefinedFileAttributeViewTest.java @@ -0,0 +1,134 @@ +package org.cryptomator.cryptofs.attr; + +import org.cryptomator.cryptofs.CiphertextFilePath; +import org.cryptomator.cryptofs.CryptoPath; +import org.cryptomator.cryptofs.CryptoPathMapper; +import org.cryptomator.cryptofs.Symlinks; +import org.cryptomator.cryptofs.common.CiphertextFileType; +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.FileContentCryptor; +import org.cryptomator.cryptolib.api.FileHeader; +import org.cryptomator.cryptolib.api.FileHeaderCryptor; +import org.cryptomator.cryptolib.api.FileNameCryptor; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.attribute.UserDefinedFileAttributeView; +import java.nio.file.spi.FileSystemProvider; +import java.util.Arrays; +import java.util.List; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class CryptoUserDefinedFileAttributeViewTest { + + private final Cryptor cryptor = mock(Cryptor.class); + private final FileNameCryptor fileNameCryptor = mock(FileNameCryptor.class); + private final FileHeaderCryptor fileHeaderCryptor = mock(FileHeaderCryptor.class); + private final FileContentCryptor fileContentCryptor = mock(FileContentCryptor.class); + private final CiphertextFilePath ciphertextPath = mock(CiphertextFilePath.class); + private final Path ciphertextRawPath = mock(Path.class); + private final FileSystem fileSystem = mock(FileSystem.class); + private final FileSystemProvider provider = mock(FileSystemProvider.class); + private final UserDefinedFileAttributeView delegate = mock(UserDefinedFileAttributeView.class); + private final CryptoPath cleartextPath = mock(CryptoPath.class); + private final CryptoPathMapper pathMapper = mock(CryptoPathMapper.class); + private final Symlinks symlinks = mock(Symlinks.class); + + private CryptoUserDefinedFileAttributeView inTest; + + @BeforeEach + public void setUp() throws IOException { + when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor); + when(cryptor.fileHeaderCryptor()).thenReturn(fileHeaderCryptor); + when(cryptor.fileContentCryptor()).thenReturn(fileContentCryptor); + when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).thenAnswer(invocation -> invocation.getArgument(1)); + when(fileNameCryptor.decryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).thenAnswer(invocation -> invocation.getArgument(1)); + when(fileHeaderCryptor.headerSize()).thenReturn(4); + when(fileHeaderCryptor.decryptHeader(Mockito.any())).thenReturn(Mockito.mock(FileHeader.class)); + when(fileHeaderCryptor.encryptHeader(Mockito.any())).thenReturn(StandardCharsets.UTF_8.encode("HEAD")); + when(fileContentCryptor.ciphertextChunkSize()).thenReturn(1024); + when(fileContentCryptor.cleartextChunkSize()).thenReturn(1024); + when(fileContentCryptor.cleartextSize(Mockito.anyLong())).thenCallRealMethod(); + when(fileContentCryptor.decryptChunk(Mockito.any(), Mockito.anyLong(), Mockito.any(), Mockito.anyBoolean())).thenAnswer(invocation -> invocation.getArgument(0)); + when(fileContentCryptor.encryptChunk(Mockito.any(), Mockito.anyLong(), Mockito.any())).thenAnswer(invocation -> invocation.getArgument(0)); + + when(ciphertextRawPath.getFileSystem()).thenReturn(fileSystem); + when(fileSystem.provider()).thenReturn(provider); + when(provider.getFileAttributeView(ciphertextRawPath, UserDefinedFileAttributeView.class)).thenReturn(delegate); + when(pathMapper.getCiphertextFileType(cleartextPath)).thenReturn(CiphertextFileType.FILE); + when(pathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextPath); + when(ciphertextPath.getFilePath()).thenReturn(ciphertextRawPath); + + inTest = new CryptoUserDefinedFileAttributeView(cleartextPath, pathMapper, new LinkOption[]{}, symlinks, cryptor); + } + + @Test + public void testName() { + Assertions.assertEquals("user", inTest.name()); + } + + @Test + public void testList() throws IOException { + when(delegate.list()).thenReturn(Arrays.asList("c9r.Foo", "c9r.Bar")); + List result = inTest.list(); + MatcherAssert.assertThat(result, CoreMatchers.hasItems("Foo", "Bar")); + } + + @Test + public void testSize() throws IOException { + when(delegate.size("c9r.Foo")).thenReturn(50); + int result = inTest.size("Foo"); + Assertions.assertEquals(46, result); + } + + @Test + public void testWrite() throws IOException { + when(delegate.write(Mockito.eq("c9r.Foo"), Mockito.any())).thenAnswer(invocation -> { + ByteBuffer buf = invocation.getArgument(1); + int len = buf.remaining(); + buf.position(buf.position() + len); + return len; + }); + + int written = inTest.write("Foo", StandardCharsets.UTF_8.encode("Hello World")); + Assertions.assertEquals("Hello World".length(), written); + Mockito.verify(delegate).write(Mockito.eq("c9r.Foo"), Mockito.any()); + } + + @Test + public void testRead() throws IOException { + byte[] content = "HEADHello World".getBytes(StandardCharsets.UTF_8); + when(delegate.size("c9r.Foo")).thenReturn(content.length); + when(delegate.read(Mockito.eq("c9r.Foo"), Mockito.any())).thenAnswer(invocation -> { + ByteBuffer buf = invocation.getArgument(1); + buf.put(content); + return content.length; + }); + + ByteBuffer buf = ByteBuffer.allocate(inTest.size("Foo")); + int read = inTest.read("Foo", buf); + Assertions.assertEquals("Hello World".length(), read); + buf.flip(); + Assertions.assertEquals("Hello World", StandardCharsets.UTF_8.decode(buf).toString()); + } + + @Test + public void testDelete() throws IOException { + inTest.delete("Foo"); + Mockito.verify(delegate).delete(Mockito.eq("c9r.Foo")); + + } + +} \ No newline at end of file From cfab87468797d5189ab9dcdba8419023e6d7578a Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 5 Jan 2023 13:38:13 +0100 Subject: [PATCH 6/6] added integration tests and fixed some errors --- .../cryptofs/attr/AttributeViewModule.java | 3 +- .../CryptoUserDefinedFileAttributeView.java | 20 +++-- .../attr/FileAttributeIntegrationTest.java | 86 ++++++++++++++++++- 3 files changed, 98 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewModule.java b/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewModule.java index 1c04763e..b9ecc41d 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewModule.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewModule.java @@ -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; @@ -44,7 +45,7 @@ abstract class AttributeViewModule { @Binds @IntoMap - @ClassKey(CryptoUserDefinedFileAttributeView.class) + @ClassKey(UserDefinedFileAttributeView.class) @AttributeViewScoped public abstract FileAttributeView provideUserDefinedAttributeView(CryptoUserDefinedFileAttributeView view); diff --git a/src/main/java/org/cryptomator/cryptofs/attr/CryptoUserDefinedFileAttributeView.java b/src/main/java/org/cryptomator/cryptofs/attr/CryptoUserDefinedFileAttributeView.java index 655b5317..908ea2c6 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/CryptoUserDefinedFileAttributeView.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/CryptoUserDefinedFileAttributeView.java @@ -45,8 +45,9 @@ public List list() throws IOException { @Override public int size(String cleartextName) throws IOException { var ciphertextName = encryptName(cleartextName); - var ciphertextSize = getCiphertextAttributeView(UserDefinedFileAttributeView.class).size(ciphertextName); - return (int) cryptor.fileContentCryptor().cleartextSize(ciphertextSize) - cryptor.fileHeaderCryptor().headerSize(); + var totalCiphertextSize = getCiphertextAttributeView(UserDefinedFileAttributeView.class).size(ciphertextName); + var ciphertextBodySize = totalCiphertextSize - cryptor.fileHeaderCryptor().headerSize(); + return (int) cryptor.fileContentCryptor().cleartextSize(ciphertextBodySize); } @Override @@ -68,14 +69,15 @@ public int read(String cleartextName, ByteBuffer dst) throws IOException { @Override public int write(String cleartextName, ByteBuffer src) throws IOException { var ciphertextName = encryptName(cleartextName); - try (var out = new ByteArrayOutputStream(); - var ciphertextChannel = Channels.newChannel(out); // + var out = new ByteArrayOutputStream(); + final int size; + try (var ciphertextChannel = Channels.newChannel(out); // var cleartextChannel = new EncryptingWritableByteChannel(ciphertextChannel, cryptor)) { - int size = cleartextChannel.write(src); - var buf = ByteBuffer.wrap(out.toByteArray()); - getCiphertextAttributeView(UserDefinedFileAttributeView.class).write(ciphertextName, buf); - return size; - } + size = cleartextChannel.write(src); + } // close to flush cached ciphertext + var buf = ByteBuffer.wrap(out.toByteArray()); + getCiphertextAttributeView(UserDefinedFileAttributeView.class).write(ciphertextName, buf); + return size; } @Override diff --git a/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java index 467b5598..1bf0cdb8 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java @@ -8,7 +8,9 @@ *******************************************************************************/ package org.cryptomator.cryptofs.attr; +import com.google.common.jimfs.Configuration; import com.google.common.jimfs.Jimfs; +import com.google.common.jimfs.PathType; import org.cryptomator.cryptofs.CryptoFileSystemProperties; import org.cryptomator.cryptofs.CryptoFileSystemProvider; import org.cryptomator.cryptolib.api.Masterkey; @@ -22,13 +24,22 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; import java.io.IOException; import java.net.URI; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.NoSuchFileException; @@ -37,6 +48,7 @@ import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; +import java.nio.file.attribute.UserDefinedFileAttributeView; import java.nio.file.attribute.UserPrincipal; import java.time.Instant; import java.util.Map; @@ -58,7 +70,7 @@ public class FileAttributeIntegrationTest { @BeforeAll public static void setupClass() throws IOException, MasterkeyLoadingFailedException { - inMemoryFs = Jimfs.newFileSystem(); + inMemoryFs = Jimfs.newFileSystem(Configuration.unix().toBuilder().setAttributeViews("basic", "owner", "user").build()); pathToVault = inMemoryFs.getRootDirectories().iterator().next().resolve("vault"); Files.createDirectory(pathToVault); MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class); @@ -193,6 +205,78 @@ public void testFileAttributeViewUpdatesAfterMove() throws IOException { } } + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + @DisplayName("Extended Attributes") + public class UserDefinedFileAttributes { + + private Path file; + + @BeforeAll + public void setup() throws IOException { + Assumptions.assumeTrue(inMemoryFs.supportedFileAttributeViews().contains("user")); + Assumptions.assumeTrue(fileSystem.supportedFileAttributeViews().contains("user")); + file = fileSystem.getPath("/xattr.txt"); + Files.createFile(file); + } + + @Order(1) + @DisplayName("setxattr /xattr.txt") + @ParameterizedTest(name = "{0}") + @ValueSource(strings = {"attr1", "attr2", "attr3", "attr4", "attr5"}) + public void testSetxattr(String attrName) throws IOException { + var attrView = Files.getFileAttributeView(file, UserDefinedFileAttributeView.class); + var attrValue = StandardCharsets.UTF_8.encode(attrName); + + int written = attrView.write(attrName, attrValue); + + Assertions.assertEquals(attrName.length(), written); + } + + @Order(2) + @Test + @DisplayName("removexattr /xattr.txt") + public void testRemovexattr() { + var attrView = Files.getFileAttributeView(file, UserDefinedFileAttributeView.class); + + Assertions.assertDoesNotThrow(() -> attrView.delete("attr3")); + } + + @Order(3) + @Test + @DisplayName("listxattr /xattr.txt") + public void testListxattr() throws IOException { + var attrView = Files.getFileAttributeView(file, UserDefinedFileAttributeView.class); + var result = attrView.list(); + + Assertions.assertAll( + () -> Assertions.assertTrue(result.contains("attr1")), + () -> Assertions.assertTrue(result.contains("attr2")), + () -> Assertions.assertFalse(result.contains("attr3")), + () -> Assertions.assertTrue(result.contains("attr4")), + () -> Assertions.assertTrue(result.contains("attr5")) + ); + } + + @Order(4) + @DisplayName("getxattr") + @ParameterizedTest(name = "{0}") + @ValueSource(strings = {"attr1", "attr2", "attr4", "attr5"}) + public void testGetxattr(String attrName) throws IOException { + var attrView = Files.getFileAttributeView(file, UserDefinedFileAttributeView.class); + var buffer = ByteBuffer.allocate(attrView.size(attrName)); + var read = attrView.read(attrName, buffer); + buffer.flip(); + var value = StandardCharsets.UTF_8.decode(buffer).toString(); + + Assertions.assertEquals(attrName.length(), read); + Assertions.assertEquals(attrName, value); + } + + } + + private static Matcher isAfter(FileTime previousFileTime) { return new BaseMatcher<>() { @Override