Skip to content

Commit

Permalink
[OSCrypt] Decrypt data that was encrypted with empty key
Browse files Browse the repository at this point in the history
If the data wasn't decrypted with the encryption key, OSCrypt tries
again, using an empty key.

This ensures data remains available, after restoring encryption keys.

Bug: 1195256
Change-Id: I609511350657a2d3ef930c584ceed282e227e93a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4318564
Reviewed-by: Lei Zhang <thestig@chromium.org>
Reviewed-by: Mark Pearson <mpearson@chromium.org>
Commit-Queue: Christos Froussios <cfroussios@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1118445}
  • Loading branch information
Froussios authored and Chromium LUCI CQ committed Mar 17, 2023
1 parent 6722d98 commit bbd5470
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 16 deletions.
63 changes: 47 additions & 16 deletions components/os_crypt/sync/os_crypt_linux.cc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "base/cxx17_backports.h"
#include "base/logging.h"
#include "base/memory/singleton.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
Expand Down Expand Up @@ -46,6 +47,10 @@ constexpr size_t kIVBlockSizeAES128 = 16;
constexpr char kObfuscationPrefixV10[] = "v10";
constexpr char kObfuscationPrefixV11[] = "v11";

// The UMA metric name for whether the false was decryptable with an empty key.
constexpr char kMetricDecryptedWithEmptyKey[] =
"OSCrypt.Linux.DecryptedWithEmptyKey";

// Generates a newly allocated SymmetricKey object based on a password.
// Ownership of the key is passed to the caller. Returns null key if a key
// generation error occurs.
Expand All @@ -63,6 +68,20 @@ std::unique_ptr<crypto::SymmetricKey> GenerateEncryptionKey(
return encryption_key;
}

// Decrypt `ciphertext` using `encryption_key` and store the result in
// `encryption_key`.
bool DecryptWith(const std::string& ciphertext,
crypto::SymmetricKey* encryption_key,
std::string* plaintext) {
const std::string iv(kIVBlockSizeAES128, ' ');
crypto::Encryptor encryptor;
if (!encryptor.Init(encryption_key, crypto::Encryptor::CBC, iv)) {
return false;
}

return encryptor.Decrypt(ciphertext, plaintext);
}

} // namespace

namespace OSCrypt {
Expand Down Expand Up @@ -123,8 +142,9 @@ bool OSCryptImpl::EncryptString16(const std::u16string& plaintext,
bool OSCryptImpl::DecryptString16(const std::string& ciphertext,
std::u16string* plaintext) {
std::string utf8;
if (!DecryptString(ciphertext, &utf8))
if (!DecryptString(ciphertext, &utf8)) {
return false;
}

*plaintext = base::UTF8ToUTF16(utf8);
return true;
Expand All @@ -146,16 +166,19 @@ bool OSCryptImpl::EncryptString(const std::string& plaintext,
obfuscation_prefix = kObfuscationPrefixV10;
}

if (!encryption_key)
if (!encryption_key) {
return false;
}

const std::string iv(kIVBlockSizeAES128, ' ');
crypto::Encryptor encryptor;
if (!encryptor.Init(encryption_key, crypto::Encryptor::CBC, iv))
if (!encryptor.Init(encryption_key, crypto::Encryptor::CBC, iv)) {
return false;
}

if (!encryptor.Encrypt(plaintext, ciphertext))
if (!encryptor.Encrypt(plaintext, ciphertext)) {
return false;
}

// Prefix the cipher text with version information.
ciphertext->insert(0, obfuscation_prefix);
Expand Down Expand Up @@ -194,21 +217,27 @@ bool OSCryptImpl::DecryptString(const std::string& ciphertext,
return false;
}

const std::string iv(kIVBlockSizeAES128, ' ');
crypto::Encryptor encryptor;
if (!encryptor.Init(encryption_key, crypto::Encryptor::CBC, iv))
return false;

// Strip off the versioning prefix before decrypting.
const std::string raw_ciphertext =
ciphertext.substr(obfuscation_prefix.length());

if (!encryptor.Decrypt(raw_ciphertext, plaintext)) {
VLOG(1) << "Decryption failed";
return false;
if (DecryptWith(raw_ciphertext, encryption_key, plaintext)) {
base::UmaHistogramBoolean(kMetricDecryptedWithEmptyKey, false);
return true;
}

return true;
// Some clients have encrypted data with an empty key. See
// crbug.com/1195256.
auto empty_key = GenerateEncryptionKey(std::string());
if (DecryptWith(raw_ciphertext, empty_key.get(), plaintext)) {
VLOG(1) << "Decryption succeeded after retrying with an empty key";
base::UmaHistogramBoolean(kMetricDecryptedWithEmptyKey, true);
return true;
}

VLOG(1) << "Decryption failed";
base::UmaHistogramBoolean(kMetricDecryptedWithEmptyKey, false);
return false;
}

void OSCryptImpl::SetConfig(std::unique_ptr<os_crypt::Config> config) {
Expand Down Expand Up @@ -241,8 +270,9 @@ void OSCryptImpl::SetRawEncryptionKey(const std::string& raw_key) {
}

std::string OSCryptImpl::GetRawEncryptionKey() {
if (crypto::SymmetricKey* key = GetPasswordV11())
if (crypto::SymmetricKey* key = GetPasswordV11()) {
return key->key();
}
return std::string();
}

Expand All @@ -256,11 +286,12 @@ void OSCryptImpl::ClearCacheForTesting() {
void OSCryptImpl::UseMockKeyStorageForTesting(
base::OnceCallback<std::unique_ptr<KeyStorageLinux>()>
storage_provider_factory) {
if (storage_provider_factory)
if (storage_provider_factory) {
storage_provider_factory_ = std::move(storage_provider_factory);
else
} else {
storage_provider_factory_ =
base::BindOnce(&OSCryptImpl::CreateKeyStorage, base::Unretained(this));
}
}

// Create the KeyStorage. Will be null if no service is found. A Config must be
Expand Down
24 changes: 24 additions & 0 deletions components/os_crypt/sync/os_crypt_linux_unittest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <string>

#include "base/functional/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "components/os_crypt/sync/key_storage_linux.h"
#include "components/os_crypt/sync/os_crypt.h"
#include "components/os_crypt/sync/os_crypt_mocker_linux.h"
Expand Down Expand Up @@ -98,4 +99,27 @@ TEST_F(OSCryptLinuxTest, SetRawEncryptionKey) {
ASSERT_EQ(originaltext, decipheredtext);
}

// Because of crbug.com/1195256, there might be data that is encrypted with an
// empty key. These should remain decryptable, even when a proper key is
// available.
TEST_F(OSCryptLinuxTest, DecryptWhenEncryptionKeyIsEmpty) {
base::HistogramTester histogram_tester;
const std::string originaltext = "hello";
std::string ciphertext;
std::string decipheredtext;

// Encrypt a value using "" as the key.
OSCrypt::SetEncryptionPasswordForTesting("");
ASSERT_TRUE(OSCrypt::EncryptString(originaltext, &ciphertext));

// Set a proper encryption key.
OSCrypt::ClearCacheForTesting();
OSCrypt::SetEncryptionPasswordForTesting("key");
// The text is decryptable.
ASSERT_TRUE(OSCrypt::DecryptString(ciphertext, &decipheredtext));
EXPECT_EQ(originaltext, decipheredtext);
histogram_tester.ExpectUniqueSample("OSCrypt.Linux.DecryptedWithEmptyKey",
true, 1);
}

} // namespace
11 changes: 11 additions & 0 deletions tools/metrics/histograms/metadata/others/histograms.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9101,6 +9101,17 @@ chromium-metrics-reviews@google.com.
</summary>
</histogram>

<histogram name="OSCrypt.Linux.DecryptedWithEmptyKey" enum="Boolean"
expires_after="2023-09-01">
<owner>cfroussios@chromium.org</owner>
<owner>thestig@chromium.org</owner>
<summary>
True, iff the data was not decryptable with the stored encryption key, but
was decryptable with a key generated from an empty string. This is emitted
every time OSCrypt attempts to decrypt a value.
</summary>
</histogram>

<histogram name="OSCrypt.Win.Decrypt.Result" enum="BooleanSuccess"
expires_after="2023-09-10">
<owner>wfh@chromium.org</owner>
Expand Down

0 comments on commit bbd5470

Please sign in to comment.