github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/c-deps/libroach/ccl/encrypted_env_test.cc (about) 1 // Copyright 2017 The Cockroach Authors. 2 // 3 // Licensed as a CockroachDB Enterprise file under the Cockroach Community 4 // License (the "License"); you may not use this file except in compliance with 5 // the License. You may obtain a copy of the License at 6 // 7 // https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt 8 9 #include <thread> 10 #include "../file_registry.h" 11 #include "../fmt.h" 12 #include "../testutils.h" 13 #include "ccl/baseccl/encryption_options.pb.h" 14 #include "ctr_stream.h" 15 #include "testutils.h" 16 17 using namespace cockroach; 18 using namespace testutils; 19 20 TEST(EncryptedEnv, ConcurrentAccess) { 21 // This test creates a standalone encrypted env, verifies that what goes 22 // in comes out, and that concurrent accesses to the same file handle are ok. 23 std::unique_ptr<rocksdb::Env> env(rocksdb::NewMemEnv(rocksdb::Env::Default())); 24 auto key_manager = new MemKeyManager(MakeAES128Key(env.get())); 25 auto stream = new CTRCipherStreamCreator(key_manager, enginepb::Data); 26 27 auto file_registry = 28 std::unique_ptr<FileRegistry>(new FileRegistry(env.get(), "/", false /* read-only */)); 29 EXPECT_OK(file_registry->Load()); 30 31 std::unique_ptr<rocksdb::Env> encrypted_env( 32 rocksdb_utils::NewEncryptedEnv(env.get(), file_registry.get(), stream)); 33 34 std::string filename("/foo"); 35 std::string contents("this is the string stored inside the file!"); 36 size_t kContentsLength = 42; 37 ASSERT_EQ(kContentsLength, contents.size()); 38 39 // Write the file. 40 EXPECT_OK( 41 rocksdb::WriteStringToFile(encrypted_env.get(), contents, filename, false /* should_sync */)); 42 43 // Read the file using the mem env (no encryption). 44 std::string result_plain; 45 EXPECT_OK(rocksdb::ReadFileToString(env.get(), filename, &result_plain)); 46 EXPECT_STRNE(contents.c_str(), result_plain.c_str()); 47 48 // Read the file back using the encrypted env. 49 std::string result_encrypted; 50 EXPECT_OK(rocksdb::ReadFileToString(encrypted_env.get(), filename, &result_encrypted)); 51 EXPECT_STREQ(contents.c_str(), result_encrypted.c_str()); 52 53 // Open as a random access file. 54 std::unique_ptr<rocksdb::RandomAccessFile> file; 55 EXPECT_OK(encrypted_env->NewRandomAccessFile(filename, &file, rocksdb::EnvOptions())); 56 57 // Reader thread. Captures all useful variables. 58 auto read_file = [&]() { 59 char scratch[kContentsLength]; // needs to be at least len(contents). 60 rocksdb::Slice result_read; 61 62 for (int i = 0; i < 100; i++) { 63 EXPECT_OK(file->Read(0, kContentsLength, &result_read, scratch)); 64 EXPECT_EQ(kContentsLength, result_read.size()); 65 66 // We need to go through Slice.ToString as .data() does not have a null terminator. 67 EXPECT_STREQ(contents.c_str(), result_read.ToString().c_str()); 68 } 69 }; 70 71 // Call it once by itself. 72 read_file(); 73 74 // Run two at the same time. We're not using rocksdb thread utilities as they don't support 75 // lambda functions with variable capture, everything has to done through args. 76 auto t1 = std::thread(read_file); 77 auto t2 = std::thread(read_file); 78 t1.join(); 79 t2.join(); 80 } 81 82 namespace { 83 84 rocksdb::Status checkFileEntry(FileRegistry& registry, const std::string& filename, 85 enginepbccl::EncryptionType enc_type) { 86 auto entry = registry.GetFileEntry(filename); 87 if (entry == nullptr) { 88 return rocksdb::Status::InvalidArgument( 89 fmt::StringPrintf("file %s has no entry", filename.c_str())); 90 } 91 92 enginepbccl::EncryptionSettings enc_settings; 93 if (!enc_settings.ParseFromString(entry->encryption_settings())) { 94 return rocksdb::Status::InvalidArgument( 95 fmt::StringPrintf("failed to parse encryption settings for file %s", filename.c_str())); 96 } 97 98 return rocksdb::Status::OK(); 99 } 100 101 rocksdb::Status checkNoFileEntry(FileRegistry& registry, const std::string& filename) { 102 auto entry = registry.GetFileEntry(filename); 103 if (entry != nullptr) { 104 return rocksdb::Status::InvalidArgument( 105 fmt::StringPrintf("file %s has an unexpected entry", filename.c_str())); 106 } 107 108 return rocksdb::Status::OK(); 109 } 110 111 rocksdb::Status checkFileContents(rocksdb::Env* env, const std::string& filename, 112 const std::string& contents) { 113 std::string result; 114 auto status = rocksdb::ReadFileToString(env, filename, &result); 115 if (!status.ok()) { 116 return status; 117 } 118 119 if (contents != result) { 120 return rocksdb::Status::InvalidArgument( 121 fmt::StringPrintf("file %s contents mismatch, expected %s, got %s", filename.c_str(), 122 contents.c_str(), result.c_str())); 123 } 124 125 return rocksdb::Status::OK(); 126 } 127 128 }; // anonymous namespace 129 130 TEST(EncryptedEnv, BasicOps) { 131 // Check various file operations against an encrypted env. 132 // This exercises the env/file registry interaction. 133 // We need to use a real underlying env as the MemEnv takes a **lot** 134 // of shortcuts. 135 rocksdb::Env* env = rocksdb::Env::Default(); 136 auto key_manager = new MemKeyManager(MakeAES128Key(env)); 137 auto stream = new CTRCipherStreamCreator(key_manager, enginepb::Data); 138 139 auto tmpdir = std::unique_ptr<TempDirHandler>(new TempDirHandler()); 140 141 auto file_registry = 142 std::unique_ptr<FileRegistry>(new FileRegistry(env, tmpdir->Path(""), false /* read-only */)); 143 EXPECT_OK(file_registry->Load()); 144 145 std::unique_ptr<rocksdb::Env> encrypted_env( 146 rocksdb_utils::NewEncryptedEnv(env, file_registry.get(), stream)); 147 148 auto file1 = tmpdir->Path("foo1"); 149 auto file2 = tmpdir->Path("foo2"); 150 auto file3 = tmpdir->Path("foo3"); 151 152 std::string contents("this is the first file!"); 153 std::string contents2("this is the second file!"); 154 ASSERT_OK(rocksdb::WriteStringToFile(encrypted_env.get(), contents, file1, false)); 155 156 // Check its presence in the file registry. 157 EXPECT_OK(checkFileEntry(*file_registry, file1, enginepbccl::AES128_CTR)); 158 159 // Rename file. 160 ASSERT_OK(encrypted_env->RenameFile(file1, file2)); 161 EXPECT_OK(checkNoFileEntry(*file_registry, file1)); 162 EXPECT_OK(checkFileEntry(*file_registry, file2, enginepbccl::AES128_CTR)); 163 164 // Link file (hard link). 165 ASSERT_OK(encrypted_env->LinkFile(file2, file3)); 166 EXPECT_OK(checkFileEntry(*file_registry, file2, enginepbccl::AES128_CTR)); 167 EXPECT_OK(checkFileEntry(*file_registry, file3, enginepbccl::AES128_CTR)); 168 169 // Delete file. 170 ASSERT_OK(encrypted_env->DeleteFile(file2)); 171 EXPECT_OK(checkNoFileEntry(*file_registry, file2)); 172 EXPECT_OK(checkFileEntry(*file_registry, file3, enginepbccl::AES128_CTR)); 173 174 ASSERT_OK(encrypted_env->DeleteFile(file3)); 175 EXPECT_OK(checkNoFileEntry(*file_registry, file2)); 176 EXPECT_OK(checkNoFileEntry(*file_registry, file3)); 177 178 /***** Odd cases *****/ 179 ASSERT_OK(rocksdb::WriteStringToFile(encrypted_env.get(), contents, file1, false)); 180 ASSERT_OK(rocksdb::WriteStringToFile(encrypted_env.get(), contents2, file2, false)); 181 EXPECT_OK(checkFileEntry(*file_registry, file1, enginepbccl::AES128_CTR)); 182 EXPECT_OK(checkFileEntry(*file_registry, file2, enginepbccl::AES128_CTR)); 183 EXPECT_OK(checkFileContents(encrypted_env.get(), file1, contents)); 184 EXPECT_OK(checkFileContents(encrypted_env.get(), file2, contents2)); 185 186 // Rename to existing file (replace). 187 ASSERT_OK(encrypted_env->RenameFile(file1, file2)); 188 EXPECT_OK(checkNoFileEntry(*file_registry, file1)); 189 EXPECT_OK(checkFileEntry(*file_registry, file2, enginepbccl::AES128_CTR)); 190 EXPECT_OK(checkFileContents(encrypted_env.get(), file2, contents)); 191 192 ASSERT_OK(rocksdb::WriteStringToFile(encrypted_env.get(), contents2, file3, false)); 193 EXPECT_OK(checkFileContents(encrypted_env.get(), file3, contents2)); 194 195 // Link to an existing file: fails with "file exists". 196 ASSERT_ERR(encrypted_env->LinkFile(file2, file3), ".* File exists"); 197 EXPECT_OK(checkFileContents(encrypted_env.get(), file2, contents)); 198 EXPECT_OK(checkFileContents(encrypted_env.get(), file3, contents2)); 199 200 // Let's check that the plain env can't read the contents. 201 EXPECT_PARTIAL_ERR(checkFileContents(env, file2, contents), 202 "file .*foo2 contents mismatch, expected this is the first file!, got "); 203 204 /***** Switch to plaintext *****/ 205 key_manager->set_key(nullptr); 206 207 // Rename. 208 ASSERT_OK(encrypted_env->RenameFile(file2, file1)); 209 EXPECT_OK(checkNoFileEntry(*file_registry, file2)); 210 EXPECT_OK(checkFileEntry(*file_registry, file1, enginepbccl::AES128_CTR)); 211 212 // Link. 213 ASSERT_OK(encrypted_env->LinkFile(file1, file2)); 214 EXPECT_OK(checkFileEntry(*file_registry, file1, enginepbccl::AES128_CTR)); 215 EXPECT_OK(checkFileEntry(*file_registry, file2, enginepbccl::AES128_CTR)); 216 217 // Create a new file. This should overwrite the previous file entry. 218 std::string contents3("we're in plaintext!"); 219 ASSERT_OK(rocksdb::WriteStringToFile(encrypted_env.get(), contents3, file1, false)); 220 EXPECT_OK(checkFileEntry(*file_registry, file1, enginepbccl::Plaintext)); 221 EXPECT_OK(checkFileEntry(*file_registry, file2, enginepbccl::AES128_CTR)); 222 EXPECT_OK(checkFileContents(encrypted_env.get(), file1, contents3)); 223 // Check with the plain env. 224 EXPECT_OK(checkFileContents(env, file1, contents3)); 225 EXPECT_OK(checkFileContents(env, file2, contents3)); 226 } 227 228 TEST(EncryptedEnv, ReuseWritableFileUnderlyingFailure) { 229 // Ensure all WALs are either empty or decryptable no matter when/whether 230 // `EncryptedEnv::ReuseWritableFile` fails. See comment above 231 // `EncryptedEnv::ReuseWritableFile`'s definition for details on why this 232 // guarantee is necessary for crash-safety. 233 234 // `ReuseWritableFileInjectionEnv` is used as the env underlying an 235 // `EncryptedEnv`. It injects faults in the operations used by 236 // `EncryptedEnv::ReuseWritableFile()`. 237 class ReuseWritableFileInjectionEnv : public rocksdb::EnvWrapper { 238 public: 239 enum class Mode { 240 kNone, 241 kFailCreation, 242 kFailDeletion, 243 kEnd, 244 }; 245 246 explicit ReuseWritableFileInjectionEnv(Env* target) : EnvWrapper(target), mode_(Mode::kNone) {} 247 248 void set_mode(Mode mode) { mode_ = mode; } 249 250 rocksdb::Status NewWritableFile(const std::string& fname, 251 std::unique_ptr<rocksdb::WritableFile>* result, 252 const rocksdb::EnvOptions& options) override { 253 if (mode_ == Mode::kFailCreation) { 254 return rocksdb::Status::IOError("injected error"); 255 } 256 return rocksdb::EnvWrapper::NewWritableFile(fname, result, options); 257 } 258 259 rocksdb::Status DeleteFile(const std::string& fname) override { 260 if (mode_ == Mode::kFailDeletion) { 261 return rocksdb::Status::IOError("injected error"); 262 } 263 return rocksdb::EnvWrapper::DeleteFile(fname); 264 } 265 266 private: 267 Mode mode_; 268 }; 269 270 for (int i = 0; i < static_cast<int>(ReuseWritableFileInjectionEnv::Mode::kEnd); ++i) { 271 auto mode = static_cast<ReuseWritableFileInjectionEnv::Mode>(i); 272 ReuseWritableFileInjectionEnv env(rocksdb::Env::Default()); 273 auto key_manager = new MemKeyManager(MakeAES128Key(&env)); 274 auto stream = new CTRCipherStreamCreator(key_manager, enginepb::Data); 275 276 auto tmpdir = std::unique_ptr<TempDirHandler>(new TempDirHandler()); 277 278 auto file_registry = std::unique_ptr<FileRegistry>( 279 new FileRegistry(&env, tmpdir->Path(""), false /* read-only */)); 280 EXPECT_OK(file_registry->Load()); 281 282 std::unique_ptr<rocksdb::Env> encrypted_env( 283 rocksdb_utils::NewEncryptedEnv(&env, file_registry.get(), stream)); 284 285 auto old_file = tmpdir->Path("foo1"); 286 std::string contents("this is a file!"); 287 ASSERT_OK(rocksdb::WriteStringToFile(encrypted_env.get(), contents, old_file, false)); 288 289 auto new_file = tmpdir->Path("foo2"); 290 env.set_mode(mode); 291 { 292 std::unique_ptr<rocksdb::WritableFile> res; 293 if (mode == ReuseWritableFileInjectionEnv::Mode::kNone) { 294 EXPECT_OK( 295 encrypted_env->ReuseWritableFile(new_file, old_file, &res, rocksdb::EnvOptions())); 296 } else { 297 EXPECT_TRUE( 298 encrypted_env->ReuseWritableFile(new_file, old_file, &res, rocksdb::EnvOptions()) 299 .IsIOError()); 300 } 301 } 302 303 switch (mode) { 304 case ReuseWritableFileInjectionEnv::Mode::kNone: { 305 // In success scenario, the new file should be empty and the old file should be deleted. 306 std::string new_file_contents; 307 EXPECT_OK(rocksdb::ReadFileToString(encrypted_env.get(), new_file, &new_file_contents)); 308 EXPECT_STREQ("", new_file_contents.c_str()); 309 EXPECT_TRUE(encrypted_env->FileExists(old_file).IsNotFound()); 310 break; 311 } 312 case ReuseWritableFileInjectionEnv::Mode::kFailCreation: 313 case ReuseWritableFileInjectionEnv::Mode::kFailDeletion: { 314 // In failure scenarios, all existing files must be empty or readable. 315 for (const auto& file : {old_file, new_file}) { 316 auto exists_status = encrypted_env->FileExists(file); 317 if (exists_status.ok()) { 318 std::string file_contents; 319 rocksdb::ReadFileToString(encrypted_env.get(), file, &file_contents); 320 if (!file_contents.empty()) { 321 EXPECT_STREQ(contents.c_str(), file_contents.c_str()); 322 } 323 } else { 324 EXPECT_TRUE(exists_status.IsNotFound()); 325 } 326 } 327 break; 328 } 329 case ReuseWritableFileInjectionEnv::Mode::kEnd: 330 assert(false); 331 break; 332 } 333 } 334 }