From 3dfbb125a1387100836d1a446ddf7e06e781885e Mon Sep 17 00:00:00 2001 From: AlexeyBarabash Date: Wed, 16 Feb 2022 18:07:25 +0200 Subject: [PATCH] Time-limited sync words class and tests Fixes brave/brave-browser#22242 --- components/brave_sync/BUILD.gn | 9 +- components/brave_sync/time_limited_words.cc | 172 ++++++++++++++++ components/brave_sync/time_limited_words.h | 61 ++++++ .../brave_sync/time_limited_words_unittest.cc | 185 ++++++++++++++++++ 4 files changed, 424 insertions(+), 3 deletions(-) create mode 100644 components/brave_sync/time_limited_words.cc create mode 100644 components/brave_sync/time_limited_words.h create mode 100644 components/brave_sync/time_limited_words_unittest.cc diff --git a/components/brave_sync/BUILD.gn b/components/brave_sync/BUILD.gn index 7c280159d8a2..c654c3a28c8e 100644 --- a/components/brave_sync/BUILD.gn +++ b/components/brave_sync/BUILD.gn @@ -72,12 +72,14 @@ source_set("prefs") { ] } -source_set("qr_code_data") { +source_set("time_limited_codes") { sources = [ "qr_code_data.cc", "qr_code_data.h", "qr_code_validator.cc", "qr_code_validator.h", + "time_limited_words.cc", + "time_limited_words.h", ] deps = [ @@ -105,7 +107,7 @@ group("brave_sync") { ":features", ":network_time_helper", ":prefs", - ":qr_code_data", + ":time_limited_codes", "//base", ] } @@ -120,11 +122,12 @@ source_set("unit_tests") { "//brave/components/brave_sync/brave_sync_prefs_unittest.cc", "//brave/components/brave_sync/qr_code_data_unittest.cc", "//brave/components/brave_sync/qr_code_validator_unittest.cc", + "//brave/components/brave_sync/time_limited_words_unittest.cc", ] deps = [ ":prefs", - ":qr_code_data", + ":time_limited_codes", "//base/test:test_support", "//components/os_crypt:test_support", "//components/prefs:test_support", diff --git a/components/brave_sync/time_limited_words.cc b/components/brave_sync/time_limited_words.cc new file mode 100644 index 000000000000..a09d6b228d9f --- /dev/null +++ b/components/brave_sync/time_limited_words.cc @@ -0,0 +1,172 @@ +/* Copyright (c) 2022 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "brave/components/brave_sync/time_limited_words.h" + +#include +#include +#include + +#include "base/containers/span.h" +#include "base/logging.h" +#include "base/notreached.h" +#include "base/strings/string_split.h" +#include "base/strings/string_util.h" +#include "base/time/time.h" +#include "brave/components/brave_sync/crypto/crypto.h" +#include "brave/components/brave_sync/qr_code_validator.h" +#include "brave/vendor/bip39wally-core-native/include/wally_bip39.h" +#include "brave/vendor/bip39wally-core-native/src/wordlist.h" + +namespace brave_sync { + +namespace { + +// TODO(alexeybarabash): subject to change +static constexpr char kWordsv1SunsetDate[] = "Fri, 1 Apr 2022 00:00:00 GMT"; + +} // namespace + +using base::Time; +using base::TimeDelta; + +Time TimeLimitedWords::words_v1_sunset_day_; + +std::string TimeLimitedWords::GetWordByIndex(size_t index) { + DCHECK_EQ(BIP39_WORDLIST_LEN, 2048); + index = index % BIP39_WORDLIST_LEN; + char* word = nullptr; + if (bip39_get_word(nullptr, index, &word) != WALLY_OK) { + LOG(ERROR) << "bip39_get_word failed for index " << index; + return std::string(); + } + + std::string str_word = word; + wally_free_string(word); + + return str_word; +} + +int TimeLimitedWords::GetIndexByWord(const std::string& word) { + std::string word_prepared = base::ToLowerASCII(word); + + struct words* mnemonic_w = nullptr; + if (bip39_get_wordlist(nullptr, &mnemonic_w) != WALLY_OK) { + DCHECK(false); + return -1; + } + + DCHECK_NE(mnemonic_w, nullptr); + size_t idx = wordlist_lookup_word(mnemonic_w, word_prepared.c_str()); + if (!idx) { + return -1; + } + + return idx - 1; +} + +Time TimeLimitedWords::GetWordsV1SunsetDay() { + if (words_v1_sunset_day_.is_null()) { + bool convert_result = + Time::FromUTCString(kWordsv1SunsetDate, &words_v1_sunset_day_); + CHECK(convert_result); + } + + CHECK(!words_v1_sunset_day_.is_null()); + + return words_v1_sunset_day_; +} + +int TimeLimitedWords::GetRoundedDaysDiff(const Time& time1, const Time& time2) { + TimeDelta delta = time2 - time1; + + double delta_in_days_f = delta.InMillisecondsF() / Time::kMillisecondsPerDay; + + int days_rounded = std::round(delta_in_days_f); + return days_rounded; +} + +std::string TimeLimitedWords::GenerateForNow(const std::string& pure_words) { + return TimeLimitedWords::GenerateForDate(pure_words, Time::Now()); +} + +std::string TimeLimitedWords::GenerateForDate(const std::string& pure_words, + const Time& not_after) { + int days_since_qrv2_epoh = + GetRoundedDaysDiff(QrCodeDataValidator::GetQRv1SunsetDay(), not_after); + + if (days_since_qrv2_epoh < 0) { + // Something goes bad, requested |not_after| is even before sync v2 epoch + return std::string(); + } + + std::string last_word = GetWordByIndex(days_since_qrv2_epoh); + + std::string time_limited_code = pure_words + " " + last_word; + return time_limited_code; +} + +WordsValidationResult TimeLimitedWords::Validate( + const std::string& time_limited_words, + std::string* pure_words) { + CHECK_NE(pure_words, nullptr); + *pure_words = std::string(); + + static constexpr size_t kPureWordsCount = 24u; + static constexpr size_t kWordsV2Count = 25u; + + auto now = Time::Now(); + + std::vector words = base::SplitString( + time_limited_words, " ", base::WhitespaceHandling::TRIM_WHITESPACE, + base::SplitResult::SPLIT_WANT_NONEMPTY); + + size_t num_words = words.size(); + + if (num_words == kPureWordsCount) { + if (now < GetWordsV1SunsetDay()) { + std::string recombined_pure_words = base::JoinString( + base::span(words.begin(), kPureWordsCount), " "); + if (crypto::IsPassphraseValid(recombined_pure_words)) { + *pure_words = recombined_pure_words; + return WordsValidationResult::kValid; + } else { + return WordsValidationResult::kNotValidPureWords; + } + } else { + return WordsValidationResult::kVersionDeprecated; + } + } else if (num_words == kWordsV2Count) { + std::string recombined_pure_words = base::JoinString( + base::span(words.begin(), kPureWordsCount), " "); + if (crypto::IsPassphraseValid(recombined_pure_words)) { + int days_actual = + GetRoundedDaysDiff(QrCodeDataValidator::GetQRv1SunsetDay(), now) % + BIP39_WORDLIST_LEN; + + int days_encoded = GetIndexByWord(words[kWordsV2Count - 1]); + DCHECK(days_encoded < BIP39_WORDLIST_LEN); + + int days_abs_diff = std::abs(days_actual - days_encoded); + if (days_abs_diff <= 1) { + *pure_words = recombined_pure_words; + return WordsValidationResult::kValid; + } else if (days_actual > days_encoded) { + return WordsValidationResult::kExpired; + } else if (days_encoded > days_actual) { + return WordsValidationResult::kValidForTooLong; + } + } else { + return WordsValidationResult::kNotValidPureWords; + } + } else { + return WordsValidationResult::kWrongWordsNumber; + } + + NOTREACHED(); + return WordsValidationResult::kNotValidPureWords; +} + +} // namespace brave_sync diff --git a/components/brave_sync/time_limited_words.h b/components/brave_sync/time_limited_words.h new file mode 100644 index 000000000000..328a7737b186 --- /dev/null +++ b/components/brave_sync/time_limited_words.h @@ -0,0 +1,61 @@ +/* Copyright (c) 2022 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_COMPONENTS_BRAVE_SYNC_TIME_LIMITED_WORDS_H_ +#define BRAVE_COMPONENTS_BRAVE_SYNC_TIME_LIMITED_WORDS_H_ + +#include +#include + +#include "base/gtest_prod_util.h" +#include "base/time/time.h" + +namespace brave_sync { + +enum class WordsValidationResult { + kValid = 0, + kNotValidPureWords = 1, + kVersionDeprecated = 2, + kExpired = 3, + kValidForTooLong = 4, + kWrongWordsNumber = 5, +}; + +FORWARD_DECLARE_TEST(TimeLimitedWordsTest, GenerateForDate); +FORWARD_DECLARE_TEST(TimeLimitedWordsTest, GetIndexByWord); +FORWARD_DECLARE_TEST(TimeLimitedWordsTest, GetRoundedDaysDiff); +FORWARD_DECLARE_TEST(TimeLimitedWordsTest, GetWordByIndex); +FORWARD_DECLARE_TEST(TimeLimitedWordsTest, Validate); + +class TimeLimitedWords { + public: + static std::string GenerateForNow(const std::string& pure_words); + + static WordsValidationResult Validate(const std::string& time_limited_words, + std::string* pure_words); + + static base::Time GetWordsV1SunsetDay(); + + private: + FRIEND_TEST_ALL_PREFIXES(TimeLimitedWordsTest, GenerateForDate); + FRIEND_TEST_ALL_PREFIXES(TimeLimitedWordsTest, GetIndexByWord); + FRIEND_TEST_ALL_PREFIXES(TimeLimitedWordsTest, GetRoundedDaysDiff); + FRIEND_TEST_ALL_PREFIXES(TimeLimitedWordsTest, GetWordByIndex); + FRIEND_TEST_ALL_PREFIXES(TimeLimitedWordsTest, Validate); + + static std::string GenerateForDate(const std::string& pure_words, + const base::Time& not_after); + static int GetRoundedDaysDiff(const base::Time& time1, + const base::Time& time2); + + static std::string GetWordByIndex(size_t index); + static int GetIndexByWord(const std::string& word); + + static base::Time words_v1_sunset_day_; +}; + +} // namespace brave_sync + +#endif // BRAVE_COMPONENTS_BRAVE_SYNC_TIME_LIMITED_WORDS_H_ diff --git a/components/brave_sync/time_limited_words_unittest.cc b/components/brave_sync/time_limited_words_unittest.cc new file mode 100644 index 000000000000..0f74dd1aae96 --- /dev/null +++ b/components/brave_sync/time_limited_words_unittest.cc @@ -0,0 +1,185 @@ +/* Copyright (c) 2022 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "brave/components/brave_sync/time_limited_words.h" + +#include + +#include "base/logging.h" +#include "base/test/gtest_util.h" +#include "base/time/time_override.h" +#include "brave/components/brave_sync/qr_code_validator.h" +#include "testing/gtest/include/gtest/gtest.h" + +using base::subtle::ScopedTimeClockOverrides; + +namespace brave_sync { + +namespace { + +const char kValidSyncCode[] = + "fringe digital begin feed equal output proof cheap " + "exotic ill sure question trial squirrel glove celery " + "awkward push jelly logic broccoli almost grocery drift"; + +const char kInvalidSyncCode[] = + "fringe digital begin feed equal output proof cheap " + "exotic ill sure question trial squirrel glove celery " + "awkward push jelly logic broccoli almost grocery driftZ"; + +base::Time g_overridden_now; +std::unique_ptr OverrideWithTimeNow( + const base::Time& overridden_now) { + g_overridden_now = overridden_now; + return std::make_unique( + []() { return g_overridden_now; }, nullptr, nullptr); +} +} // namespace + +TEST(TimeLimitedWordsTest, GetRoundedDaysDiff) { + const base::Time time1 = QrCodeDataValidator::GetQRv1SunsetDay(); + + base::Time time2 = time1 + base::Hours(11); + EXPECT_EQ(TimeLimitedWords::GetRoundedDaysDiff(time1, time2), 0); + + time2 = time1 + base::Hours(13); + EXPECT_EQ(TimeLimitedWords::GetRoundedDaysDiff(time1, time2), 1); + EXPECT_EQ(TimeLimitedWords::GetRoundedDaysDiff(time2, time1), -1); +} + +TEST(TimeLimitedWordsTest, GetIndexByWord) { + EXPECT_EQ(TimeLimitedWords::GetIndexByWord("abandon"), 0); + EXPECT_EQ(TimeLimitedWords::GetIndexByWord("ability"), 1); + EXPECT_EQ(TimeLimitedWords::GetIndexByWord("not_bip39_word"), -1); +} + +TEST(TimeLimitedWordsTest, GetWordByIndex) { + EXPECT_EQ(TimeLimitedWords::GetWordByIndex(0), "abandon"); + EXPECT_EQ(TimeLimitedWords::GetWordByIndex(1), "ability"); + EXPECT_EQ(TimeLimitedWords::GetWordByIndex(2047), "zoo"); + EXPECT_EQ(TimeLimitedWords::GetWordByIndex(2048), "abandon"); +} + +TEST(TimeLimitedWordsTest, GenerateForDate) { + EXPECT_EQ(std::string(kValidSyncCode) + " abandon", + TimeLimitedWords::GenerateForDate( + kValidSyncCode, QrCodeDataValidator::GetQRv1SunsetDay())); + EXPECT_EQ(std::string(kValidSyncCode) + " ability", + TimeLimitedWords::GenerateForDate( + kValidSyncCode, + QrCodeDataValidator::GetQRv1SunsetDay() + base::Days(1))); + EXPECT_EQ("", TimeLimitedWords::GenerateForDate( + kValidSyncCode, + QrCodeDataValidator::GetQRv1SunsetDay() - base::Days(1))); +} + +TEST(TimeLimitedWordsTest, Validate) { + std::string pure_words; + WordsValidationResult result; + + { + // Valid v1 sync code, prior to sunset date + auto time_override = OverrideWithTimeNow( + TimeLimitedWords::GetWordsV1SunsetDay() - base::Days(1)); + result = TimeLimitedWords::Validate(kValidSyncCode, &pure_words); + EXPECT_EQ(result, WordsValidationResult::kValid); + EXPECT_EQ(pure_words, kValidSyncCode); + } + + { + // Valid v1 sync code plus ending space, prior to sunset date + auto time_override = OverrideWithTimeNow( + TimeLimitedWords::GetWordsV1SunsetDay() - base::Days(1)); + result = TimeLimitedWords::Validate(kValidSyncCode + std::string(" "), + &pure_words); + EXPECT_EQ(result, WordsValidationResult::kValid); + EXPECT_EQ(pure_words, kValidSyncCode); + } + + { + // Invalid v1 sync code, prior to sunset date + auto time_override = OverrideWithTimeNow( + TimeLimitedWords::GetWordsV1SunsetDay() - base::Days(1)); + result = TimeLimitedWords::Validate(kInvalidSyncCode, &pure_words); + EXPECT_EQ(result, WordsValidationResult::kNotValidPureWords); + EXPECT_EQ(pure_words, ""); + } + + const base::Time anchorDayForWordsV2 = + QrCodeDataValidator::GetQRv1SunsetDay() + base::Days(20); + const std::string valid25thAnchoredWord = + TimeLimitedWords::GetWordByIndex(20); + const std::string valid25thAnchoredWords = + kValidSyncCode + std::string(" ") + valid25thAnchoredWord; + + { + // Valid v2 sync code, after sunset date, around anchored day + auto time_override = OverrideWithTimeNow(anchorDayForWordsV2); + result = TimeLimitedWords::Validate(valid25thAnchoredWords, &pure_words); + EXPECT_EQ(result, WordsValidationResult::kValid); + EXPECT_EQ(pure_words, kValidSyncCode); + } + + { + // Valid v2 sync code, after sunset date, expired + const std::string valid25thExpiredWord = + TimeLimitedWords::GetWordByIndex(15); + const std::string valid25thExpiredWords = + kValidSyncCode + std::string(" ") + valid25thExpiredWord; + + auto time_override = OverrideWithTimeNow(anchorDayForWordsV2); + result = TimeLimitedWords::Validate(valid25thExpiredWords, &pure_words); + EXPECT_EQ(result, WordsValidationResult::kExpired); + EXPECT_EQ(pure_words, ""); + } + + { + // Valid v2 sync code, after sunset date, valid for too long + const std::string valid25thValidTooLongWord = + TimeLimitedWords::GetWordByIndex(25); + const std::string valid25thValidTooLongWords = + kValidSyncCode + std::string(" ") + valid25thValidTooLongWord; + + auto time_override = OverrideWithTimeNow(anchorDayForWordsV2); + result = + TimeLimitedWords::Validate(valid25thValidTooLongWords, &pure_words); + EXPECT_EQ(result, WordsValidationResult::kValidForTooLong); + EXPECT_EQ(pure_words, ""); + } + + { + // Wrong words number + auto time_override = OverrideWithTimeNow(anchorDayForWordsV2); + result = TimeLimitedWords::Validate("abandon ability", &pure_words); + EXPECT_EQ(result, WordsValidationResult::kWrongWordsNumber); + EXPECT_EQ(pure_words, ""); + + result = TimeLimitedWords::Validate( + valid25thAnchoredWords + " abandon ability", &pure_words); + EXPECT_EQ(result, WordsValidationResult::kWrongWordsNumber); + EXPECT_EQ(pure_words, ""); + } + + { + // Valid v2 sync code, after sunset date, day modulo 2048 which is + // "2027-08-11 00:00:00.000 UTC" + const std::string validModulo2048Word = + TimeLimitedWords::GetWordByIndex(2048); + const std::string validModulo2048Words = + kValidSyncCode + std::string(" ") + validModulo2048Word; + + auto time_override = OverrideWithTimeNow( + QrCodeDataValidator::GetQRv1SunsetDay() + base::Days(2048)); + result = TimeLimitedWords::Validate(validModulo2048Words, &pure_words); + EXPECT_EQ(result, WordsValidationResult::kValid); + EXPECT_EQ(pure_words, kValidSyncCode); + } +} + +TEST(TimeLimitedWordsDeathTest, ValidateCheckWithNullptr) { + EXPECT_CHECK_DEATH(TimeLimitedWords::Validate("abandon ability", nullptr)); +} + +} // namespace brave_sync