Skip to content

Commit

Permalink
feat: Optional encryption for disk cache.
Browse files Browse the repository at this point in the history
  • Loading branch information
nstdio committed Apr 3, 2022
1 parent 0f81a97 commit 3a1af2d
Show file tree
Hide file tree
Showing 25 changed files with 1,203 additions and 257 deletions.
163 changes: 153 additions & 10 deletions src/main/java/io/github/nstdio/http/ext/Cache.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,25 @@

package io.github.nstdio.http.ext;

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse.BodySubscriber;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Flow.Subscriber;
import java.util.function.Consumer;

import static io.github.nstdio.http.ext.Preconditions.checkArgument;
import static io.github.nstdio.http.ext.Preconditions.checkState;
import static java.util.Objects.requireNonNull;

@SuppressWarnings("WeakerAccess")
public interface Cache {
/**
Expand All @@ -44,7 +54,9 @@ static InMemoryCacheBuilder newInMemoryCacheBuilder() {
* @throws IllegalStateException When Jackson (a.k.a. ObjectMapper) is not in classpath.
*/
static DiskCacheBuilder newDiskCacheBuilder() {
return new DiskCacheBuilder(MetadataSerializer.findAvailable());
MetadataSerializer.requireAvailability();

return new DiskCacheBuilder();
}

/**
Expand Down Expand Up @@ -175,11 +187,9 @@ public Cache build() {
* The builder for in persistent cache.
*/
class DiskCacheBuilder extends ConstrainedCacheBuilder<DiskCacheBuilder> {
private Path dir;
private final MetadataSerializer serializer;
Path dir;

DiskCacheBuilder(MetadataSerializer serializer) {
this.serializer = serializer;
DiskCacheBuilder() {
}

/**
Expand All @@ -190,17 +200,150 @@ class DiskCacheBuilder extends ConstrainedCacheBuilder<DiskCacheBuilder> {
* @return builder itself.
*/
public DiskCacheBuilder dir(Path dir) {
this.dir = Objects.requireNonNull(dir);
this.dir = requireNonNull(dir);
return this;
}

/**
* Creates a new {@code EncryptedDiskCacheBuilder} instance which will create {@link Cache} that stores all cache
* files encrypted by provided keys.
*
* @return a newly created builder.
*/
public EncryptedDiskCacheBuilder encrypted() {
return new EncryptedDiskCacheBuilder(this);
}

StreamFactory newStreamFactory() {
return new SimpleStreamFactory();
}

@Override
public Cache build() {
if (dir == null) {
throw new IllegalStateException("dir cannot be null");
checkState(dir != null, "dir cannot be null");

var streamFactory = newStreamFactory();
var serializer = MetadataSerializer.findAvailable(streamFactory);

return build(new DiskCache(size, maxItems, serializer, streamFactory, dir));
}
}

/**
* The {@link DiskCacheBuilder} that will create {@link Cache} that maintains all files in encrypted manner. The
* request, response body, response headers all will be stored encrypted by user provided keys.
*/
final class EncryptedDiskCacheBuilder extends DiskCacheBuilder {
private Key publicKey;
private Key privateKey;
private String cipherAlgorithm;
private String provider;

EncryptedDiskCacheBuilder(DiskCacheBuilder b) {
this.dir = b.dir;
this.size = b.size;
this.maxItems = b.maxItems;
this.responseFilter = b.responseFilter;
this.requestFilter = b.requestFilter;
}

/**
* {@inheritDoc}
*/
public EncryptedDiskCacheBuilder encrypted() {
return this;
}

/**
* The secret key to encrypt/decrypt all files that this cache maintains.
*
* @param key The encryption key. Never null.
*
* @return builder itself.
*/
public EncryptedDiskCacheBuilder key(SecretKey key) {
this.privateKey = requireNonNull(key, "key cannot be null");
this.publicKey = key;
return this;
}

/**
* Sets the public key to use.
*
* @param key The encryption key. Never null.
*
* @return builder itself.
*/
public EncryptedDiskCacheBuilder publicKey(PublicKey key) {
this.publicKey = requireNonNull(key, "publicKey cannot be null");
return this;
}

/**
* Sets the private key to use.
*
* @param key The encryption key. Never null.
*
* @return builder itself.
*/
public EncryptedDiskCacheBuilder privateKey(PrivateKey key) {
this.privateKey = requireNonNull(key, "privateKey cannot be null");
return this;
}

/**
* The cipher algorithm to use. Typically, has form of "algorithm/mode/padding" or "algorithm". Note that
* "NoPadding" padding scheme is not supported.
*
* @param algorithm The algorithm name.
*
* @return builder itself.
*
* @throws IllegalArgumentException if {@code algorithm} is null, or contains "NoPadding" padding scheme.
* @throws NoSuchAlgorithmException if algorithm empty, is in an invalid format, or if no Provider supports a
* CipherSpi implementation for the specified algorithm.
* @throws NoSuchPaddingException if algorithm contains a padding scheme that is not available.
* @see javax.crypto.Cipher#getInstance(String)
*/
public EncryptedDiskCacheBuilder cipherAlgorithm(String algorithm) throws NoSuchPaddingException, NoSuchAlgorithmException {
this.cipherAlgorithm = validAlgorithm(algorithm);

return this;
}

private String validAlgorithm(String algo) throws NoSuchPaddingException, NoSuchAlgorithmException {
checkArgument(algo != null, "algorithm cannot be null");
String[] parts = algo.split("/");
if (parts.length == 3 && parts[2].equals("NoPadding")) {
throw new IllegalArgumentException("NoPadding transformations are not supported");
}

return build(new DiskCache(maxItems, size, serializer, dir));
Cipher.getInstance(algo);
return algo;
}

/**
* The {@link java.security.Provider} name to use.
*
* @param provider The provider name.
*
* @return builder itself.
*
* @see javax.crypto.Cipher#getInstance(String, String)
*/
public EncryptedDiskCacheBuilder provider(String provider) {
checkArgument(provider != null, "provider cannot be null");
this.provider = provider;
return this;
}

@Override
StreamFactory newStreamFactory() {
checkState(publicKey != null && privateKey != null, "specify keypair or secret");
checkState(cipherAlgorithm != null, "algorithm cannot be null");

var delegate = super.newStreamFactory();
return new EncryptedStreamFactory(delegate, publicKey, privateKey, cipherAlgorithm, provider);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@
import java.util.Objects;
import java.util.function.Predicate;

import static io.github.nstdio.http.ext.Preconditions.checkArgument;
import static io.github.nstdio.http.ext.Predicates.alwaysTrue;

abstract class ConstrainedCacheBuilder<B extends ConstrainedCacheBuilder<B>> implements Cache.CacheBuilder {
int maxItems = 1 << 13;
long size = -1;
private Predicate<HttpRequest> requestFilter;
private Predicate<ResponseInfo> responseFilter;
Predicate<HttpRequest> requestFilter;
Predicate<ResponseInfo> responseFilter;

ConstrainedCacheBuilder() {
}
Expand All @@ -40,9 +41,7 @@ abstract class ConstrainedCacheBuilder<B extends ConstrainedCacheBuilder<B>> imp
* @return builder itself.
*/
public B maxItems(int maxItems) {
if (maxItems <= 0) {
throw new IllegalArgumentException("maxItems should be positive");
}
checkArgument(maxItems > 0, "maxItems should be positive");

this.maxItems = maxItems;
return self();
Expand Down
18 changes: 11 additions & 7 deletions src/main/java/io/github/nstdio/http/ext/DiskCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,20 @@

class DiskCache extends SizeConstrainedCache {
private final MetadataSerializer metadataSerializer;
private final StreamFactory streamFactory;
private final Executor executor;
private final Path dir;

DiskCache(Path dir) {
this(1 << 13, -1, new JacksonMetadataSerializer(), dir);
this(-1, 1 << 13, new JacksonMetadataSerializer(), new SimpleStreamFactory(), dir);
}

DiskCache(int maxItems, long maxBytes, MetadataSerializer metadataSerializer, Path dir) {
DiskCache(long maxBytes, int maxItems, MetadataSerializer metadataSerializer, StreamFactory streamFactory, Path dir) {
super(maxItems, maxBytes, null);
addEvictionListener(this::deleteQuietly);

this.metadataSerializer = metadataSerializer;
this.streamFactory = streamFactory;
this.dir = dir;
this.executor = Executors.newSingleThreadExecutor(r -> new Thread(r, "disk-cache-io"));

Expand All @@ -74,7 +76,7 @@ private void restore() {
.filter(entryPaths -> Files.exists(entryPaths.body()))
.map(entryPaths -> {
var metadata = metadataSerializer.read(entryPaths.metadata());
return metadata != null ? new DiskCacheEntry(entryPaths, metadata) : null;
return metadata != null ? new DiskCacheEntry(entryPaths, streamFactory, metadata) : null;
})
.filter(Objects::nonNull)
.forEach(entry -> put(entry.metadata().request(), entry));
Expand Down Expand Up @@ -121,12 +123,12 @@ public Writer<Path> writer(CacheEntryMetadata metadata) {
return new Writer<>() {
@Override
public BodySubscriber<Path> subscriber() {
return new PathSubscriber(entryPaths.body());
return new PathSubscriber(streamFactory, entryPaths.body());
}

@Override
public Consumer<Path> finisher() {
return path -> put(metadata.request(), new DiskCacheEntry(entryPaths, metadata));
return path -> put(metadata.request(), new DiskCacheEntry(entryPaths, streamFactory, metadata));
}
};
}
Expand All @@ -143,19 +145,21 @@ private static class EntryPaths {
@Accessors(fluent = true)
private static class DiskCacheEntry implements CacheEntry {
private final EntryPaths path;
private final StreamFactory streamFactory;
private final CacheEntryMetadata metadata;

private final long bodySize;

private DiskCacheEntry(EntryPaths path, CacheEntryMetadata metadata) {
private DiskCacheEntry(EntryPaths path, StreamFactory streamFactory, CacheEntryMetadata metadata) {
this.path = path;
this.streamFactory = streamFactory;
this.metadata = metadata;
this.bodySize = size(path.body());
}

@Override
public void subscribeTo(Subscriber<List<ByteBuffer>> sub) {
Subscription subscription = new PathReadingSubscription(sub, path.body());
Subscription subscription = new PathReadingSubscription(sub, streamFactory, path.body());
sub.onSubscribe(subscription);
}

Expand Down
Loading

0 comments on commit 3a1af2d

Please sign in to comment.