Skip to content

Commit

Permalink
R2-2317 add support for SSE-C via workerd bindings
Browse files Browse the repository at this point in the history
  • Loading branch information
helloimalastair committed Aug 7, 2024
1 parent 4bae509 commit e99deb5
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 3 deletions.
20 changes: 20 additions & 0 deletions src/workerd/api/r2-api.capnp
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ struct R2Conditional {
# timestamp.
}

struct R2SSECOptions {
key @0 :Text;
}

struct R2Checksums {
# The JSON name of these fields must comform to the representation of the ChecksumAlgorithm in
# the R2 gateway worker.
Expand Down Expand Up @@ -93,6 +97,7 @@ struct R2GetRequest {
range @1 :R2Range;
rangeHeader @3 :Text;
onlyIf @2 :R2Conditional;
ssec @4 :R2SSECOptions;
}

struct R2PutRequest {
Expand All @@ -106,19 +111,22 @@ struct R2PutRequest {
sha384 @7 :Data $Json.hex;
sha512 @8 :Data $Json.hex;
storageClass @9 :Text;
ssec @10 :R2SSECOptions;
}

struct R2CreateMultipartUploadRequest {
object @0 :Text;
customFields @1 :List(Record);
httpFields @2 :R2HttpFields;
storageClass @3 :Text;
ssec @4 :R2SSECOptions;
}

struct R2UploadPartRequest {
object @0 :Text;
uploadId @1 :Text;
partNumber @2 :UInt32;
ssec @3 :R2SSECOptions;
}

struct R2CompleteMultipartUploadRequest {
Expand Down Expand Up @@ -187,6 +195,11 @@ struct R2ErrorResponse {
message @2 :Text;
}

struct R2SSECResponse {
algorithm @0 :Text;
keyMd5 @1 :Text;
}

struct R2HeadResponse {
name @0 :Text;
# The name of the object.
Expand Down Expand Up @@ -220,6 +233,9 @@ struct R2HeadResponse {
storageClass @9 :Text;
# The storage class of the object. Standard or Infrequent Access.
# Provided on object creation to specify which storage tier R2 should use for this object.

ssec @10 :R2SSECResponse;
# The algorithm/key hash used for encryption(if the user used SSE-C)
}

using R2GetResponse = R2HeadResponse;
Expand All @@ -230,12 +246,16 @@ struct R2CreateMultipartUploadResponse {
uploadId @0 :Text;
# The unique identifier of this object, required for subsequent operations on
# this multipart upload.
ssec @1 :R2SSECResponse;
# The algorithm/key hash used for encryption(if the user used SSE-C)
}

struct R2UploadPartResponse {
etag @0 :Text;
# The ETag the of the uploaded part.
# This ETag is required in order to complete the multipart upload.
ssec @1 :R2SSECResponse;
# The algorithm/key hash used for encryption(if the user used SSE-C)
}

using R2CompleteMultipartUploadResponse = R2PutResponse;
Expand Down
47 changes: 46 additions & 1 deletion src/workerd/api/r2-bucket.c++
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "util.h"
#include <array>
#include <math.h>
#include <regex>
#include <workerd/api/http.h>
#include <workerd/api/streams.h>
#include <workerd/util/mimetype.h>
Expand All @@ -17,7 +18,9 @@
#include <capnp/compat/json.h>
#include <workerd/util/http-util.h>
#include <workerd/api/r2-api.capnp.h>

#include "kj/encoding.h"
#include "workerd/jsg/exception.h"
#include "workerd/jsg/jsg.h"
namespace workerd::api::public_beta {
static bool isWholeNumber(double x) {
double intpart;
Expand Down Expand Up @@ -298,6 +301,20 @@ void initGetOptions(jsg::Lock& js, Builder& builder, Options& o) {
}
}
}
KJ_IF_SOME(ssec, o.ssec) {
auto ssecBuilder = builder.initSsec();
KJ_SWITCH_ONEOF(ssec.key) {
KJ_CASE_ONEOF(keyString, kj::String) {
JSG_REQUIRE(std::regex_match(keyString.begin(), keyString.end(), std::regex("[0-9a-f]")), Error, "SSE-C Key has invalid format");
JSG_REQUIRE(keyString.size() == 64, Error, "SSE-C Key must be 32 bytes in length");
ssecBuilder.setKey(kj::str(keyString));
}
KJ_CASE_ONEOF(keyBuff, kj::Array<byte>) {
JSG_REQUIRE(keyBuff.size() == 32, Error, "SSE-C Key must be 32 bytes in length");
ssecBuilder.setKey(kj::encodeHex(keyBuff));
}
}
}
}

static bool isQuotedEtag(kj::StringPtr etag) {
Expand Down Expand Up @@ -551,6 +568,20 @@ R2Bucket::put(jsg::Lock& js, kj::String name, kj::Maybe<R2PutValue> value,
KJ_IF_SOME(s, o.storageClass) {
putBuilder.setStorageClass(s);
}
KJ_IF_SOME(ssec, o.ssec) {
auto ssecBuilder = putBuilder.initSsec();
KJ_SWITCH_ONEOF(ssec.key) {
KJ_CASE_ONEOF(keyString, kj::String) {
JSG_REQUIRE(std::regex_match(keyString.begin(), keyString.end(), std::regex("[0-9a-f]")), Error, "SSE-C Key has invalid format");
JSG_REQUIRE(keyString.size() == 64, Error, "SSE-C Key must be 32 bytes in length");
ssecBuilder.setKey(kj::str(keyString));
}
KJ_CASE_ONEOF(keyBuff, kj::Array<byte>) {
JSG_REQUIRE(keyBuff.size() == 32, Error, "SSE-C Key must be 32 bytes in length");
ssecBuilder.setKey(kj::encodeHex(keyBuff));
}
}
}
}

auto requestJson = json.encode(requestBuilder);
Expand Down Expand Up @@ -642,6 +673,20 @@ jsg::Promise<jsg::Ref<R2MultipartUpload>> R2Bucket::createMultipartUpload(jsg::L
KJ_IF_SOME(s, o.storageClass) {
createMultipartUploadBuilder.setStorageClass(s);
}
KJ_IF_SOME(ssec, o.ssec) {
auto ssecBuilder = createMultipartUploadBuilder.initSsec();
KJ_SWITCH_ONEOF(ssec.key) {
KJ_CASE_ONEOF(keyString, kj::String) {
JSG_REQUIRE(std::regex_match(keyString.begin(), keyString.end(), std::regex("[0-9a-f]")), Error, "SSE-C Key has invalid format");
JSG_REQUIRE(keyString.size() == 64, Error, "SSE-C Key must be 32 bytes in length");
ssecBuilder.setKey(kj::str(keyString));
}
KJ_CASE_ONEOF(keyBuff, kj::Array<byte>) {
JSG_REQUIRE(keyBuff.size() == 32, Error, "SSE-C Key must be 32 bytes in length");
ssecBuilder.setKey(kj::encodeHex(keyBuff));
}
}
}
}

auto requestJson = json.encode(requestBuilder);
Expand Down
14 changes: 12 additions & 2 deletions src/workerd/api/r2-bucket.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#include <workerd/jsg/jsg.h>
#include "streams.h"
#include "workerd/jsg/value.h"
#include <workerd/api/r2-api.capnp.h>
#include <capnp/compat/json.h>
#include <workerd/util/http-util.h>
Expand Down Expand Up @@ -70,11 +71,18 @@ class R2Bucket: public jsg::Object {
JSG_STRUCT_TS_OVERRIDE(R2Conditional);
};

struct SSECOptions {
kj::OneOf<kj::Array<byte>, kj::String> key;
JSG_STRUCT(key);
JSG_STRUCT_TS_OVERRIDE(R2SSECOptions);
};

struct GetOptions {
jsg::Optional<kj::OneOf<Conditional, jsg::Ref<Headers>>> onlyIf;
jsg::Optional<kj::OneOf<Range, jsg::Ref<Headers>>> range;
jsg::Optional<SSECOptions> ssec;

JSG_STRUCT(onlyIf, range);
JSG_STRUCT(onlyIf, range, ssec);
JSG_STRUCT_TS_OVERRIDE(R2GetOptions);
};

Expand Down Expand Up @@ -172,15 +180,17 @@ class R2Bucket: public jsg::Object {
jsg::Optional<kj::OneOf<kj::Array<kj::byte>, jsg::NonCoercible<kj::String>>> sha384;
jsg::Optional<kj::OneOf<kj::Array<kj::byte>, jsg::NonCoercible<kj::String>>> sha512;
jsg::Optional<kj::String> storageClass;
jsg::Optional<SSECOptions> ssec;

JSG_STRUCT(onlyIf, httpMetadata, customMetadata, md5, sha1, sha256, sha384, sha512, storageClass);
JSG_STRUCT(onlyIf, httpMetadata, customMetadata, md5, sha1, sha256, sha384, sha512, storageClass, ssec);
JSG_STRUCT_TS_OVERRIDE(R2PutOptions);
};

struct MultipartOptions {
jsg::Optional<kj::OneOf<HttpMetadata, jsg::Ref<Headers>>> httpMetadata;
jsg::Optional<jsg::Dict<kj::String>> customMetadata;
jsg::Optional<kj::String> storageClass;
jsg::Optional<SSECOptions> ssec;

JSG_STRUCT(httpMetadata, customMetadata, storageClass);
JSG_STRUCT_TS_OVERRIDE(R2MultipartOptions);
Expand Down
1 change: 1 addition & 0 deletions src/workerd/api/r2.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ namespace workerd::api::public_beta {
api::public_beta::R2Bucket::GetResult, \
api::public_beta::R2Bucket::Range, \
api::public_beta::R2Bucket::Conditional, \
api::public_beta::R2Bucket::SSECOptions, \
api::public_beta::R2Bucket::GetOptions, \
api::public_beta::R2Bucket::PutOptions, \
api::public_beta::R2Bucket::MultipartOptions, \
Expand Down

0 comments on commit e99deb5

Please sign in to comment.