]>
Commit | Line | Data |
---|---|---|
7c673cae | 1 | // -*- mode:C; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- |
9f95a23c | 2 | // vim: ts=8 sw=2 smarttab ft=cpp |
11fdf7f2 | 3 | |
7c673cae FG |
4 | /** |
5 | * Crypto filters for Put/Post/Get operations. | |
6 | */ | |
11fdf7f2 | 7 | |
f67539c2 TL |
8 | #include <string_view> |
9 | ||
7c673cae FG |
10 | #include <rgw/rgw_op.h> |
11 | #include <rgw/rgw_crypt.h> | |
12 | #include <auth/Crypto.h> | |
13 | #include <rgw/rgw_b64.h> | |
14 | #include <rgw/rgw_rest_s3.h> | |
11fdf7f2 | 15 | #include "include/ceph_assert.h" |
7c673cae FG |
16 | #include "crypto/crypto_accel.h" |
17 | #include "crypto/crypto_plugin.h" | |
9f95a23c | 18 | #include "rgw/rgw_kms.h" |
f67539c2 TL |
19 | #include "rapidjson/document.h" |
20 | #include "rapidjson/writer.h" | |
21 | #include "rapidjson/error/error.h" | |
22 | #include "rapidjson/error/en.h" | |
23 | #include <unicode/normalizer2.h> // libicu | |
9f95a23c TL |
24 | |
25 | #include <openssl/evp.h> | |
7c673cae | 26 | |
7c673cae FG |
27 | #define dout_context g_ceph_context |
28 | #define dout_subsys ceph_subsys_rgw | |
29 | ||
30 | using namespace rgw; | |
31 | ||
7c673cae | 32 | |
f67539c2 TL |
33 | template<typename M> |
34 | class canonical_char_sorter { | |
35 | private: | |
36 | const icu::Normalizer2* normalizer; | |
37 | CephContext *cct; | |
38 | ||
39 | public: | |
40 | canonical_char_sorter(CephContext *cct) : cct(cct) { | |
41 | UErrorCode status = U_ZERO_ERROR; | |
42 | normalizer = icu::Normalizer2::getNFCInstance(status); | |
43 | if (U_FAILURE(status)) { | |
44 | lderr(cct) << "ERROR: can't get nfc instance, error = " << status << dendl; | |
45 | normalizer = 0; | |
46 | } | |
47 | } | |
48 | bool compare_helper (const M *, const M *); | |
49 | bool make_string_canonical(rapidjson::Value &, | |
50 | rapidjson::Document::AllocatorType&); | |
51 | }; | |
52 | ||
53 | template<typename M> | |
54 | bool | |
55 | canonical_char_sorter<M>::compare_helper (const M*a, const M*b) | |
56 | { | |
57 | UErrorCode status = U_ZERO_ERROR; | |
58 | const std::string as{a->name.GetString(), a->name.GetStringLength()}, | |
59 | bs{b->name.GetString(), b->name.GetStringLength()}; | |
60 | icu::UnicodeString aw{icu::UnicodeString::fromUTF8(as)}, bw{icu::UnicodeString::fromUTF8(bs)}; | |
61 | int32_t afl{ aw.countChar32()}, bfl{bw.countChar32()}; | |
62 | std::u32string af, bf; | |
63 | af.resize(afl); bf.resize(bfl); | |
64 | auto *astr{af.c_str()}, *bstr{bf.c_str()}; | |
65 | aw.toUTF32((int32_t*)astr, afl, status); | |
66 | bw.toUTF32((int32_t*)bstr, bfl, status); | |
67 | bool r{af < bf}; | |
68 | return r; | |
69 | } | |
70 | ||
71 | template<typename M> | |
72 | bool | |
73 | canonical_char_sorter<M>::make_string_canonical (rapidjson::Value &v, rapidjson::Document::AllocatorType&a) | |
74 | { | |
75 | UErrorCode status = U_ZERO_ERROR; | |
76 | const std::string as{v.GetString(), v.GetStringLength()}; | |
77 | ||
78 | if (!normalizer) | |
79 | return false; | |
80 | const icu::UnicodeString aw{icu::UnicodeString::fromUTF8(as)}; | |
81 | icu::UnicodeString an{normalizer->normalize(aw, status)}; | |
82 | if (U_FAILURE(status)) { | |
83 | ldout(cct, 5) << "conversion error; code=" << status << | |
84 | " on string " << as << dendl; | |
85 | return false; | |
86 | } | |
87 | std::string ans; | |
88 | an.toUTF8String(ans); | |
89 | v.SetString(ans.c_str(), ans.length(), a); | |
90 | return true; | |
91 | } | |
92 | ||
93 | typedef | |
94 | rapidjson::GenericMember<rapidjson::UTF8<>, rapidjson::MemoryPoolAllocator<> > | |
95 | MyMember; | |
96 | ||
97 | template<typename H> | |
98 | bool | |
99 | sort_and_write(rapidjson::Value &d, H &writer, canonical_char_sorter<MyMember>& ccs) | |
100 | { | |
101 | bool r; | |
102 | switch(d.GetType()) { | |
103 | case rapidjson::kObjectType: { | |
104 | struct comparer { | |
105 | canonical_char_sorter<MyMember> &r; | |
106 | comparer(canonical_char_sorter<MyMember> &r) : r(r) {}; | |
107 | bool operator()(const MyMember*a, const MyMember*b) { | |
108 | return r.compare_helper(a,b); | |
109 | } | |
110 | } cmp_functor{ccs}; | |
111 | if (!(r = writer.StartObject())) | |
112 | break; | |
113 | const auto &o{d.GetObject()}; | |
114 | auto b{o.begin()},e{o.end()}; | |
115 | std::vector<MyMember*> q; | |
116 | for (auto &m: d.GetObject()) | |
117 | q.push_back(&m); | |
118 | std::sort(q.begin(), q.end(), cmp_functor); | |
119 | for (auto m: q) { | |
120 | assert(m->name.IsString()); | |
121 | if (!(r = writer.Key(m->name.GetString(), m->name.GetStringLength()))) | |
122 | goto Done; | |
123 | if (!(r = sort_and_write(m->value, writer, ccs))) | |
124 | goto Done; | |
125 | } | |
126 | r = writer.EndObject(); | |
127 | break; } | |
128 | case rapidjson::kArrayType: | |
129 | if (!(r = writer.StartArray())) | |
130 | break; | |
131 | for (auto &v: d.GetArray()) { | |
132 | if (!(r = sort_and_write(v, writer, ccs))) | |
133 | goto Done; | |
134 | } | |
135 | r = writer.EndArray(); | |
136 | break; | |
137 | default: | |
138 | r = d.Accept(writer); | |
139 | break; | |
140 | } | |
141 | Done: | |
142 | return r; | |
143 | } | |
144 | ||
145 | enum struct mec_option { | |
146 | empty = 0, number_ok = 1 | |
147 | }; | |
148 | ||
149 | enum struct mec_error { | |
150 | success = 0, conversion, number | |
151 | }; | |
152 | ||
153 | mec_error | |
154 | make_everything_canonical(rapidjson::Value &d, rapidjson::Document::AllocatorType&a, canonical_char_sorter<MyMember>& ccs, mec_option f = mec_option::empty ) | |
155 | { | |
156 | mec_error r; | |
157 | switch(d.GetType()) { | |
158 | case rapidjson::kObjectType: | |
159 | for (auto &m: d.GetObject()) { | |
160 | assert(m.name.IsString()); | |
161 | if (!ccs.make_string_canonical(m.name, a)) { | |
162 | r = mec_error::conversion; | |
163 | goto Error; | |
164 | } | |
165 | if ((r = make_everything_canonical(m.value, a, ccs, f)) != mec_error::success) | |
166 | goto Error; | |
167 | } | |
168 | break; | |
169 | case rapidjson::kArrayType: | |
170 | for (auto &v: d.GetArray()) { | |
171 | if ((r = make_everything_canonical(v, a, ccs, f)) != mec_error::success) | |
172 | goto Error; | |
173 | } | |
174 | break; | |
175 | case rapidjson::kStringType: | |
176 | if (!ccs.make_string_canonical(d, a)) { | |
177 | r = mec_error::conversion; | |
178 | goto Error; | |
179 | } | |
180 | break; | |
181 | case rapidjson::kNumberType: | |
182 | if (static_cast<int>(f) & static_cast<int>(mec_option::number_ok)) | |
183 | break; | |
184 | r = mec_error::number; | |
185 | goto Error; | |
186 | default: | |
187 | break; | |
188 | } | |
189 | r = mec_error::success; | |
190 | Error: | |
191 | return r; | |
192 | } | |
193 | ||
194 | bool | |
195 | add_object_to_context(rgw_obj &obj, rapidjson::Document &d) | |
196 | { | |
197 | ARN a{obj}; | |
198 | const char aws_s3_arn[] { "aws:s3:arn" }; | |
199 | std::string as{a.to_string()}; | |
200 | rapidjson::Document::AllocatorType &allocator { d.GetAllocator() }; | |
201 | rapidjson::Value name, val; | |
202 | ||
203 | if (!d.IsObject()) | |
204 | return false; | |
205 | if (d.HasMember(aws_s3_arn)) | |
206 | return true; | |
207 | val.SetString(as.c_str(), as.length(), allocator); | |
208 | name.SetString(aws_s3_arn, sizeof aws_s3_arn - 1, allocator); | |
209 | d.AddMember(name, val, allocator); | |
210 | return true; | |
211 | } | |
212 | ||
213 | static inline const std::string & | |
214 | get_tenant_or_id(req_state *s) | |
215 | { | |
216 | const std::string &tenant{ s->user->get_tenant() }; | |
217 | if (!tenant.empty()) return tenant; | |
218 | return s->user->get_id().id; | |
219 | } | |
220 | ||
221 | int | |
222 | make_canonical_context(struct req_state *s, | |
223 | std::string_view &context, | |
224 | std::string &cooked_context) | |
225 | { | |
226 | rapidjson::Document d; | |
227 | bool b = false; | |
228 | mec_option options { | |
229 | //mec_option::number_ok : SEE BOTTOM OF FILE | |
230 | mec_option::empty }; | |
231 | rgw_obj obj; | |
232 | std::ostringstream oss; | |
233 | canonical_char_sorter<MyMember> ccs{s->cct}; | |
234 | ||
235 | obj.bucket.tenant = get_tenant_or_id(s); | |
236 | obj.bucket.name = s->bucket->get_name(); | |
237 | obj.key.name = s->object->get_name(); | |
238 | std::string iline; | |
239 | rapidjson::Document::AllocatorType &allocator { d.GetAllocator() }; | |
240 | ||
241 | try { | |
242 | iline = rgw::from_base64(context); | |
243 | } catch (const std::exception& e) { | |
244 | oss << "bad context: " << e.what(); | |
245 | s->err.message = oss.str(); | |
246 | return -ERR_INVALID_REQUEST; | |
247 | } | |
248 | rapidjson::StringStream isw(iline.c_str()); | |
249 | if (!iline.length()) | |
250 | d.SetObject(); | |
251 | // else if (qflag) SEE BOTTOM OF FILE | |
252 | // d.ParseStream<rapidjson::kParseNumbersAsStringsFlag>(isw); | |
253 | else | |
254 | d.ParseStream<rapidjson::kParseFullPrecisionFlag>(isw); | |
255 | if (isw.Tell() != iline.length()) { | |
256 | oss << "bad context: did not consume all of input: @ " | |
257 | << isw.Tell(); | |
258 | s->err.message = oss.str(); | |
259 | return -ERR_INVALID_REQUEST; | |
260 | } | |
261 | if (d.HasParseError()) { | |
262 | oss << "bad context: parse error: @ " << d.GetErrorOffset() | |
263 | << " " << rapidjson::GetParseError_En(d.GetParseError()); | |
264 | s->err.message = oss.str(); | |
265 | return -ERR_INVALID_REQUEST; | |
266 | } | |
267 | rapidjson::StringBuffer buf; | |
268 | rapidjson::Writer<rapidjson::StringBuffer> writer(buf); | |
269 | if (!add_object_to_context(obj, d)) { | |
270 | lderr(s->cct) << "ERROR: can't add default value to context" << dendl; | |
271 | s->err.message = "context: internal error adding defaults"; | |
272 | return -ERR_INVALID_REQUEST; | |
273 | } | |
274 | b = make_everything_canonical(d, allocator, ccs, options) == mec_error::success; | |
275 | if (!b) { | |
276 | lderr(s->cct) << "ERROR: can't make canonical json <" | |
277 | << context << ">" << dendl; | |
278 | s->err.message = "context: can't make canonical"; | |
279 | return -ERR_INVALID_REQUEST; | |
280 | } | |
281 | b = sort_and_write(d, writer, ccs); | |
282 | if (!b) { | |
283 | ldout(s->cct, 5) << "format error <" << context | |
284 | << ">: partial.results=" << buf.GetString() << dendl; | |
285 | s->err.message = "unable to reformat json"; | |
286 | return -ERR_INVALID_REQUEST; | |
287 | } | |
288 | cooked_context = rgw::to_base64(buf.GetString()); | |
289 | return 0; | |
290 | } | |
291 | ||
292 | ||
7c673cae FG |
293 | CryptoAccelRef get_crypto_accel(CephContext *cct) |
294 | { | |
295 | CryptoAccelRef ca_impl = nullptr; | |
296 | stringstream ss; | |
297 | PluginRegistry *reg = cct->get_plugin_registry(); | |
298 | string crypto_accel_type = cct->_conf->plugin_crypto_accelerator; | |
299 | ||
300 | CryptoPlugin *factory = dynamic_cast<CryptoPlugin*>(reg->get_with_load("crypto", crypto_accel_type)); | |
301 | if (factory == nullptr) { | |
302 | lderr(cct) << __func__ << " cannot load crypto accelerator of type " << crypto_accel_type << dendl; | |
303 | return nullptr; | |
304 | } | |
305 | int err = factory->factory(&ca_impl, &ss); | |
306 | if (err) { | |
307 | lderr(cct) << __func__ << " factory return error " << err << | |
308 | " with description: " << ss.str() << dendl; | |
309 | } | |
310 | return ca_impl; | |
311 | } | |
312 | ||
313 | ||
9f95a23c TL |
314 | template <std::size_t KeySizeV, std::size_t IvSizeV> |
315 | static inline | |
316 | bool evp_sym_transform(CephContext* const cct, | |
317 | const EVP_CIPHER* const type, | |
318 | unsigned char* const out, | |
319 | const unsigned char* const in, | |
320 | const size_t size, | |
321 | const unsigned char* const iv, | |
322 | const unsigned char* const key, | |
323 | const bool encrypt) | |
324 | { | |
325 | using pctx_t = \ | |
326 | std::unique_ptr<EVP_CIPHER_CTX, decltype(&::EVP_CIPHER_CTX_free)>; | |
327 | pctx_t pctx{ EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free }; | |
328 | ||
329 | if (!pctx) { | |
330 | return false; | |
331 | } | |
332 | ||
333 | if (1 != EVP_CipherInit_ex(pctx.get(), type, nullptr, | |
334 | nullptr, nullptr, encrypt)) { | |
335 | ldout(cct, 5) << "EVP: failed to 1st initialization stage" << dendl; | |
336 | return false; | |
337 | } | |
338 | ||
339 | // we want to support ciphers that don't use IV at all like AES-256-ECB | |
340 | if constexpr (static_cast<bool>(IvSizeV)) { | |
341 | ceph_assert(EVP_CIPHER_CTX_iv_length(pctx.get()) == IvSizeV); | |
342 | ceph_assert(EVP_CIPHER_CTX_block_size(pctx.get()) == IvSizeV); | |
343 | } | |
344 | ceph_assert(EVP_CIPHER_CTX_key_length(pctx.get()) == KeySizeV); | |
345 | ||
346 | if (1 != EVP_CipherInit_ex(pctx.get(), nullptr, nullptr, key, iv, encrypt)) { | |
347 | ldout(cct, 5) << "EVP: failed to 2nd initialization stage" << dendl; | |
348 | return false; | |
349 | } | |
350 | ||
351 | // disable padding | |
352 | if (1 != EVP_CIPHER_CTX_set_padding(pctx.get(), 0)) { | |
353 | ldout(cct, 5) << "EVP: cannot disable PKCS padding" << dendl; | |
354 | return false; | |
355 | } | |
356 | ||
357 | // operate! | |
358 | int written = 0; | |
359 | ceph_assert(size <= static_cast<size_t>(std::numeric_limits<int>::max())); | |
360 | if (1 != EVP_CipherUpdate(pctx.get(), out, &written, in, size)) { | |
361 | ldout(cct, 5) << "EVP: EVP_CipherUpdate failed" << dendl; | |
362 | return false; | |
363 | } | |
364 | ||
365 | int finally_written = 0; | |
366 | static_assert(sizeof(*out) == 1); | |
367 | if (1 != EVP_CipherFinal_ex(pctx.get(), out + written, &finally_written)) { | |
368 | ldout(cct, 5) << "EVP: EVP_CipherFinal_ex failed" << dendl; | |
369 | return false; | |
370 | } | |
371 | ||
372 | // padding is disabled so EVP_CipherFinal_ex should not append anything | |
373 | ceph_assert(finally_written == 0); | |
374 | return (written + finally_written) == static_cast<int>(size); | |
375 | } | |
376 | ||
377 | ||
7c673cae FG |
378 | /** |
379 | * Encryption in CBC mode. Chunked to 4K blocks. Offset is used as IV for each 4K block. | |
380 | * | |
381 | * | |
382 | * | |
383 | * A. Encryption | |
384 | * 1. Input is split to 4K chunks + remainder in one, smaller chunk | |
385 | * 2. Each full chunk is encrypted separately with CBC chained mode, with initial IV derived from offset | |
386 | * 3. Last chunk is 16*m + n. | |
387 | * 4. 16*m bytes are encrypted with CBC chained mode, with initial IV derived from offset | |
388 | * 5. Last n bytes are xor-ed with pattern obtained by CBC encryption of | |
389 | * last encrypted 16 byte block <16m-16, 16m-15) with IV = {0}. | |
390 | * 6. (Special case) If m == 0 then last n bytes are xor-ed with pattern | |
391 | * obtained by CBC encryption of {0} with IV derived from offset | |
392 | * | |
393 | * B. Decryption | |
394 | * 1. Input is split to 4K chunks + remainder in one, smaller chunk | |
395 | * 2. Each full chunk is decrypted separately with CBC chained mode, with initial IV derived from offset | |
396 | * 3. Last chunk is 16*m + n. | |
397 | * 4. 16*m bytes are decrypted with CBC chained mode, with initial IV derived from offset | |
398 | * 5. Last n bytes are xor-ed with pattern obtained by CBC ENCRYPTION of | |
399 | * last (still encrypted) 16 byte block <16m-16,16m-15) with IV = {0} | |
400 | * 6. (Special case) If m == 0 then last n bytes are xor-ed with pattern | |
401 | * obtained by CBC ENCRYPTION of {0} with IV derived from offset | |
402 | */ | |
7c673cae FG |
403 | class AES_256_CBC : public BlockCrypt { |
404 | public: | |
405 | static const size_t AES_256_KEYSIZE = 256 / 8; | |
406 | static const size_t AES_256_IVSIZE = 128 / 8; | |
407 | static const size_t CHUNK_SIZE = 4096; | |
408 | private: | |
409 | static const uint8_t IV[AES_256_IVSIZE]; | |
410 | CephContext* cct; | |
411 | uint8_t key[AES_256_KEYSIZE]; | |
412 | public: | |
11fdf7f2 | 413 | explicit AES_256_CBC(CephContext* cct): cct(cct) { |
7c673cae FG |
414 | } |
415 | ~AES_256_CBC() { | |
92f5a8d4 | 416 | ::ceph::crypto::zeroize_for_security(key, AES_256_KEYSIZE); |
7c673cae FG |
417 | } |
418 | bool set_key(const uint8_t* _key, size_t key_size) { | |
419 | if (key_size != AES_256_KEYSIZE) { | |
420 | return false; | |
421 | } | |
422 | memcpy(key, _key, AES_256_KEYSIZE); | |
423 | return true; | |
424 | } | |
425 | size_t get_block_size() { | |
426 | return CHUNK_SIZE; | |
427 | } | |
428 | ||
7c673cae FG |
429 | bool cbc_transform(unsigned char* out, |
430 | const unsigned char* in, | |
9f95a23c | 431 | const size_t size, |
7c673cae FG |
432 | const unsigned char (&iv)[AES_256_IVSIZE], |
433 | const unsigned char (&key)[AES_256_KEYSIZE], | |
434 | bool encrypt) | |
435 | { | |
9f95a23c TL |
436 | return evp_sym_transform<AES_256_KEYSIZE, AES_256_IVSIZE>( |
437 | cct, EVP_aes_256_cbc(), out, in, size, iv, key, encrypt); | |
7c673cae FG |
438 | } |
439 | ||
7c673cae FG |
440 | bool cbc_transform(unsigned char* out, |
441 | const unsigned char* in, | |
442 | size_t size, | |
443 | off_t stream_offset, | |
444 | const unsigned char (&key)[AES_256_KEYSIZE], | |
445 | bool encrypt) | |
446 | { | |
447 | static std::atomic<bool> failed_to_get_crypto(false); | |
448 | CryptoAccelRef crypto_accel; | |
449 | if (! failed_to_get_crypto.load()) | |
450 | { | |
451 | crypto_accel = get_crypto_accel(cct); | |
452 | if (!crypto_accel) | |
453 | failed_to_get_crypto = true; | |
454 | } | |
455 | bool result = true; | |
456 | unsigned char iv[AES_256_IVSIZE]; | |
457 | for (size_t offset = 0; result && (offset < size); offset += CHUNK_SIZE) { | |
458 | size_t process_size = offset + CHUNK_SIZE <= size ? CHUNK_SIZE : size - offset; | |
459 | prepare_iv(iv, stream_offset + offset); | |
460 | if (crypto_accel != nullptr) { | |
461 | if (encrypt) { | |
462 | result = crypto_accel->cbc_encrypt(out + offset, in + offset, | |
463 | process_size, iv, key); | |
464 | } else { | |
465 | result = crypto_accel->cbc_decrypt(out + offset, in + offset, | |
466 | process_size, iv, key); | |
467 | } | |
468 | } else { | |
469 | result = cbc_transform( | |
470 | out + offset, in + offset, process_size, | |
471 | iv, key, encrypt); | |
472 | } | |
473 | } | |
474 | return result; | |
475 | } | |
476 | ||
477 | ||
478 | bool encrypt(bufferlist& input, | |
479 | off_t in_ofs, | |
480 | size_t size, | |
481 | bufferlist& output, | |
482 | off_t stream_offset) | |
483 | { | |
484 | bool result = false; | |
485 | size_t aligned_size = size / AES_256_IVSIZE * AES_256_IVSIZE; | |
486 | size_t unaligned_rest_size = size - aligned_size; | |
487 | output.clear(); | |
488 | buffer::ptr buf(aligned_size + AES_256_IVSIZE); | |
489 | unsigned char* buf_raw = reinterpret_cast<unsigned char*>(buf.c_str()); | |
490 | const unsigned char* input_raw = reinterpret_cast<const unsigned char*>(input.c_str()); | |
491 | ||
492 | /* encrypt main bulk of data */ | |
493 | result = cbc_transform(buf_raw, | |
494 | input_raw + in_ofs, | |
495 | aligned_size, | |
496 | stream_offset, key, true); | |
497 | if (result && (unaligned_rest_size > 0)) { | |
498 | /* remainder to encrypt */ | |
499 | if (aligned_size % CHUNK_SIZE > 0) { | |
500 | /* use last chunk for unaligned part */ | |
501 | unsigned char iv[AES_256_IVSIZE] = {0}; | |
502 | result = cbc_transform(buf_raw + aligned_size, | |
503 | buf_raw + aligned_size - AES_256_IVSIZE, | |
504 | AES_256_IVSIZE, | |
505 | iv, key, true); | |
506 | } else { | |
507 | /* 0 full blocks in current chunk, use IV as base for unaligned part */ | |
508 | unsigned char iv[AES_256_IVSIZE] = {0}; | |
509 | unsigned char data[AES_256_IVSIZE]; | |
510 | prepare_iv(data, stream_offset + aligned_size); | |
511 | result = cbc_transform(buf_raw + aligned_size, | |
512 | data, | |
513 | AES_256_IVSIZE, | |
514 | iv, key, true); | |
515 | } | |
516 | if (result) { | |
517 | for(size_t i = aligned_size; i < size; i++) { | |
518 | *(buf_raw + i) ^= *(input_raw + in_ofs + i); | |
519 | } | |
520 | } | |
521 | } | |
522 | if (result) { | |
523 | ldout(cct, 25) << "Encrypted " << size << " bytes"<< dendl; | |
524 | buf.set_length(size); | |
525 | output.append(buf); | |
526 | } else { | |
527 | ldout(cct, 5) << "Failed to encrypt" << dendl; | |
528 | } | |
529 | return result; | |
530 | } | |
531 | ||
532 | ||
533 | bool decrypt(bufferlist& input, | |
534 | off_t in_ofs, | |
535 | size_t size, | |
536 | bufferlist& output, | |
537 | off_t stream_offset) | |
538 | { | |
539 | bool result = false; | |
540 | size_t aligned_size = size / AES_256_IVSIZE * AES_256_IVSIZE; | |
541 | size_t unaligned_rest_size = size - aligned_size; | |
542 | output.clear(); | |
543 | buffer::ptr buf(aligned_size + AES_256_IVSIZE); | |
544 | unsigned char* buf_raw = reinterpret_cast<unsigned char*>(buf.c_str()); | |
545 | unsigned char* input_raw = reinterpret_cast<unsigned char*>(input.c_str()); | |
546 | ||
547 | /* decrypt main bulk of data */ | |
548 | result = cbc_transform(buf_raw, | |
549 | input_raw + in_ofs, | |
550 | aligned_size, | |
551 | stream_offset, key, false); | |
552 | if (result && unaligned_rest_size > 0) { | |
553 | /* remainder to decrypt */ | |
554 | if (aligned_size % CHUNK_SIZE > 0) { | |
555 | /*use last chunk for unaligned part*/ | |
556 | unsigned char iv[AES_256_IVSIZE] = {0}; | |
557 | result = cbc_transform(buf_raw + aligned_size, | |
558 | input_raw + in_ofs + aligned_size - AES_256_IVSIZE, | |
559 | AES_256_IVSIZE, | |
560 | iv, key, true); | |
561 | } else { | |
562 | /* 0 full blocks in current chunk, use IV as base for unaligned part */ | |
563 | unsigned char iv[AES_256_IVSIZE] = {0}; | |
564 | unsigned char data[AES_256_IVSIZE]; | |
565 | prepare_iv(data, stream_offset + aligned_size); | |
566 | result = cbc_transform(buf_raw + aligned_size, | |
567 | data, | |
568 | AES_256_IVSIZE, | |
569 | iv, key, true); | |
570 | } | |
571 | if (result) { | |
572 | for(size_t i = aligned_size; i < size; i++) { | |
573 | *(buf_raw + i) ^= *(input_raw + in_ofs + i); | |
574 | } | |
575 | } | |
576 | } | |
577 | if (result) { | |
578 | ldout(cct, 25) << "Decrypted " << size << " bytes"<< dendl; | |
579 | buf.set_length(size); | |
580 | output.append(buf); | |
581 | } else { | |
582 | ldout(cct, 5) << "Failed to decrypt" << dendl; | |
583 | } | |
584 | return result; | |
585 | } | |
586 | ||
587 | ||
11fdf7f2 | 588 | void prepare_iv(unsigned char (&iv)[AES_256_IVSIZE], off_t offset) { |
7c673cae FG |
589 | off_t index = offset / AES_256_IVSIZE; |
590 | off_t i = AES_256_IVSIZE - 1; | |
591 | unsigned int val; | |
592 | unsigned int carry = 0; | |
593 | while (i>=0) { | |
594 | val = (index & 0xff) + IV[i] + carry; | |
595 | iv[i] = val; | |
596 | carry = val >> 8; | |
597 | index = index >> 8; | |
598 | i--; | |
599 | } | |
600 | } | |
601 | }; | |
602 | ||
603 | ||
604 | std::unique_ptr<BlockCrypt> AES_256_CBC_create(CephContext* cct, const uint8_t* key, size_t len) | |
605 | { | |
606 | auto cbc = std::unique_ptr<AES_256_CBC>(new AES_256_CBC(cct)); | |
607 | cbc->set_key(key, AES_256_KEYSIZE); | |
9f95a23c | 608 | return cbc; |
7c673cae FG |
609 | } |
610 | ||
611 | ||
612 | const uint8_t AES_256_CBC::IV[AES_256_CBC::AES_256_IVSIZE] = | |
613 | { 'a', 'e', 's', '2', '5', '6', 'i', 'v', '_', 'c', 't', 'r', '1', '3', '3', '7' }; | |
614 | ||
615 | ||
7c673cae FG |
616 | bool AES_256_ECB_encrypt(CephContext* cct, |
617 | const uint8_t* key, | |
618 | size_t key_size, | |
619 | const uint8_t* data_in, | |
620 | uint8_t* data_out, | |
9f95a23c TL |
621 | size_t data_size) |
622 | { | |
7c673cae | 623 | if (key_size == AES_256_KEYSIZE) { |
9f95a23c TL |
624 | return evp_sym_transform<AES_256_KEYSIZE, 0 /* no IV in ECB */>( |
625 | cct, EVP_aes_256_ecb(), data_out, data_in, data_size, | |
626 | nullptr /* no IV in ECB */, key, true /* encrypt */); | |
7c673cae FG |
627 | } else { |
628 | ldout(cct, 5) << "Key size must be 256 bits long" << dendl; | |
9f95a23c | 629 | return false; |
7c673cae | 630 | } |
7c673cae FG |
631 | } |
632 | ||
7c673cae FG |
633 | |
634 | RGWGetObj_BlockDecrypt::RGWGetObj_BlockDecrypt(CephContext* cct, | |
11fdf7f2 | 635 | RGWGetObj_Filter* next, |
7c673cae FG |
636 | std::unique_ptr<BlockCrypt> crypt): |
637 | RGWGetObj_Filter(next), | |
638 | cct(cct), | |
639 | crypt(std::move(crypt)), | |
640 | enc_begin_skip(0), | |
641 | ofs(0), | |
642 | end(0), | |
643 | cache() | |
644 | { | |
645 | block_size = this->crypt->get_block_size(); | |
646 | } | |
647 | ||
648 | RGWGetObj_BlockDecrypt::~RGWGetObj_BlockDecrypt() { | |
649 | } | |
650 | ||
b3b6e05e | 651 | int RGWGetObj_BlockDecrypt::read_manifest(const DoutPrefixProvider *dpp, bufferlist& manifest_bl) { |
7c673cae FG |
652 | parts_len.clear(); |
653 | RGWObjManifest manifest; | |
654 | if (manifest_bl.length()) { | |
11fdf7f2 | 655 | auto miter = manifest_bl.cbegin(); |
7c673cae | 656 | try { |
11fdf7f2 | 657 | decode(manifest, miter); |
7c673cae | 658 | } catch (buffer::error& err) { |
b3b6e05e | 659 | ldpp_dout(dpp, 0) << "ERROR: couldn't decode manifest" << dendl; |
7c673cae FG |
660 | return -EIO; |
661 | } | |
662 | RGWObjManifest::obj_iterator mi; | |
b3b6e05e | 663 | for (mi = manifest.obj_begin(dpp); mi != manifest.obj_end(dpp); ++mi) { |
7c673cae FG |
664 | if (mi.get_cur_stripe() == 0) { |
665 | parts_len.push_back(0); | |
666 | } | |
667 | parts_len.back() += mi.get_stripe_size(); | |
668 | } | |
11fdf7f2 | 669 | if (cct->_conf->subsys.should_gather<ceph_subsys_rgw, 20>()) { |
7c673cae | 670 | for (size_t i = 0; i<parts_len.size(); i++) { |
b3b6e05e | 671 | ldpp_dout(dpp, 20) << "Manifest part " << i << ", size=" << parts_len[i] << dendl; |
7c673cae FG |
672 | } |
673 | } | |
674 | } | |
675 | return 0; | |
676 | } | |
677 | ||
678 | int RGWGetObj_BlockDecrypt::fixup_range(off_t& bl_ofs, off_t& bl_end) { | |
679 | off_t inp_ofs = bl_ofs; | |
680 | off_t inp_end = bl_end; | |
681 | if (parts_len.size() > 0) { | |
682 | off_t in_ofs = bl_ofs; | |
683 | off_t in_end = bl_end; | |
684 | ||
685 | size_t i = 0; | |
a8e16298 | 686 | while (i<parts_len.size() && (in_ofs >= (off_t)parts_len[i])) { |
7c673cae FG |
687 | in_ofs -= parts_len[i]; |
688 | i++; | |
689 | } | |
690 | //in_ofs is inside block i | |
691 | size_t j = 0; | |
a8e16298 | 692 | while (j<(parts_len.size() - 1) && (in_end >= (off_t)parts_len[j])) { |
7c673cae FG |
693 | in_end -= parts_len[j]; |
694 | j++; | |
695 | } | |
a8e16298 | 696 | //in_end is inside part j, OR j is the last part |
7c673cae | 697 | |
a8e16298 TL |
698 | size_t rounded_end = ( in_end & ~(block_size - 1) ) + (block_size - 1); |
699 | if (rounded_end > parts_len[j]) { | |
7c673cae FG |
700 | rounded_end = parts_len[j] - 1; |
701 | } | |
702 | ||
703 | enc_begin_skip = in_ofs & (block_size - 1); | |
704 | ofs = bl_ofs - enc_begin_skip; | |
705 | end = bl_end; | |
7c673cae | 706 | bl_end += rounded_end - in_end; |
a8e16298 | 707 | bl_ofs = std::min(bl_ofs - enc_begin_skip, bl_end); |
7c673cae FG |
708 | } |
709 | else | |
710 | { | |
711 | enc_begin_skip = bl_ofs & (block_size - 1); | |
712 | ofs = bl_ofs & ~(block_size - 1); | |
713 | end = bl_end; | |
714 | bl_ofs = bl_ofs & ~(block_size - 1); | |
715 | bl_end = ( bl_end & ~(block_size - 1) ) + (block_size - 1); | |
716 | } | |
717 | ldout(cct, 20) << "fixup_range [" << inp_ofs << "," << inp_end | |
718 | << "] => [" << bl_ofs << "," << bl_end << "]" << dendl; | |
719 | return 0; | |
720 | } | |
721 | ||
a8e16298 TL |
722 | int RGWGetObj_BlockDecrypt::process(bufferlist& in, size_t part_ofs, size_t size) |
723 | { | |
724 | bufferlist data; | |
725 | if (!crypt->decrypt(in, 0, size, data, part_ofs)) { | |
726 | return -ERR_INTERNAL_ERROR; | |
727 | } | |
728 | off_t send_size = size - enc_begin_skip; | |
729 | if (ofs + enc_begin_skip + send_size > end + 1) { | |
730 | send_size = end + 1 - ofs - enc_begin_skip; | |
731 | } | |
732 | int res = next->handle_data(data, enc_begin_skip, send_size); | |
733 | enc_begin_skip = 0; | |
734 | ofs += size; | |
735 | in.splice(0, size); | |
736 | return res; | |
737 | } | |
7c673cae FG |
738 | |
739 | int RGWGetObj_BlockDecrypt::handle_data(bufferlist& bl, off_t bl_ofs, off_t bl_len) { | |
7c673cae | 740 | ldout(cct, 25) << "Decrypt " << bl_len << " bytes" << dendl; |
9f95a23c | 741 | bl.begin(bl_ofs).copy(bl_len, cache); |
a8e16298 TL |
742 | |
743 | int res = 0; | |
7c673cae | 744 | size_t part_ofs = ofs; |
a8e16298 TL |
745 | for (size_t part : parts_len) { |
746 | if (part_ofs >= part) { | |
747 | part_ofs -= part; | |
748 | } else if (part_ofs + cache.length() >= part) { | |
749 | // flush data up to part boundaries, aligned or not | |
750 | res = process(cache, part_ofs, part - part_ofs); | |
751 | if (res < 0) { | |
752 | return res; | |
753 | } | |
754 | part_ofs = 0; | |
755 | } else { | |
756 | break; | |
757 | } | |
7c673cae | 758 | } |
a8e16298 | 759 | // write up to block boundaries, aligned only |
31f18b77 FG |
760 | off_t aligned_size = cache.length() & ~(block_size - 1); |
761 | if (aligned_size > 0) { | |
a8e16298 | 762 | res = process(cache, part_ofs, aligned_size); |
7c673cae | 763 | } |
31f18b77 | 764 | return res; |
7c673cae FG |
765 | } |
766 | ||
767 | /** | |
768 | * flush remainder of data to output | |
769 | */ | |
770 | int RGWGetObj_BlockDecrypt::flush() { | |
a8e16298 | 771 | ldout(cct, 25) << "Decrypt flushing " << cache.length() << " bytes" << dendl; |
7c673cae FG |
772 | int res = 0; |
773 | size_t part_ofs = ofs; | |
a8e16298 TL |
774 | for (size_t part : parts_len) { |
775 | if (part_ofs >= part) { | |
776 | part_ofs -= part; | |
777 | } else if (part_ofs + cache.length() >= part) { | |
778 | // flush data up to part boundaries, aligned or not | |
779 | res = process(cache, part_ofs, part - part_ofs); | |
780 | if (res < 0) { | |
781 | return res; | |
782 | } | |
783 | part_ofs = 0; | |
784 | } else { | |
785 | break; | |
786 | } | |
7c673cae | 787 | } |
a8e16298 | 788 | // flush up to block boundaries, aligned or not |
7c673cae | 789 | if (cache.length() > 0) { |
a8e16298 | 790 | res = process(cache, part_ofs, cache.length()); |
7c673cae FG |
791 | } |
792 | return res; | |
793 | } | |
794 | ||
795 | RGWPutObj_BlockEncrypt::RGWPutObj_BlockEncrypt(CephContext* cct, | |
11fdf7f2 TL |
796 | rgw::putobj::DataProcessor *next, |
797 | std::unique_ptr<BlockCrypt> crypt) | |
798 | : Pipe(next), | |
7c673cae FG |
799 | cct(cct), |
800 | crypt(std::move(crypt)), | |
11fdf7f2 | 801 | block_size(this->crypt->get_block_size()) |
7c673cae | 802 | { |
7c673cae FG |
803 | } |
804 | ||
11fdf7f2 TL |
805 | int RGWPutObj_BlockEncrypt::process(bufferlist&& data, uint64_t logical_offset) |
806 | { | |
807 | ldout(cct, 25) << "Encrypt " << data.length() << " bytes" << dendl; | |
7c673cae | 808 | |
11fdf7f2 TL |
809 | // adjust logical offset to beginning of cached data |
810 | ceph_assert(logical_offset >= cache.length()); | |
811 | logical_offset -= cache.length(); | |
812 | ||
813 | const bool flush = (data.length() == 0); | |
814 | cache.claim_append(data); | |
31f18b77 | 815 | |
11fdf7f2 TL |
816 | uint64_t proc_size = cache.length() & ~(block_size - 1); |
817 | if (flush) { | |
31f18b77 | 818 | proc_size = cache.length(); |
7c673cae | 819 | } |
31f18b77 | 820 | if (proc_size > 0) { |
11fdf7f2 TL |
821 | bufferlist in, out; |
822 | cache.splice(0, proc_size, &in); | |
823 | if (!crypt->encrypt(in, 0, proc_size, out, logical_offset)) { | |
31f18b77 | 824 | return -ERR_INTERNAL_ERROR; |
7c673cae | 825 | } |
11fdf7f2 TL |
826 | int r = Pipe::process(std::move(out), logical_offset); |
827 | logical_offset += proc_size; | |
828 | if (r < 0) | |
829 | return r; | |
7c673cae | 830 | } |
31f18b77 | 831 | |
11fdf7f2 | 832 | if (flush) { |
7c673cae | 833 | /*replicate 0-sized handle_data*/ |
11fdf7f2 | 834 | return Pipe::process({}, logical_offset); |
7c673cae | 835 | } |
11fdf7f2 | 836 | return 0; |
7c673cae FG |
837 | } |
838 | ||
7c673cae FG |
839 | |
840 | std::string create_random_key_selector(CephContext * const cct) { | |
841 | char random[AES_256_KEYSIZE]; | |
11fdf7f2 | 842 | cct->random()->get_bytes(&random[0], sizeof(random)); |
7c673cae FG |
843 | return std::string(random, sizeof(random)); |
844 | } | |
845 | ||
7c673cae FG |
846 | typedef enum { |
847 | X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_ALGORITHM=0, | |
848 | X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY, | |
849 | X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY_MD5, | |
850 | X_AMZ_SERVER_SIDE_ENCRYPTION, | |
851 | X_AMZ_SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID, | |
f67539c2 | 852 | X_AMZ_SERVER_SIDE_ENCRYPTION_CONTEXT, |
7c673cae FG |
853 | X_AMZ_SERVER_SIDE_ENCRYPTION_LAST |
854 | } crypt_option_e; | |
855 | ||
856 | typedef struct { | |
857 | const char* http_header_name; | |
858 | const std::string post_part_name; | |
859 | } crypt_option_names; | |
860 | ||
861 | static const crypt_option_names crypt_options[] = { | |
862 | {"HTTP_X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_ALGORITHM", "x-amz-server-side-encryption-customer-algorithm"}, | |
863 | {"HTTP_X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY", "x-amz-server-side-encryption-customer-key"}, | |
864 | {"HTTP_X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY_MD5", "x-amz-server-side-encryption-customer-key-md5"}, | |
865 | {"HTTP_X_AMZ_SERVER_SIDE_ENCRYPTION", "x-amz-server-side-encryption"}, | |
866 | {"HTTP_X_AMZ_SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID", "x-amz-server-side-encryption-aws-kms-key-id"}, | |
f67539c2 | 867 | {"HTTP_X_AMZ_SERVER_SIDE_ENCRYPTION_CONTEXT", "x-amz-server-side-encryption-context"}, |
7c673cae FG |
868 | }; |
869 | ||
f67539c2 | 870 | static std::string_view get_crypt_attribute( |
31f18b77 | 871 | const RGWEnv* env, |
7c673cae FG |
872 | std::map<std::string, |
873 | RGWPostObj_ObjStore::post_form_part, | |
874 | const ltstr_nocase>* parts, | |
875 | crypt_option_e option) | |
876 | { | |
877 | static_assert( | |
878 | X_AMZ_SERVER_SIDE_ENCRYPTION_LAST == sizeof(crypt_options)/sizeof(*crypt_options), | |
879 | "Missing items in crypt_options"); | |
880 | if (parts != nullptr) { | |
881 | auto iter | |
882 | = parts->find(crypt_options[option].post_part_name); | |
883 | if (iter == parts->end()) | |
f67539c2 | 884 | return std::string_view(); |
7c673cae | 885 | bufferlist& data = iter->second.data; |
f67539c2 | 886 | std::string_view str = std::string_view(data.c_str(), data.length()); |
7c673cae FG |
887 | return rgw_trim_whitespace(str); |
888 | } else { | |
889 | const char* hdr = env->get(crypt_options[option].http_header_name, nullptr); | |
890 | if (hdr != nullptr) { | |
f67539c2 | 891 | return std::string_view(hdr); |
7c673cae | 892 | } else { |
f67539c2 | 893 | return std::string_view(); |
7c673cae FG |
894 | } |
895 | } | |
896 | } | |
897 | ||
898 | ||
899 | int rgw_s3_prepare_encrypt(struct req_state* s, | |
900 | std::map<std::string, ceph::bufferlist>& attrs, | |
901 | std::map<std::string, | |
902 | RGWPostObj_ObjStore::post_form_part, | |
903 | const ltstr_nocase>* parts, | |
904 | std::unique_ptr<BlockCrypt>* block_crypt, | |
905 | std::map<std::string, std::string>& crypt_http_responses) | |
906 | { | |
907 | int res = 0; | |
908 | crypt_http_responses.clear(); | |
909 | { | |
f67539c2 | 910 | std::string_view req_sse_ca = |
7c673cae FG |
911 | get_crypt_attribute(s->info.env, parts, X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_ALGORITHM); |
912 | if (! req_sse_ca.empty()) { | |
913 | if (req_sse_ca != "AES256") { | |
b3b6e05e | 914 | ldpp_dout(s, 5) << "ERROR: Invalid value for header " |
d2e6a577 FG |
915 | << "x-amz-server-side-encryption-customer-algorithm" |
916 | << dendl; | |
3efd9988 FG |
917 | s->err.message = "The requested encryption algorithm is not valid, must be AES256."; |
918 | return -ERR_INVALID_ENCRYPTION_ALGORITHM; | |
7c673cae FG |
919 | } |
920 | if (s->cct->_conf->rgw_crypt_require_ssl && | |
f64942e4 | 921 | !rgw_transport_is_secure(s->cct, *s->info.env)) { |
b3b6e05e | 922 | ldpp_dout(s, 5) << "ERROR: Insecure request, rgw_crypt_require_ssl is set" << dendl; |
7c673cae FG |
923 | return -ERR_INVALID_REQUEST; |
924 | } | |
3efd9988 FG |
925 | |
926 | std::string key_bin; | |
927 | try { | |
928 | key_bin = from_base64( | |
7c673cae | 929 | get_crypt_attribute(s->info.env, parts, X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY) ); |
3efd9988 | 930 | } catch (...) { |
b3b6e05e | 931 | ldpp_dout(s, 5) << "ERROR: rgw_s3_prepare_encrypt invalid encryption " |
3efd9988 FG |
932 | << "key which contains character that is not base64 encoded." |
933 | << dendl; | |
934 | s->err.message = "Requests specifying Server Side Encryption with Customer " | |
935 | "provided keys must provide an appropriate secret key."; | |
936 | return -EINVAL; | |
937 | } | |
938 | ||
7c673cae | 939 | if (key_bin.size() != AES_256_CBC::AES_256_KEYSIZE) { |
b3b6e05e | 940 | ldpp_dout(s, 5) << "ERROR: invalid encryption key size" << dendl; |
3efd9988 FG |
941 | s->err.message = "Requests specifying Server Side Encryption with Customer " |
942 | "provided keys must provide an appropriate secret key."; | |
943 | return -EINVAL; | |
7c673cae | 944 | } |
3efd9988 | 945 | |
f67539c2 | 946 | std::string_view keymd5 = |
7c673cae | 947 | get_crypt_attribute(s->info.env, parts, X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY_MD5); |
3efd9988 FG |
948 | |
949 | std::string keymd5_bin; | |
950 | try { | |
951 | keymd5_bin = from_base64(keymd5); | |
952 | } catch (...) { | |
b3b6e05e | 953 | ldpp_dout(s, 5) << "ERROR: rgw_s3_prepare_encrypt invalid encryption key " |
3efd9988 FG |
954 | << "md5 which contains character that is not base64 encoded." |
955 | << dendl; | |
956 | s->err.message = "Requests specifying Server Side Encryption with Customer " | |
957 | "provided keys must provide an appropriate secret key md5."; | |
958 | return -EINVAL; | |
959 | } | |
960 | ||
7c673cae | 961 | if (keymd5_bin.size() != CEPH_CRYPTO_MD5_DIGESTSIZE) { |
b3b6e05e | 962 | ldpp_dout(s, 5) << "ERROR: Invalid key md5 size" << dendl; |
3efd9988 FG |
963 | s->err.message = "Requests specifying Server Side Encryption with Customer " |
964 | "provided keys must provide an appropriate secret key md5."; | |
965 | return -EINVAL; | |
7c673cae | 966 | } |
3efd9988 | 967 | |
7c673cae | 968 | MD5 key_hash; |
11fdf7f2 TL |
969 | unsigned char key_hash_res[CEPH_CRYPTO_MD5_DIGESTSIZE]; |
970 | key_hash.Update(reinterpret_cast<const unsigned char*>(key_bin.c_str()), key_bin.size()); | |
7c673cae FG |
971 | key_hash.Final(key_hash_res); |
972 | ||
973 | if (memcmp(key_hash_res, keymd5_bin.c_str(), CEPH_CRYPTO_MD5_DIGESTSIZE) != 0) { | |
b3b6e05e | 974 | ldpp_dout(s, 5) << "ERROR: Invalid key md5 hash" << dendl; |
3efd9988 FG |
975 | s->err.message = "The calculated MD5 hash of the key did not match the hash that was provided."; |
976 | return -EINVAL; | |
7c673cae FG |
977 | } |
978 | ||
979 | set_attr(attrs, RGW_ATTR_CRYPT_MODE, "SSE-C-AES256"); | |
980 | set_attr(attrs, RGW_ATTR_CRYPT_KEYMD5, keymd5_bin); | |
981 | ||
982 | if (block_crypt) { | |
983 | auto aes = std::unique_ptr<AES_256_CBC>(new AES_256_CBC(s->cct)); | |
984 | aes->set_key(reinterpret_cast<const uint8_t*>(key_bin.c_str()), AES_256_KEYSIZE); | |
985 | *block_crypt = std::move(aes); | |
986 | } | |
987 | ||
988 | crypt_http_responses["x-amz-server-side-encryption-customer-algorithm"] = "AES256"; | |
f67539c2 | 989 | crypt_http_responses["x-amz-server-side-encryption-customer-key-MD5"] = std::string(keymd5); |
7c673cae | 990 | return 0; |
3efd9988 | 991 | } else { |
f67539c2 | 992 | std::string_view customer_key = |
3efd9988 FG |
993 | get_crypt_attribute(s->info.env, parts, X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY); |
994 | if (!customer_key.empty()) { | |
b3b6e05e | 995 | ldpp_dout(s, 5) << "ERROR: SSE-C encryption request is missing the header " |
3efd9988 FG |
996 | << "x-amz-server-side-encryption-customer-algorithm" |
997 | << dendl; | |
998 | s->err.message = "Requests specifying Server Side Encryption with Customer " | |
999 | "provided keys must provide a valid encryption algorithm."; | |
1000 | return -EINVAL; | |
1001 | } | |
1002 | ||
f67539c2 | 1003 | std::string_view customer_key_md5 = |
3efd9988 FG |
1004 | get_crypt_attribute(s->info.env, parts, X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY_MD5); |
1005 | if (!customer_key_md5.empty()) { | |
b3b6e05e | 1006 | ldpp_dout(s, 5) << "ERROR: SSE-C encryption request is missing the header " |
3efd9988 FG |
1007 | << "x-amz-server-side-encryption-customer-algorithm" |
1008 | << dendl; | |
1009 | s->err.message = "Requests specifying Server Side Encryption with Customer " | |
1010 | "provided keys must provide a valid encryption algorithm."; | |
1011 | return -EINVAL; | |
1012 | } | |
7c673cae | 1013 | } |
3efd9988 | 1014 | |
7c673cae | 1015 | /* AMAZON server side encryption with KMS (key management service) */ |
f67539c2 | 1016 | std::string_view req_sse = |
7c673cae FG |
1017 | get_crypt_attribute(s->info.env, parts, X_AMZ_SERVER_SIDE_ENCRYPTION); |
1018 | if (! req_sse.empty()) { | |
92f5a8d4 | 1019 | |
7c673cae | 1020 | if (s->cct->_conf->rgw_crypt_require_ssl && |
f64942e4 | 1021 | !rgw_transport_is_secure(s->cct, *s->info.env)) { |
b3b6e05e | 1022 | ldpp_dout(s, 5) << "ERROR: insecure request, rgw_crypt_require_ssl is set" << dendl; |
7c673cae FG |
1023 | return -ERR_INVALID_REQUEST; |
1024 | } | |
92f5a8d4 TL |
1025 | |
1026 | if (req_sse == "aws:kms") { | |
f67539c2 TL |
1027 | std::string_view context = |
1028 | get_crypt_attribute(s->info.env, parts, X_AMZ_SERVER_SIDE_ENCRYPTION_CONTEXT); | |
1029 | std::string cooked_context; | |
1030 | if ((res = make_canonical_context(s, context, cooked_context))) | |
1031 | return res; | |
1032 | std::string_view key_id = | |
7c673cae | 1033 | get_crypt_attribute(s->info.env, parts, X_AMZ_SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID); |
92f5a8d4 | 1034 | if (key_id.empty()) { |
b3b6e05e | 1035 | ldpp_dout(s, 5) << "ERROR: not provide a valid key id" << dendl; |
92f5a8d4 TL |
1036 | s->err.message = "Server Side Encryption with KMS managed key requires " |
1037 | "HTTP header x-amz-server-side-encryption-aws-kms-key-id"; | |
1038 | return -ERR_INVALID_ACCESS_KEY; | |
1039 | } | |
1040 | /* try to retrieve actual key */ | |
1041 | std::string key_selector = create_random_key_selector(s->cct); | |
f67539c2 TL |
1042 | set_attr(attrs, RGW_ATTR_CRYPT_MODE, "SSE-KMS"); |
1043 | set_attr(attrs, RGW_ATTR_CRYPT_KEYID, key_id); | |
1044 | set_attr(attrs, RGW_ATTR_CRYPT_KEYSEL, key_selector); | |
1045 | set_attr(attrs, RGW_ATTR_CRYPT_CONTEXT, cooked_context); | |
92f5a8d4 | 1046 | std::string actual_key; |
f67539c2 | 1047 | res = make_actual_key_from_kms(s->cct, attrs, actual_key); |
92f5a8d4 | 1048 | if (res != 0) { |
b3b6e05e | 1049 | ldpp_dout(s, 5) << "ERROR: failed to retrieve actual key from key_id: " << key_id << dendl; |
f67539c2 | 1050 | s->err.message = "Failed to retrieve the actual key, kms-keyid: " + std::string(key_id); |
92f5a8d4 TL |
1051 | return res; |
1052 | } | |
1053 | if (actual_key.size() != AES_256_KEYSIZE) { | |
b3b6e05e | 1054 | ldpp_dout(s, 5) << "ERROR: key obtained from key_id:" << |
7c673cae | 1055 | key_id << " is not 256 bit size" << dendl; |
92f5a8d4 TL |
1056 | s->err.message = "KMS provided an invalid key for the given kms-keyid."; |
1057 | return -ERR_INVALID_ACCESS_KEY; | |
1058 | } | |
92f5a8d4 TL |
1059 | |
1060 | if (block_crypt) { | |
1061 | auto aes = std::unique_ptr<AES_256_CBC>(new AES_256_CBC(s->cct)); | |
1062 | aes->set_key(reinterpret_cast<const uint8_t*>(actual_key.c_str()), AES_256_KEYSIZE); | |
1063 | *block_crypt = std::move(aes); | |
1064 | } | |
f67539c2 | 1065 | ::ceph::crypto::zeroize_for_security(actual_key.data(), actual_key.length()); |
92f5a8d4 TL |
1066 | |
1067 | crypt_http_responses["x-amz-server-side-encryption"] = "aws:kms"; | |
f67539c2 TL |
1068 | crypt_http_responses["x-amz-server-side-encryption-aws-kms-key-id"] = std::string(key_id); |
1069 | crypt_http_responses["x-amz-server-side-encryption-context"] = std::move(cooked_context); | |
92f5a8d4 TL |
1070 | return 0; |
1071 | } else if (req_sse == "AES256") { | |
1072 | /* if a default encryption key was provided, we will use it for SSE-S3 */ | |
1073 | } else { | |
b3b6e05e | 1074 | ldpp_dout(s, 5) << "ERROR: Invalid value for header x-amz-server-side-encryption" |
92f5a8d4 TL |
1075 | << dendl; |
1076 | s->err.message = "Server Side Encryption with KMS managed key requires " | |
1077 | "HTTP header x-amz-server-side-encryption : aws:kms or AES256"; | |
1078 | return -EINVAL; | |
7c673cae | 1079 | } |
3efd9988 | 1080 | } else { |
92f5a8d4 | 1081 | /* x-amz-server-side-encryption not present or empty */ |
f67539c2 | 1082 | std::string_view key_id = |
92f5a8d4 TL |
1083 | get_crypt_attribute(s->info.env, parts, |
1084 | X_AMZ_SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID); | |
3efd9988 | 1085 | if (!key_id.empty()) { |
b3b6e05e | 1086 | ldpp_dout(s, 5) << "ERROR: SSE-KMS encryption request is missing the header " |
3efd9988 FG |
1087 | << "x-amz-server-side-encryption" |
1088 | << dendl; | |
1089 | s->err.message = "Server Side Encryption with KMS managed key requires " | |
1090 | "HTTP header x-amz-server-side-encryption : aws:kms"; | |
1091 | return -EINVAL; | |
1092 | } | |
7c673cae FG |
1093 | } |
1094 | ||
1095 | /* no other encryption mode, check if default encryption is selected */ | |
1096 | if (s->cct->_conf->rgw_crypt_default_encryption_key != "") { | |
3efd9988 FG |
1097 | std::string master_encryption_key; |
1098 | try { | |
1099 | master_encryption_key = from_base64(s->cct->_conf->rgw_crypt_default_encryption_key); | |
1100 | } catch (...) { | |
b3b6e05e | 1101 | ldpp_dout(s, 5) << "ERROR: rgw_s3_prepare_encrypt invalid default encryption key " |
3efd9988 FG |
1102 | << "which contains character that is not base64 encoded." |
1103 | << dendl; | |
1104 | s->err.message = "Requests specifying Server Side Encryption with Customer " | |
1105 | "provided keys must provide an appropriate secret key."; | |
1106 | return -EINVAL; | |
1107 | } | |
1108 | ||
7c673cae | 1109 | if (master_encryption_key.size() != 256 / 8) { |
b3b6e05e | 1110 | ldpp_dout(s, 0) << "ERROR: failed to decode 'rgw crypt default encryption key' to 256 bit string" << dendl; |
7c673cae FG |
1111 | /* not an error to return; missing encryption does not inhibit processing */ |
1112 | return 0; | |
1113 | } | |
1114 | ||
1115 | set_attr(attrs, RGW_ATTR_CRYPT_MODE, "RGW-AUTO"); | |
1116 | std::string key_selector = create_random_key_selector(s->cct); | |
1117 | set_attr(attrs, RGW_ATTR_CRYPT_KEYSEL, key_selector); | |
1118 | ||
1119 | uint8_t actual_key[AES_256_KEYSIZE]; | |
1120 | if (AES_256_ECB_encrypt(s->cct, | |
1121 | reinterpret_cast<const uint8_t*>(master_encryption_key.c_str()), AES_256_KEYSIZE, | |
1122 | reinterpret_cast<const uint8_t*>(key_selector.c_str()), | |
1123 | actual_key, AES_256_KEYSIZE) != true) { | |
92f5a8d4 | 1124 | ::ceph::crypto::zeroize_for_security(actual_key, sizeof(actual_key)); |
7c673cae FG |
1125 | return -EIO; |
1126 | } | |
1127 | if (block_crypt) { | |
1128 | auto aes = std::unique_ptr<AES_256_CBC>(new AES_256_CBC(s->cct)); | |
1129 | aes->set_key(reinterpret_cast<const uint8_t*>(actual_key), AES_256_KEYSIZE); | |
1130 | *block_crypt = std::move(aes); | |
1131 | } | |
92f5a8d4 | 1132 | ::ceph::crypto::zeroize_for_security(actual_key, sizeof(actual_key)); |
7c673cae FG |
1133 | return 0; |
1134 | } | |
1135 | } | |
1136 | /*no encryption*/ | |
1137 | return 0; | |
1138 | } | |
1139 | ||
1140 | ||
1141 | int rgw_s3_prepare_decrypt(struct req_state* s, | |
1142 | map<string, bufferlist>& attrs, | |
1143 | std::unique_ptr<BlockCrypt>* block_crypt, | |
1144 | std::map<std::string, std::string>& crypt_http_responses) | |
1145 | { | |
1146 | int res = 0; | |
1147 | std::string stored_mode = get_str_attribute(attrs, RGW_ATTR_CRYPT_MODE); | |
b3b6e05e | 1148 | ldpp_dout(s, 15) << "Encryption mode: " << stored_mode << dendl; |
181888fb FG |
1149 | |
1150 | const char *req_sse = s->info.env->get("HTTP_X_AMZ_SERVER_SIDE_ENCRYPTION", NULL); | |
1151 | if (nullptr != req_sse && (s->op == OP_GET || s->op == OP_HEAD)) { | |
1152 | return -ERR_INVALID_REQUEST; | |
1153 | } | |
1154 | ||
7c673cae FG |
1155 | if (stored_mode == "SSE-C-AES256") { |
1156 | if (s->cct->_conf->rgw_crypt_require_ssl && | |
f64942e4 | 1157 | !rgw_transport_is_secure(s->cct, *s->info.env)) { |
b3b6e05e | 1158 | ldpp_dout(s, 5) << "ERROR: Insecure request, rgw_crypt_require_ssl is set" << dendl; |
7c673cae FG |
1159 | return -ERR_INVALID_REQUEST; |
1160 | } | |
1161 | const char *req_cust_alg = | |
1162 | s->info.env->get("HTTP_X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_ALGORITHM", NULL); | |
1163 | ||
3efd9988 | 1164 | if (nullptr == req_cust_alg) { |
b3b6e05e | 1165 | ldpp_dout(s, 5) << "ERROR: Request for SSE-C encrypted object missing " |
d2e6a577 FG |
1166 | << "x-amz-server-side-encryption-customer-algorithm" |
1167 | << dendl; | |
3efd9988 FG |
1168 | s->err.message = "Requests specifying Server Side Encryption with Customer " |
1169 | "provided keys must provide a valid encryption algorithm."; | |
1170 | return -EINVAL; | |
1171 | } else if (strcmp(req_cust_alg, "AES256") != 0) { | |
b3b6e05e | 1172 | ldpp_dout(s, 5) << "ERROR: The requested encryption algorithm is not valid, must be AES256." << dendl; |
3efd9988 FG |
1173 | s->err.message = "The requested encryption algorithm is not valid, must be AES256."; |
1174 | return -ERR_INVALID_ENCRYPTION_ALGORITHM; | |
1175 | } | |
1176 | ||
1177 | std::string key_bin; | |
1178 | try { | |
1179 | key_bin = from_base64(s->info.env->get("HTTP_X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY", "")); | |
1180 | } catch (...) { | |
b3b6e05e | 1181 | ldpp_dout(s, 5) << "ERROR: rgw_s3_prepare_decrypt invalid encryption key " |
3efd9988 FG |
1182 | << "which contains character that is not base64 encoded." |
1183 | << dendl; | |
1184 | s->err.message = "Requests specifying Server Side Encryption with Customer " | |
1185 | "provided keys must provide an appropriate secret key."; | |
1186 | return -EINVAL; | |
7c673cae FG |
1187 | } |
1188 | ||
7c673cae | 1189 | if (key_bin.size() != AES_256_CBC::AES_256_KEYSIZE) { |
b3b6e05e | 1190 | ldpp_dout(s, 5) << "ERROR: Invalid encryption key size" << dendl; |
3efd9988 FG |
1191 | s->err.message = "Requests specifying Server Side Encryption with Customer " |
1192 | "provided keys must provide an appropriate secret key."; | |
1193 | return -EINVAL; | |
7c673cae FG |
1194 | } |
1195 | ||
1196 | std::string keymd5 = | |
1197 | s->info.env->get("HTTP_X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY_MD5", ""); | |
3efd9988 FG |
1198 | std::string keymd5_bin; |
1199 | try { | |
1200 | keymd5_bin = from_base64(keymd5); | |
1201 | } catch (...) { | |
b3b6e05e | 1202 | ldpp_dout(s, 5) << "ERROR: rgw_s3_prepare_decrypt invalid encryption key md5 " |
3efd9988 FG |
1203 | << "which contains character that is not base64 encoded." |
1204 | << dendl; | |
1205 | s->err.message = "Requests specifying Server Side Encryption with Customer " | |
1206 | "provided keys must provide an appropriate secret key md5."; | |
1207 | return -EINVAL; | |
1208 | } | |
1209 | ||
1210 | ||
7c673cae | 1211 | if (keymd5_bin.size() != CEPH_CRYPTO_MD5_DIGESTSIZE) { |
b3b6e05e | 1212 | ldpp_dout(s, 5) << "ERROR: Invalid key md5 size " << dendl; |
3efd9988 FG |
1213 | s->err.message = "Requests specifying Server Side Encryption with Customer " |
1214 | "provided keys must provide an appropriate secret key md5."; | |
1215 | return -EINVAL; | |
7c673cae FG |
1216 | } |
1217 | ||
1218 | MD5 key_hash; | |
1219 | uint8_t key_hash_res[CEPH_CRYPTO_MD5_DIGESTSIZE]; | |
11fdf7f2 | 1220 | key_hash.Update(reinterpret_cast<const unsigned char*>(key_bin.c_str()), key_bin.size()); |
7c673cae FG |
1221 | key_hash.Final(key_hash_res); |
1222 | ||
1223 | if ((memcmp(key_hash_res, keymd5_bin.c_str(), CEPH_CRYPTO_MD5_DIGESTSIZE) != 0) || | |
1224 | (get_str_attribute(attrs, RGW_ATTR_CRYPT_KEYMD5) != keymd5_bin)) { | |
3efd9988 FG |
1225 | s->err.message = "The calculated MD5 hash of the key did not match the hash that was provided."; |
1226 | return -EINVAL; | |
7c673cae FG |
1227 | } |
1228 | auto aes = std::unique_ptr<AES_256_CBC>(new AES_256_CBC(s->cct)); | |
1229 | aes->set_key(reinterpret_cast<const uint8_t*>(key_bin.c_str()), AES_256_CBC::AES_256_KEYSIZE); | |
1230 | if (block_crypt) *block_crypt = std::move(aes); | |
1231 | ||
1232 | crypt_http_responses["x-amz-server-side-encryption-customer-algorithm"] = "AES256"; | |
1233 | crypt_http_responses["x-amz-server-side-encryption-customer-key-MD5"] = keymd5; | |
1234 | return 0; | |
1235 | } | |
1236 | ||
1237 | if (stored_mode == "SSE-KMS") { | |
1238 | if (s->cct->_conf->rgw_crypt_require_ssl && | |
f64942e4 | 1239 | !rgw_transport_is_secure(s->cct, *s->info.env)) { |
b3b6e05e | 1240 | ldpp_dout(s, 5) << "ERROR: Insecure request, rgw_crypt_require_ssl is set" << dendl; |
7c673cae FG |
1241 | return -ERR_INVALID_REQUEST; |
1242 | } | |
1243 | /* try to retrieve actual key */ | |
1244 | std::string key_id = get_str_attribute(attrs, RGW_ATTR_CRYPT_KEYID); | |
7c673cae | 1245 | std::string actual_key; |
f67539c2 | 1246 | res = reconstitute_actual_key_from_kms(s->cct, attrs, actual_key); |
7c673cae | 1247 | if (res != 0) { |
b3b6e05e | 1248 | ldpp_dout(s, 10) << "ERROR: failed to retrieve actual key from key_id: " << key_id << dendl; |
3efd9988 | 1249 | s->err.message = "Failed to retrieve the actual key, kms-keyid: " + key_id; |
7c673cae FG |
1250 | return res; |
1251 | } | |
1252 | if (actual_key.size() != AES_256_KEYSIZE) { | |
b3b6e05e | 1253 | ldpp_dout(s, 0) << "ERROR: key obtained from key_id:" << |
7c673cae | 1254 | key_id << " is not 256 bit size" << dendl; |
3efd9988 | 1255 | s->err.message = "KMS provided an invalid key for the given kms-keyid."; |
7c673cae FG |
1256 | return -ERR_INVALID_ACCESS_KEY; |
1257 | } | |
1258 | ||
1259 | auto aes = std::unique_ptr<AES_256_CBC>(new AES_256_CBC(s->cct)); | |
1260 | aes->set_key(reinterpret_cast<const uint8_t*>(actual_key.c_str()), AES_256_KEYSIZE); | |
1261 | actual_key.replace(0, actual_key.length(), actual_key.length(), '\000'); | |
1262 | if (block_crypt) *block_crypt = std::move(aes); | |
1263 | ||
1264 | crypt_http_responses["x-amz-server-side-encryption"] = "aws:kms"; | |
1265 | crypt_http_responses["x-amz-server-side-encryption-aws-kms-key-id"] = key_id; | |
1266 | return 0; | |
1267 | } | |
1268 | ||
1269 | if (stored_mode == "RGW-AUTO") { | |
3efd9988 FG |
1270 | std::string master_encryption_key; |
1271 | try { | |
1272 | master_encryption_key = from_base64(std::string(s->cct->_conf->rgw_crypt_default_encryption_key)); | |
1273 | } catch (...) { | |
b3b6e05e | 1274 | ldpp_dout(s, 5) << "ERROR: rgw_s3_prepare_decrypt invalid default encryption key " |
3efd9988 FG |
1275 | << "which contains character that is not base64 encoded." |
1276 | << dendl; | |
1277 | s->err.message = "The default encryption key is not valid base64."; | |
1278 | return -EINVAL; | |
1279 | } | |
1280 | ||
7c673cae | 1281 | if (master_encryption_key.size() != 256 / 8) { |
b3b6e05e | 1282 | ldpp_dout(s, 0) << "ERROR: failed to decode 'rgw crypt default encryption key' to 256 bit string" << dendl; |
7c673cae FG |
1283 | return -EIO; |
1284 | } | |
1285 | std::string attr_key_selector = get_str_attribute(attrs, RGW_ATTR_CRYPT_KEYSEL); | |
1286 | if (attr_key_selector.size() != AES_256_CBC::AES_256_KEYSIZE) { | |
b3b6e05e | 1287 | ldpp_dout(s, 0) << "ERROR: missing or invalid " RGW_ATTR_CRYPT_KEYSEL << dendl; |
7c673cae FG |
1288 | return -EIO; |
1289 | } | |
1290 | uint8_t actual_key[AES_256_KEYSIZE]; | |
1291 | if (AES_256_ECB_encrypt(s->cct, | |
1292 | reinterpret_cast<const uint8_t*>(master_encryption_key.c_str()), | |
1293 | AES_256_KEYSIZE, | |
1294 | reinterpret_cast<const uint8_t*>(attr_key_selector.c_str()), | |
1295 | actual_key, AES_256_KEYSIZE) != true) { | |
92f5a8d4 | 1296 | ::ceph::crypto::zeroize_for_security(actual_key, sizeof(actual_key)); |
7c673cae FG |
1297 | return -EIO; |
1298 | } | |
1299 | auto aes = std::unique_ptr<AES_256_CBC>(new AES_256_CBC(s->cct)); | |
1300 | aes->set_key(actual_key, AES_256_KEYSIZE); | |
92f5a8d4 | 1301 | ::ceph::crypto::zeroize_for_security(actual_key, sizeof(actual_key)); |
7c673cae FG |
1302 | if (block_crypt) *block_crypt = std::move(aes); |
1303 | return 0; | |
1304 | } | |
1305 | /*no decryption*/ | |
1306 | return 0; | |
1307 | } | |
f67539c2 TL |
1308 | |
1309 | /********************************************************************* | |
1310 | * "BOTTOM OF FILE" | |
1311 | * I've left some commented out lines above. They are there for | |
1312 | * a reason, which I will explain. The "canonical" json constructed | |
1313 | * by the code above as a crypto context must take a json object and | |
1314 | * turn it into a unique determinstic fixed form. For most json | |
1315 | * types this is easy. The hardest problem that is handled above is | |
1316 | * detailing with unicode strings; they must be turned into | |
1317 | * NFC form and sorted in a fixed order. Numbers, however, | |
1318 | * are another story. Json makes no distinction between integers | |
1319 | * and floating point, and both types have their problems. | |
1320 | * Integers can overflow, so very large numbers are a problem. | |
1321 | * Floating point is even worse; not all floating point numbers | |
1322 | * can be represented accurately in c++ data types, and there | |
1323 | * are many quirks regarding how overflow, underflow, and loss | |
1324 | * of significance are handled. | |
1325 | * | |
1326 | * In this version of the code, I took the simplest answer, I | |
1327 | * reject all numbers altogether. This is not ideal, but it's | |
1328 | * the only choice that is guaranteed to be future compatible. | |
1329 | * AWS S3 does not guarantee to support numbers at all; but it | |
1330 | * actually converts all numbers into strings right off. | |
1331 | * This has the interesting property that 7 and 007 are different, | |
1332 | * but that 007 and "007" are the same. I would rather | |
1333 | * treat numbers as a string of digits and have logic | |
1334 | * to produce the "most compact" equivalent form. This can | |
1335 | * fix all the overflow/underflow problems, but it requires | |
1336 | * fixing the json parser part, and I put that problem off. | |
1337 | * | |
1338 | * The commented code above indicates places in this code that | |
1339 | * will need to be revised depending on future work in this area. | |
1340 | * Removing those comments makes that work harder. | |
1341 | * February 25, 2021 | |
1342 | *********************************************************************/ |