From b5746314a5b8932e251f4a5cafabcfff35ecefb5 Mon Sep 17 00:00:00 2001 From: AlexeyBarabash Date: Thu, 26 May 2022 13:23:22 +0300 Subject: [PATCH] Merge pull request #12300 from brave/sync_timelimit_bip Time-limited sync words --- .../chrome/browser/BraveSyncWorker.java | 18 +- .../settings/BraveSyncScreensPreference.java | 63 ++++-- app/brave_generated_resources.grd | 12 + browser/android/brave_sync_worker.cc | 46 ++++ .../brave_sync_browser_proxy.js | 3 + .../brave_sync_code_dialog.html | 10 +- .../brave_sync_page/brave_sync_code_dialog.js | 11 +- .../brave_sync_page/brave_sync_page.js | 6 +- .../brave_sync_page/brave_sync_setup.html | 2 +- .../brave_sync_page/brave_sync_setup.js | 14 +- .../android/strings/android_brave_strings.grd | 3 - .../ui/webui/settings/brave_sync_handler.cc | 86 ++++++- .../ui/webui/settings/brave_sync_handler.h | 1 + components/brave_sync/BUILD.gn | 9 +- .../brave_sync/qr_code_data_unittest.cc | 1 - components/brave_sync/time_limited_words.cc | 209 ++++++++++++++++++ components/brave_sync/time_limited_words.h | 79 +++++++ .../brave_sync/time_limited_words_unittest.cc | 186 ++++++++++++++++ 18 files changed, 698 insertions(+), 61 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/android/java/org/chromium/chrome/browser/BraveSyncWorker.java b/android/java/org/chromium/chrome/browser/BraveSyncWorker.java index 57f4fb0976d1..0a9224d6628f 100644 --- a/android/java/org/chromium/chrome/browser/BraveSyncWorker.java +++ b/android/java/org/chromium/chrome/browser/BraveSyncWorker.java @@ -131,10 +131,14 @@ public void run() { } }; - public String GetCodephrase() { + public String GetPureWords() { return BraveSyncWorkerJni.get().getSyncCodeWords(mNativeBraveSyncWorker); } + public String GetTimeLimitedWordsFromPure(String pureWords) { + return BraveSyncWorkerJni.get().getTimeLimitedWordsFromPure(pureWords); + } + public void SaveCodephrase(String codephrase) { BraveSyncWorkerJni.get().saveCodeWords(mNativeBraveSyncWorker, codephrase); } @@ -159,6 +163,14 @@ public String GetSeedHexFromQrJson(String jsonQr) { return BraveSyncWorkerJni.get().getSeedHexFromQrJson(jsonQr); } + public int GetWordsValidationResult(String timeLimitedWords) { + return BraveSyncWorkerJni.get().getWordsValidationResult(timeLimitedWords); + } + + public String GetPureWordsFromTimeLimited(String timeLimitedWords) { + return BraveSyncWorkerJni.get().getPureWordsFromTimeLimited(timeLimitedWords); + } + public void RequestSync() { BraveSyncWorkerJni.get().requestSync(mNativeBraveSyncWorker); } @@ -204,6 +216,10 @@ interface Natives { String getQrDataJson(String seedHex); int getQrCodeValidationResult(String jsonQr); String getSeedHexFromQrJson(String jsonQr); + int getWordsValidationResult(String timeLimitedWords); + String getPureWordsFromTimeLimited(String timeLimitedWords); + String getTimeLimitedWordsFromPure(String pureWords); + void saveCodeWords(long nativeBraveSyncWorker, String passphrase); void finalizeSyncSetup(long nativeBraveSyncWorker); diff --git a/android/java/org/chromium/chrome/browser/settings/BraveSyncScreensPreference.java b/android/java/org/chromium/chrome/browser/settings/BraveSyncScreensPreference.java index 4118ecc27a8e..266d3fdf51e1 100644 --- a/android/java/org/chromium/chrome/browser/settings/BraveSyncScreensPreference.java +++ b/android/java/org/chromium/chrome/browser/settings/BraveSyncScreensPreference.java @@ -548,6 +548,31 @@ private void setSyncText(String title, String message, TextView textView) { textView.setText(formatedText); } + private String getWordsValidationString(String words) { + int validationResult = getBraveSyncWorker().GetWordsValidationResult(words); + Log.v(TAG, "validationResult is " + validationResult); + switch (validationResult) { + case 0: + // kValid, empty string indicates there is no error + return ""; + case 2: + // kVersionDeprecated + return getResources().getString(R.string.brave_sync_code_from_deprecated_version); + case 3: + // kExpired + return getResources().getString(R.string.brave_sync_code_expired); + case 4: + // kValidForTooLong + return getResources().getString(R.string.brave_sync_code_valid_for_too_long); + + default: + // These three different types of errors have the same message + // kWrongWordsNumber + // kNotValidPureWords + return getResources().getString(R.string.brave_sync_wrong_code_error); + } + } + /** OnClickListener for the clear button. We show an alert dialog to confirm the action */ @Override public void onClick(View v) { @@ -570,7 +595,7 @@ public void onClick(View v) { setJoinExistingChainLayout(); } else if (mStartNewChainButton == v) { // Creating a new chain - GetCodephrase(); + GetPureWords(); setNewChainLayout(); seedWordsReceived(mCodephrase); } else if (mMobileButton == v) { @@ -615,27 +640,18 @@ public void onClick(View v) { .replace(" ", " ") .replace("\n", " ") .split(" "); - if (BIP39_WORD_COUNT != words.length) { - Log.e(TAG, "Confirm code words - wrong words count " + words.length); - onSyncError(getResources().getString(R.string.brave_sync_word_count_error)); - return; - } - String hexString = getBraveSyncWorker().GetSeedHexFromWords( - TextUtils.join(" ", words)); - if (hexString == null || hexString.isEmpty()) { + String trimmedWords = TextUtils.join(" ", words); + String validationError = getWordsValidationString(trimmedWords); + if (!validationError.isEmpty()) { Log.e(TAG, "Confirm code words - wrong codephrase"); - onSyncError(getResources().getString(R.string.brave_sync_wrong_code_error)); + onSyncError(validationError); return; } - String codephraseCandidate = TextUtils.join(" ", words); - // Validate the code words with GetSeedHexFromWords - String seedHex = getBraveSyncWorker().GetSeedHexFromWords(codephraseCandidate); - if (null == seedHex || seedHex.isEmpty()) { - onSyncError(getResources().getString(R.string.brave_sync_wrong_code_error)); - return; - } + String codephraseCandidate = + getBraveSyncWorker().GetPureWordsFromTimeLimited(trimmedWords); + assert codephraseCandidate != null && !codephraseCandidate.isEmpty(); showFinalSecurityWarning(FinalWarningFor.CODE_WORDS, () -> { // We have the confirmation from user @@ -1220,9 +1236,9 @@ private void setNewChainLayout() { } private String mCodephrase; - public String GetCodephrase() { + public String GetPureWords() { if (mCodephrase == null || mCodephrase.isEmpty()) { - mCodephrase = getBraveSyncWorker().GetCodephrase(); + mCodephrase = getBraveSyncWorker().GetPureWords(); } return mCodephrase; } @@ -1268,7 +1284,7 @@ private void setAddMobileDeviceLayout() { getActivity().runOnUiThread(new Runnable() { @Override public void run() { - String seedHex = getBraveSyncWorker().GetSeedHexFromWords(GetCodephrase()); + String seedHex = getBraveSyncWorker().GetSeedHexFromWords(GetPureWords()); if (null == seedHex || seedHex.isEmpty()) { // Give up, seed must be valid Log.e(TAG, "setAddMobileDeviceLayout seedHex is empty"); @@ -1362,9 +1378,12 @@ private void setAddLaptopLayout() { getActivity().runOnUiThread(new Runnable() { @Override public void run() { - String codePhrase = GetCodephrase(); + String codePhrase = GetPureWords(); assert codePhrase != null && !codePhrase.isEmpty(); - mBraveSyncAddDeviceCodeWords.setText(codePhrase); + String timeLimitedWords = + getBraveSyncWorker().GetTimeLimitedWordsFromPure(codePhrase); + assert timeLimitedWords != null && !timeLimitedWords.isEmpty(); + mBraveSyncAddDeviceCodeWords.setText(timeLimitedWords); } }); } diff --git a/app/brave_generated_resources.grd b/app/brave_generated_resources.grd index 67a612d9e541..2480df0b7aaf 100644 --- a/app/brave_generated_resources.grd +++ b/app/brave_generated_resources.grd @@ -861,6 +861,18 @@ Or change later at $2brave://settings/ext Invalid sync code, please check and try again. + + This code is invalid + + + This code was generated by a deprecated version of Brave, please upgrade your other device first + + + This code has expired. Please generate a new one on your other device and try again. + + + This code is invalid. Please check that the time and timezone are set correctly on your device. + Leave Sync Chain diff --git a/browser/android/brave_sync_worker.cc b/browser/android/brave_sync_worker.cc index 6b76d39b6b10..ef8f33b05c84 100644 --- a/browser/android/brave_sync_worker.cc +++ b/browser/android/brave_sync_worker.cc @@ -20,6 +20,7 @@ #include "brave/components/brave_sync/qr_code_data.h" #include "brave/components/brave_sync/qr_code_validator.h" #include "brave/components/brave_sync/sync_service_impl_helper.h" +#include "brave/components/brave_sync/time_limited_words.h" #include "brave/components/sync/driver/brave_sync_service_impl.h" #include "chrome/browser/profiles/profile.h" @@ -351,6 +352,51 @@ int JNI_BraveSyncWorker_GetQrCodeValidationResult( brave_sync::QrCodeDataValidator::ValidateQrDataJson(str_json_qr)); } +int JNI_BraveSyncWorker_GetWordsValidationResult( + JNIEnv* env, + const base::android::JavaParamRef& time_limited_words) { + std::string str_time_limited_words = + base::android::ConvertJavaStringToUTF8(time_limited_words); + DCHECK(!str_time_limited_words.empty()); + + auto pure_words_with_status = + brave_sync::TimeLimitedWords::Parse(str_time_limited_words); + + return static_cast(pure_words_with_status.status); +} + +static base::android::ScopedJavaLocalRef +JNI_BraveSyncWorker_GetPureWordsFromTimeLimited( + JNIEnv* env, + const base::android::JavaParamRef& time_limited_words) { + std::string str_time_limited_words = + base::android::ConvertJavaStringToUTF8(time_limited_words); + DCHECK(!str_time_limited_words.empty()); + + auto pure_words_with_status = + brave_sync::TimeLimitedWords::Parse(str_time_limited_words); + DCHECK_EQ(pure_words_with_status.status, + brave_sync::WordsValidationStatus::kValid); + DCHECK(pure_words_with_status.pure_words.has_value()); + + return base::android::ConvertUTF8ToJavaString( + env, pure_words_with_status.pure_words.value()); +} + +static base::android::ScopedJavaLocalRef +JNI_BraveSyncWorker_GetTimeLimitedWordsFromPure( + JNIEnv* env, + const base::android::JavaParamRef& pure_words) { + std::string str_pure_words = + base::android::ConvertJavaStringToUTF8(pure_words); + DCHECK(!str_pure_words.empty()); + + std::string time_limited_words = + brave_sync::TimeLimitedWords::GenerateForNow(str_pure_words); + + return base::android::ConvertUTF8ToJavaString(env, time_limited_words); +} + static base::android::ScopedJavaLocalRef JNI_BraveSyncWorker_GetSeedHexFromQrJson( JNIEnv* env, diff --git a/browser/resources/settings/brave_sync_page/brave_sync_browser_proxy.js b/browser/resources/settings/brave_sync_page/brave_sync_browser_proxy.js index 27d5109c0fcf..42f79a2f4b0b 100644 --- a/browser/resources/settings/brave_sync_page/brave_sync_browser_proxy.js +++ b/browser/resources/settings/brave_sync_page/brave_sync_browser_proxy.js @@ -9,6 +9,9 @@ export class BraveSyncBrowserProxy { getSyncCode() { return sendWithPromise('SyncSetupGetSyncCode'); } + getPureSyncCode() { + return sendWithPromise('SyncSetupGetPureSyncCode'); + } getQRCode(syncCode) { return sendWithPromise('SyncGetQRCode', syncCode); } diff --git a/browser/resources/settings/brave_sync_page/brave_sync_code_dialog.html b/browser/resources/settings/brave_sync_page/brave_sync_code_dialog.html index 19cf6892c0a9..f78299cc74b9 100644 --- a/browser/resources/settings/brave_sync_page/brave_sync_code_dialog.html +++ b/browser/resources/settings/brave_sync_page/brave_sync_code_dialog.html @@ -333,12 +333,12 @@

$i18n{braveSyncChooseDeviceComputerTitle}

+
diff --git a/browser/resources/settings/brave_sync_page/brave_sync_code_dialog.js b/browser/resources/settings/brave_sync_page/brave_sync_code_dialog.js index 78e2091a7fb0..ce18bbcfa759 100644 --- a/browser/resources/settings/brave_sync_page/brave_sync_code_dialog.js +++ b/browser/resources/settings/brave_sync_page/brave_sync_code_dialog.js @@ -44,9 +44,9 @@ Polymer({ value: 'choose', notify: true }, - isInvalidSyncCode: { - type: Boolean, - value: false, + syncCodeValidationError: { + type: String, + value: '', notify: true }, syncCodeWordCount_: { @@ -64,7 +64,6 @@ Polymer({ }, observers: [ - 'updateSyncCodeValidity_(syncCode)', 'getQRCode_(syncCode, codeType)', ], @@ -75,10 +74,6 @@ Polymer({ this.syncBrowserProxy_ = BraveSyncBrowserProxy.getInstance(); }, - updateSyncCodeValidity_: function() { - this.isInvalidSyncCode = false - }, - computeSyncCodeWordCount_: function() { if (!this.syncCode) { return 0 diff --git a/browser/resources/settings/brave_sync_page/brave_sync_page.js b/browser/resources/settings/brave_sync_page/brave_sync_page.js index df5962796327..1ee6df479450 100644 --- a/browser/resources/settings/brave_sync_page/brave_sync_page.js +++ b/browser/resources/settings/brave_sync_page/brave_sync_page.js @@ -99,11 +99,11 @@ Polymer({ */ handleSyncPrefsChanged_: async function(syncPrefs) { if (this.syncStatus_ && !this.syncStatus_.firstSetupInProgress) { - const syncCode = await this.braveBrowserProxy_.getSyncCode() + const pureSyncCode = await this.braveBrowserProxy_.getPureSyncCode() if (syncPrefs.passphraseRequired) { - await this.browserProxy_.setDecryptionPassphrase(syncCode); + await this.browserProxy_.setDecryptionPassphrase(pureSyncCode); } else if (!this.isEncryptionSet_) { - this.browserProxy_.setEncryptionPassphrase(syncCode) + this.browserProxy_.setEncryptionPassphrase(pureSyncCode) .then(successfullySet => { this.isEncryptionSet_ = successfullySet }) diff --git a/browser/resources/settings/brave_sync_page/brave_sync_setup.html b/browser/resources/settings/brave_sync_page/brave_sync_setup.html index 97f87866b3d1..da7dc9b10054 100644 --- a/browser/resources/settings/brave_sync_page/brave_sync_setup.html +++ b/browser/resources/settings/brave_sync_page/brave_sync_setup.html @@ -62,7 +62,7 @@

$i18n{braveSyncSetupTitle}

diff --git a/browser/resources/settings/brave_sync_page/brave_sync_setup.js b/browser/resources/settings/brave_sync_page/brave_sync_setup.js index aab1a247bc27..8a478c22d9c7 100644 --- a/browser/resources/settings/brave_sync_page/brave_sync_setup.js +++ b/browser/resources/settings/brave_sync_page/brave_sync_setup.js @@ -43,10 +43,10 @@ Polymer({ type: Boolean, value: false, }, - isInvalidSyncCode_: { - type: Boolean, - value: false, - } + syncCodeValidationError_: { + type: String, + value: '', + } }, /** @private {?BraveSyncBrowserProxy} */ @@ -88,13 +88,11 @@ Polymer({ try { success = await this.syncBrowserProxy_.setSyncCode(syncCodeToSubmit) } catch (e) { - console.error("Error setting sync code") + this.syncCodeValidationError_ = e success = false } this.isSubmittingSyncCode_ = false - if (!success) { - this.isInvalidSyncCode_ = true - } else { + if (success) { this.syncCodeDialogType_ = undefined this.fire('setup-success') } diff --git a/browser/ui/android/strings/android_brave_strings.grd b/browser/ui/android/strings/android_brave_strings.grd index 4042ddd412bb..c04b43b6e0e9 100644 --- a/browser/ui/android/strings/android_brave_strings.grd +++ b/browser/ui/android/strings/android_brave_strings.grd @@ -706,9 +706,6 @@ until they verify, or until 90 days have passed. Word count: %1$d - - Incorrect number of words - Wrong sync code diff --git a/browser/ui/webui/settings/brave_sync_handler.cc b/browser/ui/webui/settings/brave_sync_handler.cc index f81a694a75e7..b2c463ddf6b4 100644 --- a/browser/ui/webui/settings/brave_sync_handler.cc +++ b/browser/ui/webui/settings/brave_sync_handler.cc @@ -15,8 +15,10 @@ #include "brave/components/brave_sync/crypto/crypto.h" #include "brave/components/brave_sync/qr_code_data.h" #include "brave/components/brave_sync/sync_service_impl_helper.h" +#include "brave/components/brave_sync/time_limited_words.h" #include "brave/components/sync/driver/brave_sync_service_impl.h" #include "brave/components/sync_device_info/brave_device_info.h" +#include "brave/grit/brave_generated_resources.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/sync/device_info_sync_service_factory.h" #include "chrome/browser/sync/sync_service_factory.h" @@ -25,8 +27,37 @@ #include "components/sync_device_info/device_info_tracker.h" #include "components/sync_device_info/local_device_info_provider.h" #include "content/public/browser/web_ui.h" +#include "ui/base/l10n/l10n_util.h" #include "ui/base/webui/web_ui_util.h" +using brave_sync::TimeLimitedWords; +using brave_sync::WordsValidationStatus; + +namespace { + +std::string GetSyncCodeValidationString( + WordsValidationStatus validation_result) { + switch (validation_result) { + case WordsValidationStatus::kValid: + return ""; + case WordsValidationStatus::kWrongWordsNumber: + case WordsValidationStatus::kNotValidPureWords: + return l10n_util::GetStringUTF8(IDS_BRAVE_SYNC_CODE_INVALID); + case WordsValidationStatus::kVersionDeprecated: + return l10n_util::GetStringUTF8( + IDS_BRAVE_SYNC_CODE_FROM_DEPRECATED_VERSION); + case WordsValidationStatus::kExpired: + return l10n_util::GetStringUTF8(IDS_BRAVE_SYNC_CODE_EXPIRED); + case WordsValidationStatus::kValidForTooLong: + return l10n_util::GetStringUTF8(IDS_BRAVE_SYNC_CODE_VALID_FOR_TOO_LONG); + default: + NOTREACHED(); + return ""; + } +} + +} // namespace + BraveSyncHandler::BraveSyncHandler() : weak_ptr_factory_(this) {} BraveSyncHandler::~BraveSyncHandler() {} @@ -45,6 +76,10 @@ void BraveSyncHandler::RegisterMessages() { "SyncSetupGetSyncCode", base::BindRepeating(&BraveSyncHandler::HandleGetSyncCode, base::Unretained(this))); + web_ui()->RegisterMessageCallback( + "SyncSetupGetPureSyncCode", + base::BindRepeating(&BraveSyncHandler::HandleGetPureSyncCode, + base::Unretained(this))); web_ui()->RegisterMessageCallback( "SyncGetQRCode", base::BindRepeating(&BraveSyncHandler::HandleGetQRCode, base::Unretained(this))); @@ -86,6 +121,22 @@ void BraveSyncHandler::HandleGetSyncCode(const base::Value::List& args) { AllowJavascript(); CHECK_EQ(1U, args.size()); + auto* sync_service = GetSyncService(); + std::string sync_code; + if (sync_service) + sync_code = sync_service->GetOrCreateSyncCode(); + + std::string time_limited_sync_code = + TimeLimitedWords::GenerateForNow(sync_code); + + ResolveJavascriptCallback(args[0].Clone(), + base::Value(time_limited_sync_code)); +} + +void BraveSyncHandler::HandleGetPureSyncCode(const base::Value::List& args) { + AllowJavascript(); + CHECK_EQ(1U, args.size()); + auto* sync_service = GetSyncService(); std::string sync_code; if (sync_service) @@ -98,10 +149,18 @@ void BraveSyncHandler::HandleGetQRCode(const base::Value::List& args) { AllowJavascript(); CHECK_EQ(2U, args.size()); CHECK(args[1].is_string()); - const std::string sync_code = args[1].GetString(); + const std::string time_limited_sync_code = args[1].GetString(); + + // Sync code arrives here with time-limit 25th word, remove it to get proper + // pure seed for QR generation (QR codes have their own expiry) + auto pure_words_with_status = TimeLimitedWords::Parse(time_limited_sync_code); + CHECK_EQ(pure_words_with_status.status, WordsValidationStatus::kValid); + CHECK(pure_words_with_status.pure_words); + CHECK_NE(pure_words_with_status.pure_words.value().size(), 0u); std::vector seed; - if (!brave_sync::crypto::PassphraseToBytes32(sync_code, &seed)) { + if (!brave_sync::crypto::PassphraseToBytes32( + pure_words_with_status.pure_words.value(), &seed)) { LOG(ERROR) << "invalid sync code when generating qr code"; RejectJavascriptCallback(args[0].Clone(), base::Value("invalid sync code")); return; @@ -145,16 +204,31 @@ void BraveSyncHandler::HandleSetSyncCode(const base::Value::List& args) { AllowJavascript(); CHECK_EQ(2U, args.size()); CHECK(args[1].is_string()); - const std::string sync_code = args[1].GetString(); - - if (sync_code.empty()) { + const std::string time_limited_sync_code = args[1].GetString(); + if (time_limited_sync_code.empty()) { LOG(ERROR) << "No sync code parameter provided!"; RejectJavascriptCallback(args[0].Clone(), base::Value(false)); return; } + auto pure_words_with_status = TimeLimitedWords::Parse(time_limited_sync_code); + + if (pure_words_with_status.status != WordsValidationStatus::kValid) { + LOG(ERROR) << "Could not validate a sync code, validation_result=" + << static_cast(pure_words_with_status.status) << " " + << GetSyncCodeValidationString(pure_words_with_status.status); + RejectJavascriptCallback(args[0].Clone(), + base::Value(GetSyncCodeValidationString( + pure_words_with_status.status))); + return; + } + + CHECK(pure_words_with_status.pure_words); + CHECK(!pure_words_with_status.pure_words.value().empty()); + auto* sync_service = GetSyncService(); - if (!sync_service || !sync_service->SetSyncCode(sync_code)) { + if (!sync_service || + !sync_service->SetSyncCode(pure_words_with_status.pure_words.value())) { RejectJavascriptCallback(args[0].Clone(), base::Value(false)); return; } diff --git a/browser/ui/webui/settings/brave_sync_handler.h b/browser/ui/webui/settings/brave_sync_handler.h index f41fd53b5a4a..7df6ff873fff 100644 --- a/browser/ui/webui/settings/brave_sync_handler.h +++ b/browser/ui/webui/settings/brave_sync_handler.h @@ -41,6 +41,7 @@ class BraveSyncHandler : public settings::SettingsPageUIHandler, // Custom message handlers: void HandleGetDeviceList(const base::Value::List& args); void HandleGetSyncCode(const base::Value::List& args); + void HandleGetPureSyncCode(const base::Value::List& args); void HandleSetSyncCode(const base::Value::List& args); void HandleGetQRCode(const base::Value::List& args); void HandleReset(const base::Value::List& args); 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/qr_code_data_unittest.cc b/components/brave_sync/qr_code_data_unittest.cc index 25d960a3b599..7babbbd5565d 100644 --- a/components/brave_sync/qr_code_data_unittest.cc +++ b/components/brave_sync/qr_code_data_unittest.cc @@ -8,7 +8,6 @@ #include #include -#include "base/logging.h" #include "base/time/time.h" #include "testing/gtest/include/gtest/gtest.h" diff --git a/components/brave_sync/time_limited_words.cc b/components/brave_sync/time_limited_words.cc new file mode 100644 index 000000000000..044a90949a0d --- /dev/null +++ b/components/brave_sync/time_limited_words.cc @@ -0,0 +1,209 @@ +/* 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/strcat.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/vendor/bip39wally-core-native/include/wally_bip39.h" +#include "brave/vendor/bip39wally-core-native/src/wordlist.h" + +namespace brave_sync { + +namespace { + +static constexpr char kWordsv1SunsetDate[] = "Mon, 1 Aug 2022 00:00:00 GMT"; +static constexpr char kWordsv2Epoch[] = "Tue, 10 May 2022 00:00:00 GMT"; + +} // namespace + +TimeLimitedWords::PureWordsWithStatus::PureWordsWithStatus() = default; + +TimeLimitedWords::PureWordsWithStatus::PureWordsWithStatus( + PureWordsWithStatus&& other) = default; + +TimeLimitedWords::PureWordsWithStatus::~PureWordsWithStatus() = default; + +TimeLimitedWords::PureWordsWithStatus& +TimeLimitedWords::PureWordsWithStatus::operator=(PureWordsWithStatus&& other) = + default; + +using base::Time; +using base::TimeDelta; + +Time TimeLimitedWords::words_v1_sunset_day_; +Time TimeLimitedWords::words_v2_epoch_; + +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_; +} + +Time TimeLimitedWords::GetWordsV2Epoch() { + if (words_v2_epoch_.is_null()) { + bool convert_result = Time::FromUTCString(kWordsv2Epoch, &words_v2_epoch_); + CHECK(convert_result); + } + + CHECK(!words_v2_epoch_.is_null()); + + return words_v2_epoch_; +} + +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_words_v2_epoch = + GetRoundedDaysDiff(GetWordsV2Epoch(), not_after); + + if (days_since_words_v2_epoch < 0) { + // Something goes bad, requested |not_after| is even before sync v2 epoch + return std::string(); + } + + std::string last_word = GetWordByIndex(days_since_words_v2_epoch); + + std::string time_limited_code = base::StrCat({pure_words, " ", last_word}); + return time_limited_code; +} + +WordsValidationStatus 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 WordsValidationStatus::kValid; + } else { + return WordsValidationStatus::kNotValidPureWords; + } + } else { + return WordsValidationStatus::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(GetWordsV2Epoch(), 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 WordsValidationStatus::kValid; + } else if (days_actual > days_encoded) { + return WordsValidationStatus::kExpired; + } else if (days_encoded > days_actual) { + return WordsValidationStatus::kValidForTooLong; + } + } else { + return WordsValidationStatus::kNotValidPureWords; + } + } else { + return WordsValidationStatus::kWrongWordsNumber; + } + + NOTREACHED(); + return WordsValidationStatus::kNotValidPureWords; +} + +TimeLimitedWords::PureWordsWithStatus TimeLimitedWords::Parse( + const std::string& time_limited_words) { + PureWordsWithStatus ret; + std::string pure_words; + ret.status = Validate(time_limited_words, &pure_words); + + if (ret.status == WordsValidationStatus::kValid) { + ret.pure_words = pure_words; + } else { + ret.pure_words = absl::nullopt; + } + + return ret; +} + +} // 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..87515e7e33ec --- /dev/null +++ b/components/brave_sync/time_limited_words.h @@ -0,0 +1,79 @@ +/* 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" +#include "third_party/abseil-cpp/absl/types/optional.h" + +namespace brave_sync { + +enum class WordsValidationStatus { + 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, Parse); + +class TimeLimitedWords { + public: + struct PureWordsWithStatus { + PureWordsWithStatus(); + PureWordsWithStatus(PureWordsWithStatus&& other); + PureWordsWithStatus& operator=(PureWordsWithStatus&& other); + + PureWordsWithStatus(const PureWordsWithStatus&) = delete; + PureWordsWithStatus& operator=(const PureWordsWithStatus&) = delete; + + ~PureWordsWithStatus(); + + absl::optional pure_words; + WordsValidationStatus status; + }; + + static std::string GenerateForNow(const std::string& pure_words); + static PureWordsWithStatus Parse(const std::string& time_limited_words); + + static base::Time GetWordsV1SunsetDay(); + static base::Time GetWordsV2Epoch(); + + 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, Parse); + + static WordsValidationStatus Validate(const std::string& time_limited_words, + std::string* pure_words); + + 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_; + static base::Time words_v2_epoch_; +}; + +} // 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..e9c629ff2138 --- /dev/null +++ b/components/brave_sync/time_limited_words_unittest.cc @@ -0,0 +1,186 @@ +/* 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/strings/strcat.h" +#include "base/time/time_override.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 = TimeLimitedWords::GetWordsV2Epoch(); + + 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(base::StrCat({kValidSyncCode, " abandon"}), + TimeLimitedWords::GenerateForDate( + kValidSyncCode, TimeLimitedWords::GetWordsV2Epoch())); + EXPECT_EQ( + base::StrCat({kValidSyncCode, " ability"}), + TimeLimitedWords::GenerateForDate( + kValidSyncCode, TimeLimitedWords::GetWordsV2Epoch() + base::Days(1))); + EXPECT_EQ("", TimeLimitedWords::GenerateForDate( + kValidSyncCode, + TimeLimitedWords::GetWordsV2Epoch() - base::Days(1))); +} + +TEST(TimeLimitedWordsTest, Parse) { + std::string pure_words; + TimeLimitedWords::PureWordsWithStatus pure_words_with_status; + + { + // Valid v1 sync code, prior to sunset date + auto time_override = OverrideWithTimeNow( + TimeLimitedWords::GetWordsV1SunsetDay() - base::Days(1)); + pure_words_with_status = TimeLimitedWords::Parse(kValidSyncCode); + EXPECT_EQ(pure_words_with_status.status, WordsValidationStatus::kValid); + EXPECT_EQ(pure_words_with_status.pure_words.value(), kValidSyncCode); + } + + { + // Valid v1 sync code plus ending space, prior to sunset date + auto time_override = OverrideWithTimeNow( + TimeLimitedWords::GetWordsV1SunsetDay() - base::Days(1)); + pure_words_with_status = + TimeLimitedWords::Parse(base::StrCat({kValidSyncCode, " "})); + EXPECT_EQ(pure_words_with_status.status, WordsValidationStatus::kValid); + EXPECT_EQ(pure_words_with_status.pure_words.value(), kValidSyncCode); + } + + { + // Invalid v1 sync code, prior to sunset date + auto time_override = OverrideWithTimeNow( + TimeLimitedWords::GetWordsV1SunsetDay() - base::Days(1)); + pure_words_with_status = TimeLimitedWords::Parse(kInvalidSyncCode); + EXPECT_EQ(pure_words_with_status.status, + WordsValidationStatus::kNotValidPureWords); + EXPECT_FALSE(pure_words_with_status.pure_words.has_value()); + } + + const base::Time anchorDayForWordsV2 = + TimeLimitedWords::GetWordsV2Epoch() + base::Days(20); + const std::string valid25thAnchoredWord = + TimeLimitedWords::GetWordByIndex(20); + const std::string valid25thAnchoredWords = + base::StrCat({kValidSyncCode, " ", valid25thAnchoredWord}); + + { + // Valid v2 sync code, after sunset date, around anchored day + auto time_override = OverrideWithTimeNow(anchorDayForWordsV2); + pure_words_with_status = TimeLimitedWords::Parse(valid25thAnchoredWords); + EXPECT_EQ(pure_words_with_status.status, WordsValidationStatus::kValid); + EXPECT_EQ(pure_words_with_status.pure_words.value(), kValidSyncCode); + } + + { + // Valid v2 sync code, after sunset date, expired + const std::string valid25thExpiredWord = + TimeLimitedWords::GetWordByIndex(15); + const std::string valid25thExpiredWords = + base::StrCat({kValidSyncCode, " ", valid25thExpiredWord}); + + auto time_override = OverrideWithTimeNow(anchorDayForWordsV2); + pure_words_with_status = TimeLimitedWords::Parse(valid25thExpiredWords); + EXPECT_EQ(pure_words_with_status.status, WordsValidationStatus::kExpired); + EXPECT_FALSE(pure_words_with_status.pure_words.has_value()); + } + + { + // Valid v2 sync code, after sunset date, valid for too long + const std::string valid25thValidTooLongWord = + TimeLimitedWords::GetWordByIndex(25); + const std::string valid25thValidTooLongWords = + base::StrCat({kValidSyncCode, " ", valid25thValidTooLongWord}); + + auto time_override = OverrideWithTimeNow(anchorDayForWordsV2); + pure_words_with_status = + TimeLimitedWords::Parse(valid25thValidTooLongWords); + EXPECT_EQ(pure_words_with_status.status, + WordsValidationStatus::kValidForTooLong); + EXPECT_FALSE(pure_words_with_status.pure_words.has_value()); + } + + { + // Wrong words number + auto time_override = OverrideWithTimeNow(anchorDayForWordsV2); + pure_words_with_status = TimeLimitedWords::Parse("abandon ability"); + EXPECT_EQ(pure_words_with_status.status, + WordsValidationStatus::kWrongWordsNumber); + EXPECT_FALSE(pure_words_with_status.pure_words.has_value()); + + pure_words_with_status = TimeLimitedWords::Parse( + base::StrCat({valid25thAnchoredWords, " abandon ability"})); + EXPECT_EQ(pure_words_with_status.status, + WordsValidationStatus::kWrongWordsNumber); + EXPECT_FALSE(pure_words_with_status.pure_words.has_value()); + } + + { + // Valid v2 sync code, after sunset date, day modulo 2048 which is + // "2027-11-23 00:00:00.000 UTC" + // Note: While this date is way too far into the future, the codes repeat + // after a few years and so this becomes valid again, an unfortunate + // limitation of this scheme. + const std::string validModulo2048Word = + TimeLimitedWords::GetWordByIndex(2048); + const std::string validModulo2048Words = + base::StrCat({kValidSyncCode, " ", validModulo2048Word}); + + auto time_override = OverrideWithTimeNow( + TimeLimitedWords::GetWordsV2Epoch() + base::Days(2048)); + pure_words_with_status = TimeLimitedWords::Parse(validModulo2048Words); + EXPECT_EQ(pure_words_with_status.status, WordsValidationStatus::kValid); + EXPECT_EQ(pure_words_with_status.pure_words.value(), kValidSyncCode); + } +} + +} // namespace brave_sync