preSignedUri(Location location, Duration ttl)
throw new UnsupportedOperationException("Pre-signed URIs are not supported by " + getClass().getSimpleName());
}
+ /**
+ * Returns the direct encrypted pre-signed URI location for the given storage location.
+ *
+ * Pre-signed URIs allow for retrieval of the files directly from the storage location.
+ * This is useful for large files where the server would be a bottleneck.
+ *
+ * @throws UnsupportedOperationException if the pre-signed URIs are not supported
+ * @return the pre-signed URI to the storage location or `Optional.empty()`
+ * if pre-signed URI cannot be generated.
+ */
+ default Optional encryptedPreSignedUri(Location location, Duration ttl, EncryptionKey key)
+ throws IOException
+ {
+ throw new UnsupportedOperationException("Encrypted pre-signed URIs are not supported by " + getClass().getSimpleName());
+ }
+
/**
* Checks whether given exception is unrecoverable, so that further retries won't help
*
diff --git a/lib/trino-filesystem/src/main/java/io/trino/filesystem/encryption/EncryptionEnforcingFileSystem.java b/lib/trino-filesystem/src/main/java/io/trino/filesystem/encryption/EncryptionEnforcingFileSystem.java
new file mode 100644
index 000000000000..e832bdb48206
--- /dev/null
+++ b/lib/trino-filesystem/src/main/java/io/trino/filesystem/encryption/EncryptionEnforcingFileSystem.java
@@ -0,0 +1,189 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.trino.filesystem.encryption;
+
+import io.airlift.units.Duration;
+import io.trino.filesystem.FileIterator;
+import io.trino.filesystem.Location;
+import io.trino.filesystem.TrinoFileSystem;
+import io.trino.filesystem.TrinoInputFile;
+import io.trino.filesystem.TrinoOutputFile;
+import io.trino.filesystem.UriLocation;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.Optional;
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Objects.requireNonNull;
+
+/**
+ * File system implementation that enforces encrypted file system calls.
+ */
+public class EncryptionEnforcingFileSystem
+ implements TrinoFileSystem
+{
+ private final TrinoFileSystem delegate;
+ private final EncryptionKey key;
+
+ public EncryptionEnforcingFileSystem(TrinoFileSystem delegate, EncryptionKey key)
+ {
+ this.delegate = requireNonNull(delegate, "delegate is null");
+ this.key = requireNonNull(key, "key is null");
+ }
+
+ @Override
+ public TrinoInputFile newInputFile(Location location)
+ {
+ return newEncryptedInputFile(location, key);
+ }
+
+ @Override
+ public TrinoInputFile newEncryptedInputFile(Location location, EncryptionKey key)
+ {
+ checkArgument(this.key.equals(key), "Provided key is not the same as the class encryption key");
+ return delegate.newEncryptedInputFile(location, key);
+ }
+
+ @Override
+ public TrinoInputFile newInputFile(Location location, long length)
+ {
+ return delegate.newEncryptedInputFile(location, length, key);
+ }
+
+ @Override
+ public TrinoInputFile newEncryptedInputFile(Location location, long length, EncryptionKey key)
+ {
+ checkArgument(this.key.equals(key), "Provided key is not the same as the class encryption key");
+ return delegate.newEncryptedInputFile(location, length, key);
+ }
+
+ @Override
+ public TrinoInputFile newInputFile(Location location, long length, Instant lastModified)
+ {
+ return delegate.newEncryptedInputFile(location, length, lastModified, key);
+ }
+
+ @Override
+ public TrinoInputFile newEncryptedInputFile(Location location, long length, Instant lastModified, EncryptionKey key)
+ {
+ checkArgument(this.key.equals(key), "Provided key is not the same as the class encryption key");
+ return delegate.newEncryptedInputFile(location, length, key);
+ }
+
+ @Override
+ public TrinoOutputFile newOutputFile(Location location)
+ {
+ return delegate.newEncryptedOutputFile(location, key);
+ }
+
+ @Override
+ public TrinoOutputFile newEncryptedOutputFile(Location location, EncryptionKey key)
+ {
+ checkArgument(this.key.equals(key), "Provided key is not the same as the class encryption key");
+ return delegate.newEncryptedOutputFile(location, key);
+ }
+
+ @Override
+ public void deleteFile(Location location)
+ throws IOException
+ {
+ delegate.deleteFile(location);
+ }
+
+ @Override
+ public void deleteFiles(Collection locations)
+ throws IOException
+ {
+ delegate.deleteFiles(locations);
+ }
+
+ @Override
+ public void deleteDirectory(Location location)
+ throws IOException
+ {
+ delegate.deleteDirectory(location);
+ }
+
+ @Override
+ public void renameFile(Location source, Location target)
+ throws IOException
+ {
+ delegate.renameFile(source, target);
+ }
+
+ @Override
+ public FileIterator listFiles(Location location)
+ throws IOException
+ {
+ return delegate.listFiles(location);
+ }
+
+ @Override
+ public Optional directoryExists(Location location)
+ throws IOException
+ {
+ return delegate.directoryExists(location);
+ }
+
+ @Override
+ public void createDirectory(Location location)
+ throws IOException
+ {
+ delegate.createDirectory(location);
+ }
+
+ @Override
+ public void renameDirectory(Location source, Location target)
+ throws IOException
+ {
+ delegate.renameDirectory(source, target);
+ }
+
+ @Override
+ public Set listDirectories(Location location)
+ throws IOException
+ {
+ return delegate.listDirectories(location);
+ }
+
+ @Override
+ public Optional createTemporaryDirectory(Location targetPath, String temporaryPrefix, String relativePrefix)
+ throws IOException
+ {
+ return delegate.createTemporaryDirectory(targetPath, temporaryPrefix, relativePrefix);
+ }
+
+ @Override
+ public Optional preSignedUri(Location location, Duration ttl)
+ throws IOException
+ {
+ return delegate.encryptedPreSignedUri(location, ttl, key);
+ }
+
+ @Override
+ public Optional encryptedPreSignedUri(Location location, Duration ttl, EncryptionKey key)
+ throws IOException
+ {
+ checkArgument(this.key.equals(key), "Provided key is not the same as the class encryption key");
+ return delegate.encryptedPreSignedUri(location, ttl, key);
+ }
+
+ public TrinoFileSystem getDelegate()
+ {
+ return delegate;
+ }
+}
diff --git a/lib/trino-filesystem/src/main/java/io/trino/filesystem/encryption/EncryptionKey.java b/lib/trino-filesystem/src/main/java/io/trino/filesystem/encryption/EncryptionKey.java
new file mode 100644
index 000000000000..474262df09a2
--- /dev/null
+++ b/lib/trino-filesystem/src/main/java/io/trino/filesystem/encryption/EncryptionKey.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.trino.filesystem.encryption;
+
+import java.util.concurrent.ThreadLocalRandom;
+
+import static java.util.Objects.requireNonNull;
+
+public record EncryptionKey(byte[] key, String algorithm)
+{
+ public EncryptionKey
+ {
+ requireNonNull(algorithm, "algorithm is null");
+ requireNonNull(key, "key is null");
+ }
+
+ public static EncryptionKey randomAes256()
+ {
+ byte[] key = new byte[32];
+ ThreadLocalRandom.current().nextBytes(key);
+ return new EncryptionKey(key, "AES256");
+ }
+
+ @Override
+ public String toString()
+ {
+ // We intentionally overwrite toString to hide a key
+ return algorithm;
+ }
+}
diff --git a/lib/trino-filesystem/src/main/java/io/trino/filesystem/switching/SwitchingFileSystem.java b/lib/trino-filesystem/src/main/java/io/trino/filesystem/switching/SwitchingFileSystem.java
index aa2531b2d573..5c65ccc1c9b6 100644
--- a/lib/trino-filesystem/src/main/java/io/trino/filesystem/switching/SwitchingFileSystem.java
+++ b/lib/trino-filesystem/src/main/java/io/trino/filesystem/switching/SwitchingFileSystem.java
@@ -21,6 +21,7 @@
import io.trino.filesystem.TrinoInputFile;
import io.trino.filesystem.TrinoOutputFile;
import io.trino.filesystem.UriLocation;
+import io.trino.filesystem.encryption.EncryptionKey;
import io.trino.spi.connector.ConnectorSession;
import io.trino.spi.security.ConnectorIdentity;
@@ -157,6 +158,37 @@ public Optional preSignedUri(Location targetPath, Duration ttl)
return fileSystem(targetPath).preSignedUri(targetPath, ttl);
}
+ @Override
+ public TrinoInputFile newEncryptedInputFile(Location location, EncryptionKey key)
+ {
+ return fileSystem(location).newEncryptedInputFile(location, key);
+ }
+
+ @Override
+ public TrinoInputFile newEncryptedInputFile(Location location, long length, EncryptionKey key)
+ {
+ return fileSystem(location).newEncryptedInputFile(location, length, key);
+ }
+
+ @Override
+ public TrinoInputFile newEncryptedInputFile(Location location, long length, Instant lastModified, EncryptionKey key)
+ {
+ return fileSystem(location).newEncryptedInputFile(location, length, lastModified, key);
+ }
+
+ @Override
+ public TrinoOutputFile newEncryptedOutputFile(Location location, EncryptionKey key)
+ {
+ return fileSystem(location).newEncryptedOutputFile(location, key);
+ }
+
+ @Override
+ public Optional encryptedPreSignedUri(Location location, Duration ttl, EncryptionKey key)
+ throws IOException
+ {
+ return fileSystem(location).encryptedPreSignedUri(location, ttl, key);
+ }
+
private TrinoFileSystem fileSystem(Location location)
{
return createFileSystem(loader.apply(location));
diff --git a/lib/trino-filesystem/src/main/java/io/trino/filesystem/tracing/TracingFileSystem.java b/lib/trino-filesystem/src/main/java/io/trino/filesystem/tracing/TracingFileSystem.java
index 5dd90fcb1abd..f7c6576fb7f6 100644
--- a/lib/trino-filesystem/src/main/java/io/trino/filesystem/tracing/TracingFileSystem.java
+++ b/lib/trino-filesystem/src/main/java/io/trino/filesystem/tracing/TracingFileSystem.java
@@ -22,6 +22,7 @@
import io.trino.filesystem.TrinoInputFile;
import io.trino.filesystem.TrinoOutputFile;
import io.trino.filesystem.UriLocation;
+import io.trino.filesystem.encryption.EncryptionKey;
import java.io.IOException;
import java.time.Instant;
@@ -177,4 +178,50 @@ public Optional preSignedUri(Location location, Duration ttl)
.startSpan();
return withTracing(span, () -> delegate.preSignedUri(location, ttl));
}
+
+ @Override
+ public TrinoInputFile newEncryptedInputFile(Location location, EncryptionKey key)
+ {
+ Span span = tracer.spanBuilder("FileSystem.newEncryptedInputFile")
+ .setAttribute(FileSystemAttributes.FILE_LOCATION, location.toString())
+ .startSpan();
+ return withTracing(span, () -> delegate.newEncryptedInputFile(location, key));
+ }
+
+ @Override
+ public TrinoInputFile newEncryptedInputFile(Location location, long length, EncryptionKey key)
+ {
+ Span span = tracer.spanBuilder("FileSystem.newEncryptedInputFile")
+ .setAttribute(FileSystemAttributes.FILE_LOCATION, location.toString())
+ .startSpan();
+ return withTracing(span, () -> delegate.newEncryptedInputFile(location, length, key));
+ }
+
+ @Override
+ public TrinoInputFile newEncryptedInputFile(Location location, long length, Instant lastModified, EncryptionKey key)
+ {
+ Span span = tracer.spanBuilder("FileSystem.newEncryptedInputFile")
+ .setAttribute(FileSystemAttributes.FILE_LOCATION, location.toString())
+ .startSpan();
+ return withTracing(span, () -> delegate.newEncryptedInputFile(location, length, lastModified, key));
+ }
+
+ @Override
+ public TrinoOutputFile newEncryptedOutputFile(Location location, EncryptionKey key)
+ {
+ Span span = tracer.spanBuilder("FileSystem.newEncryptedOutputFile")
+ .setAttribute(FileSystemAttributes.FILE_LOCATION, location.toString())
+ .startSpan();
+ return withTracing(span, () -> delegate.newEncryptedOutputFile(location, key));
+ }
+
+ @Override
+ public Optional encryptedPreSignedUri(Location location, Duration ttl, EncryptionKey key)
+ throws IOException
+ {
+ Span span = tracer.spanBuilder("FileSystem.encryptedPreSignedUri")
+ .setAttribute(FileSystemAttributes.FILE_LOCATION, location.toString())
+ .startSpan();
+ return withTracing(span, () -> delegate.encryptedPreSignedUri(location, ttl, key));
+ }
}
diff --git a/lib/trino-filesystem/src/test/java/io/trino/filesystem/AbstractTestTrinoFileSystem.java b/lib/trino-filesystem/src/test/java/io/trino/filesystem/AbstractTestTrinoFileSystem.java
index 2b2df9116132..05498e5bb0cd 100644
--- a/lib/trino-filesystem/src/test/java/io/trino/filesystem/AbstractTestTrinoFileSystem.java
+++ b/lib/trino-filesystem/src/test/java/io/trino/filesystem/AbstractTestTrinoFileSystem.java
@@ -19,6 +19,7 @@
import com.google.common.io.Closer;
import io.airlift.slice.Slice;
import io.airlift.units.Duration;
+import io.trino.filesystem.encryption.EncryptionEnforcingFileSystem;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
@@ -63,6 +64,7 @@
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Fail.fail;
import static org.junit.jupiter.api.Assumptions.abort;
import static org.junit.jupiter.api.parallel.ExecutionMode.SAME_THREAD;
@@ -81,6 +83,11 @@ public abstract class AbstractTestTrinoFileSystem
protected abstract void verifyFileSystemIsEmpty();
+ protected boolean useServerSideEncryptionWithCustomerKey()
+ {
+ return false;
+ }
+
/**
* Specifies whether implementation {@link TrinoOutputFile#create()} is exclusive.
*/
@@ -1424,6 +1431,37 @@ public void testLargeFileDoesNotExistUntilClosed()
getFileSystem().deleteFile(location);
}
+ @Test
+ void testServerSideEncryptionWithCustomerKey()
+ throws IOException
+ {
+ if (!useServerSideEncryptionWithCustomerKey()) {
+ abort("Test is specific to SSE-C");
+ }
+
+ Location location = getRootLocation().appendPath("encrypted");
+
+ byte[] data = "this is encrypted data".getBytes(UTF_8);
+
+ // Create encrypted file
+ getFileSystem().newOutputFile(location)
+ .createOrOverwrite(data);
+
+ if (!(getFileSystem() instanceof EncryptionEnforcingFileSystem encryptionEnforcingFileSystem)) {
+ fail("Expected file system to enforce server side encryption");
+ return;
+ }
+
+ // Try to read it without a key
+ assertThatThrownBy(() -> encryptionEnforcingFileSystem.getDelegate().newInputFile(location).newStream().readAllBytes())
+ .isInstanceOf(IOException.class);
+
+ assertThat(getFileSystem().newInputFile(location).newStream().readAllBytes())
+ .isEqualTo(data);
+
+ getFileSystem().deleteFile(location);
+ }
+
@SuppressWarnings("ConstantValue")
private static byte[] getBytes()
{