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

simplify server-side encryption #1014

Merged
Merged
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
10 changes: 5 additions & 5 deletions api/src/main/java/io/minio/MinioClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -709,13 +709,13 @@ private void checkReadRequestSse(ServerSideEncryption sse) throws IllegalArgumen
return;
}

if (sse.type() != ServerSideEncryption.Type.SSE_C) {
if (!(sse instanceof ServerSideEncryptionCustomerKey)) {
throw new IllegalArgumentException("only SSE_C is supported for all read requests.");
}

if (sse.type().requiresTls() && !this.baseUrl.isHttps()) {
if (sse.tlsRequired() && !this.baseUrl.isHttps()) {
throw new IllegalArgumentException(
sse.type().name() + "operations must be performed over a secure connection.");
sse + "operations must be performed over a secure connection.");
}
}

Expand Down Expand Up @@ -2360,7 +2360,7 @@ public ObjectWriteResponse composeObject(ComposeObjectArgs args)
String uploadId = createMultipartUploadResponse.result().uploadId();

Multimap<String, String> ssecHeaders = HashMultimap.create();
if (args.sse() != null && args.sse().type() == ServerSideEncryption.Type.SSE_C) {
if (args.sse() != null && args.sse() instanceof ServerSideEncryptionCustomerKey) {
ssecHeaders.putAll(newMultimap(args.sse().headers()));
}

Expand Down Expand Up @@ -4760,7 +4760,7 @@ private ObjectWriteResponse putObject(

Map<String, String> ssecHeaders = null;
// set encryption headers in the case of SSE-C.
if (args.sse() != null && args.sse().type() == ServerSideEncryption.Type.SSE_C) {
if (args.sse() != null && args.sse() instanceof ServerSideEncryptionCustomerKey) {
ssecHeaders = args.sse().headers();
}

Expand Down
4 changes: 2 additions & 2 deletions api/src/main/java/io/minio/ObjectArgs.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ protected void checkSse(ServerSideEncryption sse, HttpUrl url) {
return;
}

if (sse.type().requiresTls() && !url.isHttps()) {
if (sse.tlsRequired() && !url.isHttps()) {
throw new IllegalArgumentException(
sse.type().name() + " operations must be performed over a secure connection.");
sse + " operations must be performed over a secure connection.");
}
}

Expand Down
176 changes: 7 additions & 169 deletions api/src/main/java/io/minio/ServerSideEncryption.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,183 +16,21 @@

package io.minio;

import com.google.common.io.BaseEncoding;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import javax.crypto.SecretKey;
import javax.security.auth.DestroyFailedException;
import javax.security.auth.Destroyable;

/** Server-side encryption options. */
public abstract class ServerSideEncryption implements Destroyable {
/** The types of server-side encryption. */
public static enum Type {
SSE_C,
SSE_S3,
SSE_KMS;
public abstract class ServerSideEncryption {
private static final Map<String, String> emptyHeaders =
Collections.unmodifiableMap(new HashMap<>());

/**
* Returns true if the server-side encryption requires a TLS connection.
*
* @return true if the type of server-side encryption requires TLS.
*/
public boolean requiresTls() {
return this.equals(SSE_C) || this.equals(SSE_KMS);
}
}

protected boolean destroyed = false;

/** Returns server side encryption type. */
public abstract Type type();

/** Returns server side encryption headers. */
public abstract Map<String, String> headers();

/** Returns server side encryption headers for source object in Put Object - Copy. */
public Map<String, String> copySourceHeaders() throws IllegalArgumentException {
throw new IllegalArgumentException(this.type().name() + " is not supported in copy source");
}

@Override
public boolean isDestroyed() {
return this.destroyed;
}

private static boolean isCustomerKeyValid(SecretKey key) {
if (key == null) {
return false;
}
return !key.isDestroyed() && key.getAlgorithm().equals("AES") && key.getEncoded().length == 32;
}

/**
* Create a new server-side-encryption object for encryption with customer provided keys (a.k.a.
* SSE-C).
*
* @param key The secret AES-256 key.
* @return An instance of ServerSideEncryption implementing SSE-C.
* @throws InvalidKeyException if the provided secret key is not a 256 bit AES key.
* @throws NoSuchAlgorithmException if the crypto provider does not implement MD5.
*/
public static ServerSideEncryptionCustomerKey withCustomerKey(SecretKey key)
throws InvalidKeyException, NoSuchAlgorithmException {
if (!isCustomerKeyValid(key)) {
throw new InvalidKeyException("The secret key is not a 256 bit AES key");
}

return new ServerSideEncryptionCustomerKey(key);
}

static final class ServerSideEncryptionS3 extends ServerSideEncryption {
private static final Map<String, String> headers;

static {
Map<String, String> map = new HashMap<>();
map.put("X-Amz-Server-Side-Encryption", "AES256");
headers = Collections.unmodifiableMap(map);
}

@Override
public final Type type() {
return Type.SSE_S3;
}

@Override
public final Map<String, String> headers() {
return headers;
}

@Override
public final void destroy() throws DestroyFailedException {
this.destroyed = true;
}
}

/**
* Create a new server-side-encryption object for encryption at rest (a.k.a. SSE-S3).
*
* @return an instance of ServerSideEncryption implementing SSE-S3
*/
public static ServerSideEncryption atRest() {
return new ServerSideEncryptionS3();
}

static final class ServerSideEncryptionKms extends ServerSideEncryption {
final Map<String, String> headers;

public ServerSideEncryptionKms(String keyId, Optional<String> context) {
Map<String, String> headers = new HashMap<>();
headers.put("X-Amz-Server-Side-Encryption", "aws:kms");
headers.put("X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id", keyId);
if (context.isPresent()) {
headers.put("X-Amz-Server-Side-Encryption-Context", context.get());
}

this.headers = Collections.unmodifiableMap(headers);
}

@Override
public final Type type() {
return Type.SSE_KMS;
}

@Override
public final Map<String, String> headers() {
if (this.isDestroyed()) {
throw new IllegalStateException("object is already destroyed");
}

return headers;
}

@Override
public final void destroy() throws DestroyFailedException {
this.destroyed = true;
}
public boolean tlsRequired() {
return true;
}

/**
* Create a new server-side-encryption object for encryption using a KMS (a.k.a. SSE-KMS).
*
* @param keyId specifies the customer-master-key (CMK) and must not be null.
* @param context is the encryption context. If the context is null no context is used.
* @return an instance of ServerSideEncryption implementing SSE-KMS.
*/
public static ServerSideEncryption withManagedKeys(String keyId, Map<String, String> context)
throws IllegalArgumentException, UnsupportedEncodingException {
if (keyId == null) {
throw new IllegalArgumentException("The key-ID cannot be null");
}
if (context == null) {
return new ServerSideEncryptionKms(keyId, Optional.empty());
}

StringBuilder builder = new StringBuilder();
int i = 0;
builder.append('{');
for (Entry<String, String> entry : context.entrySet()) {
builder.append('"');
builder.append(entry.getKey());
builder.append('"');
builder.append(':');
builder.append('"');
builder.append(entry.getValue());
builder.append('"');
if (i < context.entrySet().size() - 1) {
builder.append(',');
}
}
builder.append('}');
String contextString =
BaseEncoding.base64().encode(builder.toString().getBytes(StandardCharsets.UTF_8));
return new ServerSideEncryptionKms(keyId, Optional.of(contextString));
public Map<String, String> copySourceHeaders() {
return emptyHeaders;
}
}
38 changes: 23 additions & 15 deletions api/src/main/java/io/minio/ServerSideEncryptionCustomerKey.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* MinIO Java SDK for Amazon S3 Compatible Cloud Storage, (C) 2018 MinIO, Inc.
* MinIO Java SDK for Amazon S3 Compatible Cloud Storage, (C) 2020 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -27,12 +27,21 @@
import javax.security.auth.DestroyFailedException;

public class ServerSideEncryptionCustomerKey extends ServerSideEncryption {
final SecretKey secretKey;
final Map<String, String> headers;
final Map<String, String> copySourceHeaders;
private boolean isDestroyed = false;
private final SecretKey secretKey;
private final Map<String, String> headers;
private final Map<String, String> copySourceHeaders;

public ServerSideEncryptionCustomerKey(SecretKey key)
throws InvalidKeyException, NoSuchAlgorithmException {
if (key == null || !key.getAlgorithm().equals("AES") || key.getEncoded().length != 32) {
throw new IllegalArgumentException("Secret key must be 256 bit AES key");
}

if (key.isDestroyed()) {
throw new IllegalArgumentException("Secret key already destroyed");
}

this.secretKey = key;

byte[] keyBytes = key.getEncoded();
Expand All @@ -54,32 +63,31 @@ public ServerSideEncryptionCustomerKey(SecretKey key)
this.copySourceHeaders = Collections.unmodifiableMap(map);
}

@Override
public final Type type() {
return Type.SSE_C;
}

@Override
public final Map<String, String> headers() {
if (this.isDestroyed()) {
throw new IllegalStateException("object is already destroyed");
if (isDestroyed) {
throw new IllegalStateException("Secret key was destroyed");
}

return headers;
}

@Override
public final Map<String, String> copySourceHeaders() {
if (this.isDestroyed()) {
throw new IllegalStateException("object is already destroyed");
if (isDestroyed) {
throw new IllegalStateException("Secret key was destroyed");
}

return copySourceHeaders;
}

@Override
public final void destroy() throws DestroyFailedException {
secretKey.destroy();
this.destroyed = true;
isDestroyed = true;
}

@Override
public String toString() {
return "SSE-C";
}
}
59 changes: 59 additions & 0 deletions api/src/main/java/io/minio/ServerSideEncryptionKms.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* MinIO Java SDK for Amazon S3 Compatible Cloud Storage, (C) 2020 MinIO, Inc.
*
* 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.minio;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class ServerSideEncryptionKms extends ServerSideEncryption {
private static final ObjectMapper objectMapper = new ObjectMapper();
private final Map<String, String> headers;

public ServerSideEncryptionKms(String keyId, Map<String, String> context)
throws JsonProcessingException {
if (keyId == null) {
throw new IllegalArgumentException("Key ID cannot be null");
}

String contextJson = null;
if (context != null) {
contextJson = objectMapper.writeValueAsString(context);
}

Map<String, String> headers = new HashMap<>();
headers.put("X-Amz-Server-Side-Encryption", "aws:kms");
headers.put("X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id", keyId);
if (contextJson != null) {
headers.put("X-Amz-Server-Side-Encryption-Context", contextJson);
}

this.headers = Collections.unmodifiableMap(headers);
}

@Override
public final Map<String, String> headers() {
return headers;
}

@Override
public String toString() {
return "SSE-KMS";
}
}
Loading