From db44bc25d3d39df21bd5ad6d2af31a50506b140c Mon Sep 17 00:00:00 2001 From: Sale Djenic Date: Thu, 21 Jul 2022 13:29:18 +0200 Subject: [PATCH] feat(@desktop/keycard): initial keycard implementation Keycard implementation affected onboarding/login flows. - new user - first run - new keys into keycard - new user - first run - import seed phrase into keycard - old user - first run - login importing from keycard - login the app using keycard Fixes: #5972 --- .gitmodules | 3 + Makefile | 32 +- config.nims | 2 +- src/app/boot/app_controller.nim | 10 +- src/app/global/local_account_settings.nim | 27 - src/app/modules/main/controller.nim | 8 +- src/app/modules/startup/controller.nim | 264 ++++- .../startup/internal/biometrics_state.nim | 43 +- .../internal/keycard_create_pin_state.nim | 23 + .../keycard_display_seed_phrase_state.nim | 16 + .../startup/internal/keycard_empty_state.nim | 24 + .../internal/keycard_enter_pin_state.nim | 45 + .../internal/keycard_enter_puk_state.nim | 44 + .../keycard_enter_seed_phrase_words_state.nim | 24 + .../internal/keycard_insert_keycard_state.nim | 15 + .../startup/internal/keycard_locked_state.nim | 12 + ...eycard_max_pairing_slots_reached_state.nim | 28 + .../keycard_max_pin_retries_reached_state.nim | 12 + .../keycard_max_puk_retries_reached_state.nim | 40 + .../internal/keycard_not_empty_state.nim | 31 + .../internal/keycard_pin_set_state.nim | 40 + .../internal/keycard_plugin_reader_state.nim | 27 + .../keycard_reading_keycard_state.nim | 72 ++ .../internal/keycard_recover_state.nim | 23 + .../internal/keycard_repeat_pin_state.nim | 65 ++ .../internal/keycard_wrong_pin_state.nim | 41 + .../internal/keycard_wrong_puk_state.nim | 58 + .../internal/login_keycard_empty_state.nim | 30 + .../login_keycard_enter_pin_state.nim | 40 + .../login_keycard_insert_keycard_state.nim | 21 + ..._keycard_max_pin_retries_reached_state.nim | 22 + ..._keycard_max_puk_retries_reached_state.nim | 12 + .../login_keycard_reading_keycard_state.nim | 55 + .../internal/login_keycard_wrong_keycard.nim | 15 + .../login_keycard_wrong_pin_state.nim | 43 + .../modules/startup/internal/login_state.nim | 26 +- .../startup/internal/notification_state.nim | 6 +- src/app/modules/startup/internal/state.nim | 57 +- .../startup/internal/state_factory.nim | 134 +++ .../internal/user_profile_chat_key_state.nim | 14 +- .../user_profile_confirm_password_state.nim | 28 +- .../user_profile_create_password_state.nim | 8 +- .../internal/user_profile_create_state.nim | 8 +- .../user_profile_enter_seed_phrase_state.nim | 49 +- .../user_profile_import_seed_phrase_state.nim | 10 +- .../startup/internal/welcome_state.nim | 11 +- .../internal/welcome_state_new_user.nim | 20 +- .../internal/welcome_state_old_user.nim | 23 +- src/app/modules/startup/io_interface.nim | 30 +- .../startup/models/login_account_item.nim | 11 +- .../startup/models/login_account_model.nim | 10 +- src/app/modules/startup/module.nim | 153 ++- .../startup/selected_login_account.nim | 17 +- src/app/modules/startup/view.nim | 41 +- src/app_service/common/mnemonics.nim | 8 + src/app_service/service/accounts/service.nim | 155 ++- src/app_service/service/eth/utils.nim | 10 - .../service/keycard/async_tasks.nim | 12 + src/app_service/service/keycard/constants.nim | 66 ++ src/app_service/service/keycard/internal.nim | 57 + src/app_service/service/keycard/service.nim | 240 ++++ src/app_service/service/keychain/service.nim | 14 +- src/backend/accounts.nim | 28 + src/constants.nim | 1 + src/nim_status_client.nim | 22 +- .../Onboarding/OnboardingLayout.qml | 113 +- .../panels/AccountMenuItemPanel.qml | 23 +- .../popups/KeycardCreatePINModal.qml | 1 - .../Onboarding/stores/StartupStore.qml | 24 + .../Onboarding/views/InsertDetailsView.qml | 1 + .../views/KeycardFlowSelectionView.qml | 115 -- .../Onboarding/views/KeycardInitView.qml | 97 ++ .../Onboarding/views/KeycardPinView.qml | 200 ++++ .../Onboarding/views/KeycardPukView.qml | 195 ++++ .../Onboarding/views/KeycardStateView.qml | 288 +++++ .../Onboarding/views/KeysMainView.qml | 25 +- .../AppLayouts/Onboarding/views/LoginView.qml | 1012 +++++++++++++---- .../Onboarding/views/SeedPhraseInputView.qml | 19 +- .../Onboarding/views/SeedPhraseView.qml | 128 +++ .../views/SeedPhraseWordsInputView.qml | 187 +++ .../Onboarding/views/TouchIDAuthView.qml | 12 +- .../Profile/popups/ChangePasswordModal.qml | 2 +- .../Profile/stores/AdvancedStore.qml | 5 - .../AppLayouts/Profile/views/AdvancedView.qml | 12 - .../views/profile/MyProfileSettingsView.qml | 6 +- .../assets/icons/keycard/card-error3@2x.svg | 24 + .../assets/icons/keycard/card-error3@3x.svg | 24 + .../assets/icons/keycard/card-success3@2x.svg | 23 + .../assets/icons/keycard/card-success3@3x.svg | 23 + .../assets/icons/keycard/card-wrong3@2x.svg | 21 + .../assets/icons/keycard/card-wrong3@3x.svg | 21 + ui/imports/assets/icons/keycard/card0@2x.svg | 20 + ui/imports/assets/icons/keycard/card0@3x.svg | 20 + ui/imports/assets/icons/keycard/card1@2x.svg | 21 + ui/imports/assets/icons/keycard/card1@3x.svg | 21 + ui/imports/assets/icons/keycard/card2@2x.svg | 22 + ui/imports/assets/icons/keycard/card2@3x.svg | 22 + ui/imports/assets/icons/keycard/card3@2x.svg | 23 + ui/imports/assets/icons/keycard/card3@3x.svg | 23 + ui/imports/shared/keycard/InsertCard.qml | 72 -- ui/imports/shared/keycard/PINModal.qml | 73 -- ui/imports/shared/keycard/PairingModal.qml | 68 -- ui/imports/shared/keycard/qmldir | 3 - ui/imports/shared/stores/RootStore.qml | 1 - ui/imports/utils/Constants.qml | 63 +- ui/imports/utils/Utils.qml | 4 + vendor/nim-keycard-go | 2 +- vendor/nim-status-go | 2 +- vendor/status-keycard-go | 1 + 109 files changed, 4677 insertions(+), 921 deletions(-) create mode 100644 src/app/modules/startup/internal/keycard_create_pin_state.nim create mode 100644 src/app/modules/startup/internal/keycard_display_seed_phrase_state.nim create mode 100644 src/app/modules/startup/internal/keycard_empty_state.nim create mode 100644 src/app/modules/startup/internal/keycard_enter_pin_state.nim create mode 100644 src/app/modules/startup/internal/keycard_enter_puk_state.nim create mode 100644 src/app/modules/startup/internal/keycard_enter_seed_phrase_words_state.nim create mode 100644 src/app/modules/startup/internal/keycard_insert_keycard_state.nim create mode 100644 src/app/modules/startup/internal/keycard_locked_state.nim create mode 100644 src/app/modules/startup/internal/keycard_max_pairing_slots_reached_state.nim create mode 100644 src/app/modules/startup/internal/keycard_max_pin_retries_reached_state.nim create mode 100644 src/app/modules/startup/internal/keycard_max_puk_retries_reached_state.nim create mode 100644 src/app/modules/startup/internal/keycard_not_empty_state.nim create mode 100644 src/app/modules/startup/internal/keycard_pin_set_state.nim create mode 100644 src/app/modules/startup/internal/keycard_plugin_reader_state.nim create mode 100644 src/app/modules/startup/internal/keycard_reading_keycard_state.nim create mode 100644 src/app/modules/startup/internal/keycard_recover_state.nim create mode 100644 src/app/modules/startup/internal/keycard_repeat_pin_state.nim create mode 100644 src/app/modules/startup/internal/keycard_wrong_pin_state.nim create mode 100644 src/app/modules/startup/internal/keycard_wrong_puk_state.nim create mode 100644 src/app/modules/startup/internal/login_keycard_empty_state.nim create mode 100644 src/app/modules/startup/internal/login_keycard_enter_pin_state.nim create mode 100644 src/app/modules/startup/internal/login_keycard_insert_keycard_state.nim create mode 100644 src/app/modules/startup/internal/login_keycard_max_pin_retries_reached_state.nim create mode 100644 src/app/modules/startup/internal/login_keycard_max_puk_retries_reached_state.nim create mode 100644 src/app/modules/startup/internal/login_keycard_reading_keycard_state.nim create mode 100644 src/app/modules/startup/internal/login_keycard_wrong_keycard.nim create mode 100644 src/app/modules/startup/internal/login_keycard_wrong_pin_state.nim create mode 100644 src/app/modules/startup/internal/state_factory.nim create mode 100644 src/app_service/common/mnemonics.nim create mode 100644 src/app_service/service/keycard/async_tasks.nim create mode 100644 src/app_service/service/keycard/constants.nim create mode 100644 src/app_service/service/keycard/internal.nim create mode 100644 src/app_service/service/keycard/service.nim delete mode 100644 ui/app/AppLayouts/Onboarding/views/KeycardFlowSelectionView.qml create mode 100644 ui/app/AppLayouts/Onboarding/views/KeycardInitView.qml create mode 100644 ui/app/AppLayouts/Onboarding/views/KeycardPinView.qml create mode 100644 ui/app/AppLayouts/Onboarding/views/KeycardPukView.qml create mode 100644 ui/app/AppLayouts/Onboarding/views/KeycardStateView.qml create mode 100644 ui/app/AppLayouts/Onboarding/views/SeedPhraseView.qml create mode 100644 ui/app/AppLayouts/Onboarding/views/SeedPhraseWordsInputView.qml create mode 100644 ui/imports/assets/icons/keycard/card-error3@2x.svg create mode 100644 ui/imports/assets/icons/keycard/card-error3@3x.svg create mode 100644 ui/imports/assets/icons/keycard/card-success3@2x.svg create mode 100644 ui/imports/assets/icons/keycard/card-success3@3x.svg create mode 100644 ui/imports/assets/icons/keycard/card-wrong3@2x.svg create mode 100644 ui/imports/assets/icons/keycard/card-wrong3@3x.svg create mode 100644 ui/imports/assets/icons/keycard/card0@2x.svg create mode 100644 ui/imports/assets/icons/keycard/card0@3x.svg create mode 100644 ui/imports/assets/icons/keycard/card1@2x.svg create mode 100644 ui/imports/assets/icons/keycard/card1@3x.svg create mode 100644 ui/imports/assets/icons/keycard/card2@2x.svg create mode 100644 ui/imports/assets/icons/keycard/card2@3x.svg create mode 100644 ui/imports/assets/icons/keycard/card3@2x.svg create mode 100644 ui/imports/assets/icons/keycard/card3@3x.svg delete mode 100644 ui/imports/shared/keycard/InsertCard.qml delete mode 100644 ui/imports/shared/keycard/PINModal.qml delete mode 100644 ui/imports/shared/keycard/PairingModal.qml delete mode 100644 ui/imports/shared/keycard/qmldir create mode 160000 vendor/status-keycard-go diff --git a/.gitmodules b/.gitmodules index 06ccad6b80..8f2fdac91b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -118,3 +118,6 @@ [submodule "vendor/nim-keycard-go"] path = vendor/nim-keycard-go url = https://github.com/status-im/nim-keycard-go +[submodule "vendor/status-keycard-go"] + path = vendor/status-keycard-go + url = https://github.com/status-im/status-keycard-go.git diff --git a/Makefile b/Makefile index f1ee1cc574..990fd79630 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ BUILD_SYSTEM_DIR := vendor/nimbus-build-system run-macos \ run-windows \ status-go \ - keycard-go \ + status-keycard-go \ update ifeq ($(NIM_PARAMS),) @@ -223,15 +223,15 @@ $(STATUSGO): | deps $(MAKE) statusgo-shared-library $(HANDLE_OUTPUT) -KEYCARDGO := vendor/nim-keycard-go/go/keycard/build/libkeycard/libkeycard.$(LIBSTATUS_EXT) -KEYCARDGO_LIBDIR := $(shell pwd)/$(shell dirname "$(KEYCARDGO)") -export KEYCARDGO_LIBDIR +STATUSKEYCARDGO := vendor/status-keycard-go/build/libkeycard/libkeycard.$(LIBSTATUS_EXT) +STATUSKEYCARDGO_LIBDIR := $(shell pwd)/$(shell dirname "$(STATUSKEYCARDGO)") +export STATUSKEYCARDGO_LIBDIR -keycard-go: $(KEYCARDGO) -$(KEYCARDGO): | deps - echo -e $(BUILD_MSG) "keycard-go" - + cd vendor/nim-keycard-go && \ - $(MAKE) build-keycard-go $(HANDLE_OUTPUT) +status-keycard-go: $(STATUSKEYCARDGO) +$(STATUSKEYCARDGO): | deps + echo -e $(BUILD_MSG) "status-keycard-go" + + cd vendor/status-keycard-go && \ + $(MAKE) build-lib $(HANDLE_OUTPUT) QRCODEGEN := vendor/QR-Code-generator/c/libqrcodegen.a @@ -331,9 +331,9 @@ else endif $(NIM_STATUS_CLIENT): NIM_PARAMS += $(RESOURCES_LAYOUT) -$(NIM_STATUS_CLIENT): $(NIM_SOURCES) | $(DOTHERSIDE) $(STATUSGO) $(KEYCARDGO) $(QRCODEGEN) $(FLEETS) rcc $(QM_BINARIES) deps +$(NIM_STATUS_CLIENT): $(NIM_SOURCES) | $(DOTHERSIDE) $(STATUSGO) $(STATUSKEYCARDGO) $(QRCODEGEN) $(FLEETS) rcc $(QM_BINARIES) deps echo -e $(BUILD_MSG) "$@" && \ - $(ENV_SCRIPT) nim c $(NIM_PARAMS) --passL:"-L$(STATUSGO_LIBDIR)" --passL:"-lstatus" --passL:"-L$(KEYCARDGO_LIBDIR)" --passL:"-lkeycard" $(NIM_EXTRA_PARAMS) --passL:"$(QRCODEGEN)" --passL:"-lm" src/nim_status_client.nim && \ + $(ENV_SCRIPT) nim c $(NIM_PARAMS) --passL:"-L$(STATUSGO_LIBDIR)" --passL:"-lstatus" --passL:"-L$(STATUSKEYCARDGO_LIBDIR)" --passL:"-lkeycard" $(NIM_EXTRA_PARAMS) --passL:"$(QRCODEGEN)" --passL:"-lm" src/nim_status_client.nim && \ [[ $$? = 0 ]] && \ (([[ $(detected_OS) = Darwin ]] && \ install_name_tool -change \ @@ -407,7 +407,7 @@ $(STATUS_CLIENT_APPIMAGE): nim_status_client $(APPIMAGE_TOOL) nim-status.desktop cp -r /usr/lib/x86_64-linux-gnu/gstreamer1.0 tmp/linux/dist/usr/lib/ cp vendor/status-go/build/bin/libstatus.so tmp/linux/dist/usr/lib/ cp vendor/status-go/build/bin/libstatus.so.0 tmp/linux/dist/usr/lib/ - cp $(KEYCARDGO) tmp/linux/dist/usr/lib/ + cp $(STATUSKEYCARDGO) tmp/linux/dist/usr/lib/ echo -e $(BUILD_MSG) "AppImage" linuxdeployqt tmp/linux/dist/nim-status.desktop -no-copy-copyright-files -qmldir=ui -qmlimport=$(QTDIR)/qml -bundle-non-qt-libs @@ -530,7 +530,7 @@ $(STATUS_CLIENT_EXE): nim_status_client nim_windows_launcher $(NIM_WINDOWS_PREBU cp bin/nim_windows_launcher.exe $(OUTPUT)/Status.exe rcedit $(OUTPUT)/bin/Status.exe --set-icon $(OUTPUT)/resources/status.ico rcedit $(OUTPUT)/Status.exe --set-icon $(OUTPUT)/resources/status.ico - cp $(DOTHERSIDE) $(STATUSGO) $(KEYCARDGO) tmp/windows/tools/*.dll $(OUTPUT)/bin/ + cp $(DOTHERSIDE) $(STATUSGO) $(STATUSKEYCARDGO) tmp/windows/tools/*.dll $(OUTPUT)/bin/ cp "$(shell which libgcc_s_seh-1.dll)" $(OUTPUT)/bin/ cp "$(shell which libwinpthread-1.dll)" $(OUTPUT)/bin/ echo -e $(BUILD_MSG) "deployable folder" @@ -575,7 +575,7 @@ pkg-windows: check-pkg-target-windows $(STATUS_CLIENT_EXE) zip-windows: check-pkg-target-windows $(STATUS_CLIENT_7Z) clean: | clean-common - rm -rf bin/* node_modules bottles/* pkg/* tmp/* $(STATUSGO) $(KEYCARDGO) + rm -rf bin/* node_modules bottles/* pkg/* tmp/* $(STATUSGO) $(STATUSKEYCARDGO) + $(MAKE) -C vendor/DOtherSide/build --no-print-directory clean force-rebuild-status-go: @@ -594,7 +594,7 @@ $(ICON_TOOL): run-linux: nim_status_client echo -e "\e[92mRunning:\e[39m bin/nim_status_client" - LD_LIBRARY_PATH="$(QT5_LIBDIR)":"$(STATUSGO_LIBDIR)":"$(KEYCARDGO_LIBDIR)" \ + LD_LIBRARY_PATH="$(QT5_LIBDIR)":"$(STATUSGO_LIBDIR)":"$(STATUSKEYCARDGO_LIBDIR)" \ ./bin/nim_status_client run-macos: nim_status_client $(ICON_TOOL) @@ -609,7 +609,7 @@ run-macos: nim_status_client $(ICON_TOOL) run-windows: nim_status_client $(NIM_WINDOWS_PREBUILT_DLLS) echo -e "\e[92mRunning:\e[39m bin/nim_status_client.exe" - PATH="$(shell pwd)"/"$(shell dirname "$(DOTHERSIDE)")":"$(STATUSGO_LIBDIR)":"$(KEYCARDGO_LIBDIR)":"$(shell pwd)"/"$(shell dirname "$(NIM_WINDOWS_PREBUILT_DLLS)")":"$(PATH)" \ + PATH="$(shell pwd)"/"$(shell dirname "$(DOTHERSIDE)")":"$(STATUSGO_LIBDIR)":"$(STATUSKEYCARDGO_LIBDIR)":"$(shell pwd)"/"$(shell dirname "$(NIM_WINDOWS_PREBUILT_DLLS)")":"$(PATH)" \ ./bin/nim_status_client.exe endif # "variables.mk" was not included diff --git a/config.nims b/config.nims index 7c9ead325a..7236ca9545 100644 --- a/config.nims +++ b/config.nims @@ -16,7 +16,7 @@ if defined(macosx): # note: macdeployqt rewrites rpath appropriately when building the .app bundle switch("passL", "-rpath" & " " & getEnv("QT5_LIBDIR")) switch("passL", "-rpath" & " " & getEnv("STATUSGO_LIBDIR")) - switch("passL", "-rpath" & " " & getEnv("KEYCARDGO_LIBDIR")) + switch("passL", "-rpath" & " " & getEnv("STATUSKEYCARDGO_LIBDIR")) # statically link these libs switch("passL", "bottles/openssl@1.1/lib/libcrypto.a") switch("passL", "bottles/openssl@1.1/lib/libssl.a") diff --git a/src/app/boot/app_controller.nim b/src/app/boot/app_controller.nim index adec10018f..373c6fa948 100644 --- a/src/app/boot/app_controller.nim +++ b/src/app/boot/app_controller.nim @@ -2,6 +2,7 @@ import NimQml, chronicles import ../../app_service/service/general/service as general_service import ../../app_service/service/keychain/service as keychain_service +import ../../app_service/service/keycard/service as keycard_service import ../../app_service/service/accounts/service as accounts_service import ../../app_service/service/contacts/service as contacts_service import ../../app_service/service/language/service as language_service @@ -56,6 +57,7 @@ type # Services generalService: general_service.Service + keycardService*: keycard_service.Service keychainService: keychain_service.Service accountsService: accounts_service.Service contactsService: contacts_service.Service @@ -126,7 +128,8 @@ proc newAppController*(statusFoundation: StatusFoundation): AppController = result.globalUtilsVariant = newQVariant(singletonInstance.utils) # Services - result.generalService = general_service.newService() + result.generalService = general_service.newService() + result.keycardService = keycard_service.newService(statusFoundation.events, statusFoundation.threadpool) result.nodeConfigurationService = node_configuration_service.newService(statusFoundation.fleetConfiguration, result.settingsService) result.keychainService = keychain_service.newService(statusFoundation.events) @@ -189,7 +192,8 @@ proc newAppController*(statusFoundation: StatusFoundation): AppController = result.keychainService, result.accountsService, result.generalService, - result.profileService + result.profileService, + result.keycardService ) result.mainModule = main_module.newModule[AppController]( result, @@ -272,6 +276,7 @@ proc delete*(self: AppController) = self.generalService.delete self.ensService.delete self.gifService.delete + self.keycardService.delete proc startupDidLoad*(self: AppController) = singletonInstance.engine.setRootContextProperty("localAppSettings", self.localAppSettingsVariant) @@ -290,6 +295,7 @@ proc mainDidLoad*(self: AppController) = self.mainModule.checkForStoringPassword() proc start*(self: AppController) = + self.keycardService.init() self.keychainService.init() self.generalService.init() self.accountsService.init() diff --git a/src/app/global/local_account_settings.nim b/src/app/global/local_account_settings.nim index 01ee93ef2b..5a59688c1f 100644 --- a/src/app/global/local_account_settings.nim +++ b/src/app/global/local_account_settings.nim @@ -5,8 +5,6 @@ import ../../constants # Local Account Settings keys: const LS_KEY_STORE_TO_KEYCHAIN* = "storeToKeychain" const DEFAULT_STORE_TO_KEYCHAIN = "notNow" -const LS_KEY_IS_KEYCARD_ENABLED* = "isKeycardEnabled" -const DEFAULT_IS_KEYCARD_ENABLED = false # Local Account Settings values: const LS_VALUE_STORE* = "store" const LS_VALUE_NOTNOW* = "notNow" @@ -35,23 +33,17 @@ QtObject: proc setFileName*(self: LocalAccountSettings, fileName: string) = if(not self.settings.isNil): self.settings.delete - let filePath = os.joinPath(self.settingsFileDir, fileName) self.settings = newQSettings(filePath, QSettingsFormat.IniFormat) proc storeToKeychainValueChanged*(self: LocalAccountSettings) {.signal.} - proc isKeycardEnabledChanged*(self: LocalAccountSettings) {.signal.} proc removeKey*(self: LocalAccountSettings, key: string) = if(self.settings.isNil): return - self.settings.remove(key) - if(key == LS_KEY_STORE_TO_KEYCHAIN): self.storeToKeychainValueChanged() - elif(key == LS_KEY_IS_KEYCARD_ENABLED): - self.isKeycardEnabledChanged() proc getStoreToKeychainValue*(self: LocalAccountSettings): string {.slot.} = if(self.settings.isNil): @@ -70,22 +62,3 @@ QtObject: read = getStoreToKeychainValue write = setStoreToKeychainValue notify = storeToKeychainValueChanged - - - proc getIsKeycardEnabled*(self: LocalAccountSettings): bool {.slot.} = - if(self.settings.isNil): - return DEFAULT_IS_KEYCARD_ENABLED - - self.settings.value(LS_KEY_IS_KEYCARD_ENABLED).boolVal - - proc setIsKeycardEnabled*(self: LocalAccountSettings, value: bool) {.slot.} = - if(self.settings.isNil): - return - - self.settings.setValue(LS_KEY_IS_KEYCARD_ENABLED, newQVariant(value)) - self.isKeycardEnabledChanged() - - QtProperty[bool] isKeycardEnabled: - read = getIsKeycardEnabled - write = setIsKeycardEnabled - notify = isKeycardEnabledChanged diff --git a/src/app/modules/main/controller.nim b/src/app/modules/main/controller.nim index 62b58bb8b1..f58a34d0fd 100644 --- a/src/app/modules/main/controller.nim +++ b/src/app/modules/main/controller.nim @@ -91,8 +91,10 @@ proc init*(self: Controller) = self.events.on(SIGNAL_KEYCHAIN_SERVICE_ERROR) do(e:Args): let args = KeyChainServiceArg(e) - singletonInstance.localAccountSettings.removeKey(LS_KEY_STORE_TO_KEYCHAIN) - self.delegate.emitStoringPasswordError(args.errDescription) + # with the following condition we guard unintentional props deletion from the `.ini` file + if self.accountsService.getLoggedInAccount().isValid(): + singletonInstance.localAccountSettings.removeKey(LS_KEY_STORE_TO_KEYCHAIN) + self.delegate.emitStoringPasswordError(args.errDescription) self.events.on(SIGNAL_COMMUNITY_JOINED) do(e:Args): let args = CommunityArgs(e) @@ -253,7 +255,7 @@ proc storePassword*(self: Controller, password: string) = if (value != LS_VALUE_STORE or account.name.len == 0): return - self.keychainService.storePassword(account.name, password) + self.keychainService.storeData(account.name, password) proc getActiveSectionId*(self: Controller): string = result = self.activeSectionId diff --git a/src/app/modules/startup/controller.nim b/src/app/modules/startup/controller.nim index 4c3b9887a4..6cd1418c65 100644 --- a/src/app/modules/startup/controller.nim +++ b/src/app/modules/startup/controller.nim @@ -1,4 +1,4 @@ -import chronicles +import chronicles, strutils, os import io_interface @@ -9,12 +9,14 @@ import ../../../app_service/service/general/service as general_service import ../../../app_service/service/accounts/service as accounts_service import ../../../app_service/service/keychain/service as keychain_service import ../../../app_service/service/profile/service as profile_service +import ../../../app_service/service/keycard/service as keycard_service logScope: topics = "startup-controller" type ProfileImageDetails = object url*: string + croppedImage*: string x1*: int y1*: int x2*: int @@ -28,18 +30,29 @@ type accountsService: accounts_service.Service keychainService: keychain_service.Service profileService: profile_service.Service + keycardService: keycard_service.Service tmpProfileImageDetails: ProfileImageDetails tmpDisplayName: string tmpPassword: string - tmpMnemonic: string tmpSelectedLoginAccountKeyUid: string + tmpPin: string + tmpPinMatch: bool + tmpPuk: string + tmpValidPuk: bool + tmpSeedPhrase: string + tmpSeedPhraseLength: int + tmpKeyUid: string + tmpKeycardEvent: KeycardEvent + tmpKeychainErrorOccurred: bool + tmpRecoverUsingSeedPhraseWhileLogin: bool proc newController*(delegate: io_interface.AccessInterface, events: EventEmitter, generalService: general_service.Service, accountsService: accounts_service.Service, keychainService: keychain_service.Service, - profileService: profile_service.Service): + profileService: profile_service.Service, + keycardService: keycard_service.Service): Controller = result = Controller() result.delegate = delegate @@ -48,6 +61,14 @@ proc newController*(delegate: io_interface.AccessInterface, result.accountsService = accountsService result.keychainService = keychainService result.profileService = profileService + result.keycardService = keycardService + result.tmpPinMatch = false + result.tmpSeedPhraseLength = 0 + result.tmpKeychainErrorOccurred = true + result.tmpRecoverUsingSeedPhraseWhileLogin = false + +# Forward declaration +proc cleanTmpData(self: Controller) proc delete*(self: Controller) = discard @@ -56,10 +77,12 @@ proc init*(self: Controller) = self.events.on(SignalType.NodeLogin.event) do(e:Args): let signal = NodeSignal(e) self.delegate.onNodeLogin(signal.event.error) + self.cleanTmpData() self.events.on(SignalType.NodeStopped.event) do(e:Args): self.events.emit("nodeStopped", Args()) self.accountsService.clear() + self.cleanTmpData() self.delegate.emitLogOut() self.events.on(SignalType.NodeReady.event) do(e:Args): @@ -71,11 +94,12 @@ proc init*(self: Controller) = self.events.on(SIGNAL_KEYCHAIN_SERVICE_ERROR) do(e:Args): let args = KeyChainServiceArg(e) - # We are notifying user only about keychain errors. - if (args.errType == ERROR_TYPE_AUTHENTICATION): - return - singletonInstance.localAccountSettings.removeKey(LS_KEY_STORE_TO_KEYCHAIN) - self.delegate.emitObtainingPasswordError(args.errDescription) + self.tmpKeychainErrorOccurred = true + self.delegate.emitObtainingPasswordError(args.errDescription, args.errType) + + self.events.on(SignalKeycardResponse) do(e: Args): + let args = KeycardArgs(e) + self.delegate.onKeycardResponse(args.flowType, args.flowEvent) proc shouldStartWithOnboardingScreen*(self: Controller): bool = return self.accountsService.openedAccounts().len == 0 @@ -86,22 +110,22 @@ proc getGeneratedAccounts*(self: Controller): seq[GeneratedAccountDto] = proc getImportedAccount*(self: Controller): GeneratedAccountDto = return self.accountsService.getImportedAccount() -proc generateImages*(self: Controller, image: string, aX: int, aY: int, bX: int, bY: int): seq[general_service.Image] = - return self.generalService.generateImages(image, aX, aY, bX, bY) - proc getPasswordStrengthScore*(self: Controller, password, userName: string): int = return self.generalService.getPasswordStrengthScore(password, userName) proc generateImage*(self: Controller, imageUrl: string, aX: int, aY: int, bX: int, bY: int): string = let formatedImg = singletonInstance.utils.formatImagePath(imageUrl) - let images = self.generateImages(formatedImg, aX, aY, bX, bY) + let images = self.generalService.generateImages(formatedImg, aX, aY, bX, bY) if(images.len == 0): return for img in images: if(img.imgType == "large"): - self.tmpProfileImageDetails = ProfileImageDetails(url: imageUrl, x1: aX, y1: aY, x2: bX, y2: bY) + self.tmpProfileImageDetails = ProfileImageDetails(url: imageUrl, croppedImage: img.uri, x1: aX, y1: aY, x2: bX, y2: bY) return img.uri +proc getCroppedProfileImage*(self: Controller): string = + return self.tmpProfileImageDetails.croppedImage + proc setDisplayName*(self: Controller, value: string) = self.tmpDisplayName = value @@ -114,12 +138,83 @@ proc setPassword*(self: Controller, value: string) = proc getPassword*(self: Controller): string = return self.tmpPassword +proc setPin*(self: Controller, value: string) = + self.tmpPin = value + +proc getPin*(self: Controller): string = + return self.tmpPin + +proc setPinMatch*(self: Controller, value: bool) = + self.tmpPinMatch = value + +proc getPinMatch*(self: Controller): bool = + return self.tmpPinMatch + +proc setPuk*(self: Controller, value: string) = + self.tmpPuk = value + +proc getPuk*(self: Controller): string = + return self.tmpPuk + +proc setPukValid*(self: Controller, value: bool) = + self.tmpValidPuk = value + +proc getValidPuk*(self: Controller): bool = + return self.tmpValidPuk + +proc setSeedPhrase*(self: Controller, value: string) = + let words = value.split(" ") + self.tmpSeedPhrase = value + self.tmpSeedPhraseLength = words.len + +proc getSeedPhrase*(self: Controller): string = + return self.tmpSeedPhrase + +proc getSeedPhraseLength*(self: Controller): int = + return self.tmpSeedPhraseLength + +proc setKeyUid*(self: Controller, value: string) = + self.tmpKeyUid = value + +proc setKeycardData*(self: Controller, value: string) = + self.delegate.setKeycardData(value) + +proc setKeycardEvent*(self: Controller, value: KeycardEvent) = + self.tmpKeycardEvent = value + +proc keychainErrorOccurred*(self: Controller): bool = + return self.tmpKeychainErrorOccurred + +proc setRecoverUsingSeedPhraseWhileLogin*(self: Controller, value: bool) = + self.tmpRecoverUsingSeedPhraseWhileLogin = value + +proc getRecoverUsingSeedPhraseWhileLogin*(self: Controller): bool = + return self.tmpRecoverUsingSeedPhraseWhileLogin + +proc cleanTmpData(self: Controller) = + self.tmpSelectedLoginAccountKeyUid = "" + self.tmpProfileImageDetails = ProfileImageDetails() + self.tmpKeychainErrorOccurred = true + self.setDisplayName("") + self.setPassword("") + self.setPin("") + self.setPinMatch(false) + self.setPuk("") + self.setPukValid(false) + self.setSeedPhrase("") + self.setKeyUid("") + self.setKeycardEvent(KeycardEvent()) + self.setRecoverUsingSeedPhraseWhileLogin(false) + proc storePasswordToKeychain(self: Controller) = let account = self.accountsService.getLoggedInAccount() let value = singletonInstance.localAccountSettings.getStoreToKeychainValue() if (value != LS_VALUE_STORE or account.name.len == 0): return - self.keychainService.storePassword(account.name, self.tmpPassword) + #TODO: we should store PubKey of this account instead of display name (display name is not unique) + # and we may run into a problem if 2 accounts with the same display name are generated. + let data = if self.tmpPassword.len > 0: self.tmpPassword else: self.tmpPin + self.keychainService.storeData(account.name, data) proc storeIdentityImage*(self: Controller) = if self.tmpProfileImageDetails.url.len == 0: @@ -130,8 +225,24 @@ proc storeIdentityImage*(self: Controller) = self.tmpProfileImageDetails.y1, self.tmpProfileImageDetails.x2, self.tmpProfileImageDetails.y2) self.tmpProfileImageDetails = ProfileImageDetails() -proc setupAccount(self: Controller, accountId: string, storeToKeychain: bool) = - let error = self.accountsService.setupAccount(accountId, self.tmpPassword, self.tmpDisplayName) +proc validMnemonic*(self: Controller, mnemonic: string): bool = + let err = self.accountsService.validateMnemonic(mnemonic) + if err.len == 0: + self.setSeedPhrase(mnemonic) + return true + return false + +proc importMnemonic*(self: Controller): bool = + let error = self.accountsService.importMnemonic(self.tmpSeedPhrase) + if(error.len == 0): + self.delegate.importAccountSuccess() + return true + else: + self.delegate.importAccountError(error) + return false + +proc setupAccount(self: Controller, accountId: string, storeToKeychain: bool, keycardUsage: bool) = + let error = self.accountsService.setupAccount(accountId, self.tmpPassword, self.tmpDisplayName, keycardUsage) if error != "": self.delegate.setupAccountError(error) else: @@ -140,8 +251,6 @@ proc setupAccount(self: Controller, accountId: string, storeToKeychain: bool) = self.storePasswordToKeychain() else: singletonInstance.localAccountSettings.setStoreToKeychainValue(LS_VALUE_NEVER) - self.setPassword("") - self.setDisplayName("") proc storeGeneratedAccountAndLogin*(self: Controller, storeToKeychain: bool) = let accounts = self.getGeneratedAccounts() @@ -149,27 +258,30 @@ proc storeGeneratedAccountAndLogin*(self: Controller, storeToKeychain: bool) = error "list of generated accounts is empty" return let accountId = accounts[0].id - self.setupAccount(accountId, storeToKeychain) + self.setupAccount(accountId, storeToKeychain, keycardUsage = false) proc storeImportedAccountAndLogin*(self: Controller, storeToKeychain: bool) = let accountId = self.getImportedAccount().id - self.setupAccount(accountId, storeToKeychain) + self.setupAccount(accountId, storeToKeychain, keycardUsage = false) -proc validMnemonic*(self: Controller, mnemonic: string): bool = - let err = self.accountsService.validateMnemonic(mnemonic) - if err.len == 0: - self.tmpMnemonic = mnemonic - return true - return false - -proc importMnemonic*(self: Controller): bool = - let error = self.accountsService.importMnemonic(self.tmpMnemonic) - if(error.len == 0): - self.delegate.importAccountSuccess() - return true +proc storeKeycardAccountAndLogin*(self: Controller, storeToKeychain: bool) = + if self.importMnemonic(): + let accountId = self.getImportedAccount().id + self.setupAccount(accountId, storeToKeychain, keycardUsage = true) else: - self.delegate.importAccountError(error) - return false + error "an error ocurred while importing mnemonic" + +proc setupKeycardAccount*(self: Controller, storeToKeychain: bool) = + if self.tmpSeedPhrase.len > 0: + # if `tmpSeedPhrase` is not empty means user has recovered keycard via seed phrase + self.storeKeycardAccountAndLogin(storeToKeychain) + else: + self.accountsService.setupAccountKeycard(self.tmpKeycardEvent) + if storeToKeychain: + singletonInstance.localAccountSettings.setStoreToKeychainValue(LS_VALUE_STORE) + self.storePasswordToKeychain() + else: + singletonInstance.localAccountSettings.setStoreToKeychainValue(LS_VALUE_NEVER) proc getOpenedAccounts*(self: Controller): seq[AccountDto] = return self.accountsService.openedAccounts() @@ -180,21 +292,95 @@ proc getSelectedLoginAccount(self: Controller): AccountDto = if(acc.keyUid == self.tmpSelectedLoginAccountKeyUid): return acc +proc keyUidMatch*(self: Controller, keyUid: string): bool = + return self.tmpSelectedLoginAccountKeyUid == keyUid + proc setSelectedLoginAccountKeyUid*(self: Controller, keyUid: string) = self.tmpSelectedLoginAccountKeyUid = keyUid + let selectedAccount = self.getSelectedLoginAccount() + singletonInstance.localAccountSettings.setFileName(selectedAccount.name) + +proc isKeycardCreatedAccountSelectedOne*(self: Controller): bool = + let selectedAccount = self.getSelectedLoginAccount() + return selectedAccount.keycardPairing.len > 0 + +proc tryToObtainDataFromKeychain*(self: Controller) = # Dealing with Keychain is the MacOS only feature if(not defined(macosx)): return - let selectedAccount = self.getSelectedLoginAccount() - singletonInstance.localAccountSettings.setFileName(selectedAccount.name) let value = singletonInstance.localAccountSettings.getStoreToKeychainValue() if (value != LS_VALUE_STORE): return - self.keychainService.tryToObtainPassword(selectedAccount.name) + self.tmpKeychainErrorOccurred = false + let selectedAccount = self.getSelectedLoginAccount() + self.keychainService.tryToObtainData(selectedAccount.name) proc login*(self: Controller) = let selectedAccount = self.getSelectedLoginAccount() let error = self.accountsService.login(selectedAccount, self.tmpPassword) - self.setPassword("") if(error.len > 0): - self.delegate.emitAccountLoginError(error) \ No newline at end of file + self.delegate.emitAccountLoginError(error) + +proc loginAccountKeycard*(self: Controller) = + let error = self.accountsService.loginAccountKeycard(self.tmpKeycardEvent) + if(error.len > 0): + self.delegate.emitAccountLoginError(error) + +proc cancelCurrentFlow(self: Controller) = + self.keycardService.cancelCurrentFlow() + # in most cases we're running another flow after canceling the current one, + # this way we're giving to the keycard some time to cancel the current flow + sleep(200) + +proc runLoadAccountFlow*(self: Controller, factoryReset = false) = + self.cancelCurrentFlow() # before running into any flow we're making sure that the previous flow is canceled + self.keycardService.startLoadAccountFlow(factoryReset) + +proc runLoadAccountFlowWithSeedPhrase*(self: Controller, seedPhraseLength: int, seedPhrase: string, factoryReset = false) = + self.cancelCurrentFlow() # before running into any flow we're making sure that the previous flow is canceled + self.keycardService.startLoadAccountFlowWithSeedPhrase(seedPhraseLength, seedPhrase, factoryReset) + +proc runLoginFlow*(self: Controller) = + self.cancelCurrentFlow() # before running into any flow we're making sure that the previous flow is canceled + self.keycardService.startLoginFlow() + +proc startLoginFlowAutomatically*(self: Controller, pin: string) = + self.cancelCurrentFlow() # before running into any flow we're making sure that the previous flow is canceled + self.keycardService.startLoginFlowAutomatically(pin) + +proc runRecoverAccountFlow*(self: Controller) = + self.cancelCurrentFlow() # before running into any flow we're making sure that the previous flow is canceled + self.keycardService.startRecoverAccountFlow() + +proc resumeCurrentFlow*(self: Controller) = + self.keycardService.resumeCurrentFlow() + +proc resumeCurrentFlowLater*(self: Controller) = + self.keycardService.resumeCurrentFlowLater() + +proc factoryReset*(self: Controller) = + self.keycardService.factoryReset() + +proc storePinToKeycard*(self: Controller, pin: string, puk: string) = + self.keycardService.storePin(pin, puk) + +proc enterKeycardPin*(self: Controller, pin: string) = + self.keycardService.enterPin(pin) + +proc enterKeycardPuk*(self: Controller, puk: string) = + self.keycardService.enterPuk(puk) + +proc storeSeedPhraseToKeycard*(self: Controller, seedPhraseLength: int, seedPhrase: string) = + self.keycardService.storeSeedPhrase(seedPhraseLength, seedPhrase) + +proc buildSeedPhrasesFromIndexes*(self: Controller, seedPhraseIndexes: seq[int]) = + if seedPhraseIndexes.len == 0: + let err = "cannot generate mnemonic" + error "keycard error: ", err + ## TODO: we should not be ever in this block, but maybe we can cancel flow and reset states (as the app was just strated) + return + let sp = self.keycardService.buildSeedPhrasesFromIndexes(seedPhraseIndexes) + self.setSeedPhrase(sp.join(" ")) + +proc generateRandomPUK*(self: Controller): string = + return self.keycardService.generateRandomPUK() \ No newline at end of file diff --git a/src/app/modules/startup/internal/biometrics_state.nim b/src/app/modules/startup/internal/biometrics_state.nim index bab4fe78e3..8eaeb58be4 100644 --- a/src/app/modules/startup/internal/biometrics_state.nim +++ b/src/app/modules/startup/internal/biometrics_state.nim @@ -1,6 +1,3 @@ -import state -import ../controller - type BiometricsState* = ref object of State @@ -11,28 +8,36 @@ proc newBiometricsState*(flowType: FlowType, backState: State): BiometricsState proc delete*(self: BiometricsState) = self.State.delete -method moveToNextPrimaryState*(self: BiometricsState): bool = - return false - -method moveToNextSecondaryState*(self: BiometricsState): bool = - return false - method executePrimaryCommand*(self: BiometricsState, controller: Controller) = + let storeToKeychain = true # true, cause we have support for keychain for mac os if self.flowType == FlowType.FirstRunNewUserNewKeys: - controller.storeGeneratedAccountAndLogin(storeToKeychain = true) # true, cause we have support for keychain for mac os + controller.storeGeneratedAccountAndLogin(storeToKeychain) elif self.flowType == FlowType.FirstRunNewUserImportSeedPhrase: - controller.storeImportedAccountAndLogin(storeToKeychain = true) # true, cause we have support for keychain for mac os + controller.storeImportedAccountAndLogin(storeToKeychain) elif self.flowType == FlowType.FirstRunOldUserImportSeedPhrase: - ## This should not be the correct call for this flow, this is an issue, but since current implementation is like that - ## and this is not a bug fixing issue, left as it is. - controller.storeImportedAccountAndLogin(storeToKeychain = true) # true, cause we have support for keychain for mac os + ## This should not be the correct call for this flow, this is an issue, + ## but since current implementation is like that and this is not a bug fixing issue, left as it is. + controller.storeImportedAccountAndLogin(storeToKeychain) + elif self.flowType == FlowType.FirstRunNewUserNewKeycardKeys: + controller.storeKeycardAccountAndLogin(storeToKeychain) + elif self.flowType == FlowType.FirstRunNewUserImportSeedPhraseIntoKeycard: + controller.storeKeycardAccountAndLogin(storeToKeychain) + elif self.flowType == FlowType.FirstRunOldUserKeycardImport: + controller.setupKeycardAccount(storeToKeychain) method executeSecondaryCommand*(self: BiometricsState, controller: Controller) = + let storeToKeychain = false # false, cause we don't have keychain support for other than mac os if self.flowType == FlowType.FirstRunNewUserNewKeys: - controller.storeGeneratedAccountAndLogin(storeToKeychain = false) # false, cause we don't have keychain support for other than mac os + controller.storeGeneratedAccountAndLogin(storeToKeychain) elif self.flowType == FlowType.FirstRunNewUserImportSeedPhrase: - controller.storeImportedAccountAndLogin(storeToKeychain = false) # false, cause we don't have keychain support for other than mac os + controller.storeImportedAccountAndLogin(storeToKeychain) elif self.flowType == FlowType.FirstRunOldUserImportSeedPhrase: - ## This should not be the correct call for this flow, this is an issue, but since current implementation is like that - ## and this is not a bug fixing issue, left as it is. - controller.storeImportedAccountAndLogin(storeToKeychain = false) # false, cause we don't have keychain support for other than mac os \ No newline at end of file + ## This should not be the correct call for this flow, this is an issue, + ## but since current implementation is like that and this is not a bug fixing issue, left as it is. + controller.storeImportedAccountAndLogin(storeToKeychain) + elif self.flowType == FlowType.FirstRunNewUserNewKeycardKeys: + controller.storeKeycardAccountAndLogin(storeToKeychain) + elif self.flowType == FlowType.FirstRunNewUserImportSeedPhraseIntoKeycard: + controller.storeKeycardAccountAndLogin(storeToKeychain) + elif self.flowType == FlowType.FirstRunOldUserKeycardImport: + controller.setupKeycardAccount(storeToKeychain) \ No newline at end of file diff --git a/src/app/modules/startup/internal/keycard_create_pin_state.nim b/src/app/modules/startup/internal/keycard_create_pin_state.nim new file mode 100644 index 0000000000..c9fdc809b9 --- /dev/null +++ b/src/app/modules/startup/internal/keycard_create_pin_state.nim @@ -0,0 +1,23 @@ +type + KeycardCreatePinState* = ref object of State + pinValid: bool + +proc newKeycardCreatePinState*(flowType: FlowType, backState: State): KeycardCreatePinState = + result = KeycardCreatePinState() + result.setup(flowType, StateType.KeycardCreatePin, backState) + result.pinValid = false + +proc delete*(self: KeycardCreatePinState) = + self.State.delete + +method executeBackCommand*(self: KeycardCreatePinState, controller: Controller) = + controller.setPin("") + controller.setPinMatch(false) + +method getNextPrimaryState*(self: KeycardCreatePinState, controller: Controller): State = + if not self.pinValid: + return nil + return createState(StateType.KeycardRepeatPin, self.flowType, self.getBackState) + +method executePrimaryCommand*(self: KeycardCreatePinState, controller: Controller) = + self.pinValid = controller.getPin().len == PINLengthForStatusApp \ No newline at end of file diff --git a/src/app/modules/startup/internal/keycard_display_seed_phrase_state.nim b/src/app/modules/startup/internal/keycard_display_seed_phrase_state.nim new file mode 100644 index 0000000000..34bbd7daf1 --- /dev/null +++ b/src/app/modules/startup/internal/keycard_display_seed_phrase_state.nim @@ -0,0 +1,16 @@ +type + KeycardDisplaySeedPhraseState* = ref object of State + +proc newKeycardDisplaySeedPhraseState*(flowType: FlowType, backState: State): KeycardDisplaySeedPhraseState = + result = KeycardDisplaySeedPhraseState() + result.setup(flowType, StateType.KeycardDisplaySeedPhrase, backState) + +proc delete*(self: KeycardDisplaySeedPhraseState) = + self.State.delete + +method executeBackCommand*(self: KeycardDisplaySeedPhraseState, controller: Controller) = + controller.setPin("") + controller.setPinMatch(false) + +method getNextPrimaryState*(self: KeycardDisplaySeedPhraseState, controller: Controller): State = + return createState(StateType.KeycardEnterSeedPhraseWords, self.flowType, self.getBackState) \ No newline at end of file diff --git a/src/app/modules/startup/internal/keycard_empty_state.nim b/src/app/modules/startup/internal/keycard_empty_state.nim new file mode 100644 index 0000000000..6a3d72f8aa --- /dev/null +++ b/src/app/modules/startup/internal/keycard_empty_state.nim @@ -0,0 +1,24 @@ +type + KeycardEmptyState* = ref object of State + +proc newKeycardEmptyState*(flowType: FlowType, backState: State): KeycardEmptyState = + result = KeycardEmptyState() + result.setup(flowType, StateType.KeycardEmpty, backState) + +proc delete*(self: KeycardEmptyState) = + self.State.delete + +method executePrimaryCommand*(self: KeycardEmptyState, controller: Controller) = + if self.flowType == FlowType.FirstRunOldUserKeycardImport: + controller.runLoadAccountFlow(true) + +method resolveKeycardNextState*(self: KeycardEmptyState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + if self.flowType == FlowType.FirstRunOldUserKeycardImport: + if keycardFlowType == ResponseTypeValueKeycardFlowResult and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorConnection: + controller.resumeCurrentFlowLater() + return createState(StateType.KeycardPluginReader, FlowType.FirstRunNewUserNewKeycardKeys, self) + if keycardFlowType == ResponseTypeValueInsertCard: + return createState(StateType.KeycardInsertKeycard, FlowType.FirstRunNewUserNewKeycardKeys, self) diff --git a/src/app/modules/startup/internal/keycard_enter_pin_state.nim b/src/app/modules/startup/internal/keycard_enter_pin_state.nim new file mode 100644 index 0000000000..0d789ae8a8 --- /dev/null +++ b/src/app/modules/startup/internal/keycard_enter_pin_state.nim @@ -0,0 +1,45 @@ +type + KeycardEnterPinState* = ref object of State + pinValid: bool + +proc newKeycardEnterPinState*(flowType: FlowType, backState: State): KeycardEnterPinState = + result = KeycardEnterPinState() + result.setup(flowType, StateType.KeycardEnterPin, backState) + result.pinValid = false + +proc delete*(self: KeycardEnterPinState) = + self.State.delete + +method getNextPrimaryState*(self: KeycardEnterPinState, controller: Controller): State = + if self.flowType == FlowType.FirstRunNewUserNewKeycardKeys: + return createState(StateType.KeycardDisplaySeedPhrase, self.flowType, self.getBackState) + elif self.flowType == FirstRunNewUserImportSeedPhraseIntoKeycard: + return createState(StateType.UserProfileCreate, self.flowType, self.getBackState) + elif self.flowType == FlowType.FirstRunOldUserKeycardImport: + return nil + +method executeBackCommand*(self: KeycardEnterPinState, controller: Controller) = + controller.setPin("") + +method executePrimaryCommand*(self: KeycardEnterPinState, controller: Controller) = + if self.flowType == FlowType.FirstRunOldUserKeycardImport: + self.pinValid = controller.getPin().len == PINLengthForStatusApp + if self.pinValid: + controller.enterKeycardPin(controller.getPin()) + +method resolveKeycardNextState*(self: KeycardEnterPinState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + if self.flowType == FlowType.FirstRunOldUserKeycardImport: + if keycardFlowType == ResponseTypeValueEnterPIN and + keycardEvent.error.len > 0 and + keycardEvent.error == RequestParamPIN: + controller.setKeycardData($keycardEvent.pinRetries) + if keycardEvent.pinRetries > 0: + return createState(StateType.KeycardWrongPin, self.flowType, self.getBackState) + return createState(StateType.KeycardMaxPinRetriesReached, self.flowType, self.getBackState) + if keycardFlowType == ResponseTypeValueKeycardFlowResult: + controller.setKeycardEvent(keycardEvent) + if not defined(macosx): + controller.setupKeycardAccount(false) + return nil + return createState(StateType.Biometrics, self.flowType, self) \ No newline at end of file diff --git a/src/app/modules/startup/internal/keycard_enter_puk_state.nim b/src/app/modules/startup/internal/keycard_enter_puk_state.nim new file mode 100644 index 0000000000..b84118b712 --- /dev/null +++ b/src/app/modules/startup/internal/keycard_enter_puk_state.nim @@ -0,0 +1,44 @@ +type + KeycardEnterPukState* = ref object of State + +proc newKeycardEnterPukState*(flowType: FlowType, backState: State): KeycardEnterPukState = + result = KeycardEnterPukState() + result.setup(flowType, StateType.KeycardEnterPuk, backState) + +proc delete*(self: KeycardEnterPukState) = + self.State.delete + +method executePrimaryCommand*(self: KeycardEnterPukState, controller: Controller) = + if self.flowType == FlowType.FirstRunOldUserKeycardImport: + if controller.getPuk().len == PUKLengthForStatusApp: + controller.enterKeycardPuk(controller.getPuk()) + elif self.flowType == FlowType.AppLogin: + if controller.getPuk().len == PUKLengthForStatusApp: + controller.enterKeycardPuk(controller.getPuk()) + +method resolveKeycardNextState*(self: KeycardEnterPukState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + if self.flowType == FlowType.FirstRunOldUserKeycardImport: + if keycardFlowType == ResponseTypeValueEnterNewPIN and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorUnblocking: + return createState(StateType.KeycardCreatePin, self.flowType, self.getBackState) + if keycardFlowType == ResponseTypeValueEnterPUK and + keycardEvent.error.len > 0 and + keycardEvent.error == RequestParamPUK: + controller.setKeycardData($keycardEvent.pukRetries) + if keycardEvent.pukRetries > 0: + return createState(StateType.KeycardWrongPuk, self.flowType, self.getBackState) + return createState(StateType.KeycardMaxPukRetriesReached, self.flowType, self.getBackState) + if self.flowType == FlowType.AppLogin: + if keycardFlowType == ResponseTypeValueEnterNewPIN and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorUnblocking: + return createState(StateType.KeycardCreatePin, self.flowType, self.getBackState) + if keycardFlowType == ResponseTypeValueEnterPUK and + keycardEvent.error.len > 0 and + keycardEvent.error == RequestParamPUK: + controller.setKeycardData($keycardEvent.pukRetries) + if keycardEvent.pukRetries > 0: + return createState(StateType.KeycardWrongPuk, self.flowType, self.getBackState) + return createState(StateType.KeycardMaxPukRetriesReached, self.flowType, self.getBackState) \ No newline at end of file diff --git a/src/app/modules/startup/internal/keycard_enter_seed_phrase_words_state.nim b/src/app/modules/startup/internal/keycard_enter_seed_phrase_words_state.nim new file mode 100644 index 0000000000..72fed4df66 --- /dev/null +++ b/src/app/modules/startup/internal/keycard_enter_seed_phrase_words_state.nim @@ -0,0 +1,24 @@ +type + KeycardEnterSeedPhraseWordsState* = ref object of State + +proc newKeycardEnterSeedPhraseWordsState*(flowType: FlowType, backState: State): KeycardEnterSeedPhraseWordsState = + result = KeycardEnterSeedPhraseWordsState() + result.setup(flowType, StateType.KeycardEnterSeedPhraseWords, backState) + +proc delete*(self: KeycardEnterSeedPhraseWordsState) = + self.State.delete + +method executeBackCommand*(self: KeycardEnterSeedPhraseWordsState, controller: Controller) = + controller.setPin("") + controller.setPinMatch(false) + +method executePrimaryCommand*(self: KeycardEnterSeedPhraseWordsState, controller: Controller) = + controller.storeSeedPhraseToKeycard(controller.getSeedPhraseLength(), controller.getSeedPhrase()) + +method resolveKeycardNextState*(self: KeycardEnterSeedPhraseWordsState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + if self.flowType == FlowType.FirstRunNewUserNewKeycardKeys: + if keycardFlowType == ResponseTypeValueKeycardFlowResult and + keycardEvent.keyUid.len > 0: + controller.setKeyUid(keycardEvent.keyUid) + return createState(StateType.UserProfileCreate, self.flowType, self) \ No newline at end of file diff --git a/src/app/modules/startup/internal/keycard_insert_keycard_state.nim b/src/app/modules/startup/internal/keycard_insert_keycard_state.nim new file mode 100644 index 0000000000..8390afa564 --- /dev/null +++ b/src/app/modules/startup/internal/keycard_insert_keycard_state.nim @@ -0,0 +1,15 @@ +type + KeycardInsertKeycardState* = ref object of State + +proc newKeycardInsertKeycardState*(flowType: FlowType, backState: State): KeycardInsertKeycardState = + result = KeycardInsertKeycardState() + result.setup(flowType, StateType.KeycardInsertKeycard, backState) + +proc delete*(self: KeycardInsertKeycardState) = + self.State.delete + +method resolveKeycardNextState*(self: KeycardInsertKeycardState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + if keycardFlowType == ResponseTypeValueCardInserted: + return createState(StateType.KeycardReadingKeycard, self.flowType, self.getBackState) + return nil \ No newline at end of file diff --git a/src/app/modules/startup/internal/keycard_locked_state.nim b/src/app/modules/startup/internal/keycard_locked_state.nim new file mode 100644 index 0000000000..e21795aa57 --- /dev/null +++ b/src/app/modules/startup/internal/keycard_locked_state.nim @@ -0,0 +1,12 @@ +type + KeycardLockedState* = ref object of State + +proc newKeycardLockedState*(flowType: FlowType, backState: State): KeycardLockedState = + result = KeycardLockedState() + result.setup(flowType, StateType.KeycardLocked, backState) + +proc delete*(self: KeycardLockedState) = + self.State.delete + +method getNextPrimaryState*(self: KeycardLockedState, controller: Controller): State = + return createState(StateType.KeycardEnterSeedPhraseWords, self.flowType, self) \ No newline at end of file diff --git a/src/app/modules/startup/internal/keycard_max_pairing_slots_reached_state.nim b/src/app/modules/startup/internal/keycard_max_pairing_slots_reached_state.nim new file mode 100644 index 0000000000..c08e16b45a --- /dev/null +++ b/src/app/modules/startup/internal/keycard_max_pairing_slots_reached_state.nim @@ -0,0 +1,28 @@ +type + KeycardMaxPairingSlotsReachedState* = ref object of State + +proc newKeycardMaxPairingSlotsReachedState*(flowType: FlowType, backState: State): KeycardMaxPairingSlotsReachedState = + result = KeycardMaxPairingSlotsReachedState() + result.setup(flowType, StateType.KeycardMaxPairingSlotsReached, backState) + +proc delete*(self: KeycardMaxPairingSlotsReachedState) = + self.State.delete + +method executePrimaryCommand*(self: KeycardMaxPairingSlotsReachedState, controller: Controller) = + if self.flowType == FlowType.FirstRunOldUserKeycardImport: + controller.runLoadAccountFlow(true) + +method executeSecondaryCommand*(self: KeycardMaxPairingSlotsReachedState, controller: Controller) = + if self.flowType == FlowType.FirstRunOldUserKeycardImport: + controller.resumeCurrentFlow() + +method resolveKeycardNextState*(self: KeycardMaxPairingSlotsReachedState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + if self.flowType == FlowType.FirstRunOldUserKeycardImport: + if keycardFlowType == ResponseTypeValueKeycardFlowResult and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorConnection: + controller.resumeCurrentFlowLater() + return createState(StateType.KeycardPluginReader, FlowType.FirstRunNewUserImportSeedPhraseIntoKeycard, self.getBackState) + if keycardFlowType == ResponseTypeValueInsertCard: + return createState(StateType.KeycardInsertKeycard, FlowType.FirstRunNewUserImportSeedPhraseIntoKeycard, self.getBackState) \ No newline at end of file diff --git a/src/app/modules/startup/internal/keycard_max_pin_retries_reached_state.nim b/src/app/modules/startup/internal/keycard_max_pin_retries_reached_state.nim new file mode 100644 index 0000000000..d4a22ec37a --- /dev/null +++ b/src/app/modules/startup/internal/keycard_max_pin_retries_reached_state.nim @@ -0,0 +1,12 @@ +type + KeycardMaxPinRetriesReachedState* = ref object of State + +proc newKeycardMaxPinRetriesReachedState*(flowType: FlowType, backState: State): KeycardMaxPinRetriesReachedState = + result = KeycardMaxPinRetriesReachedState() + result.setup(flowType, StateType.KeycardMaxPinRetriesReached, backState) + +proc delete*(self: KeycardMaxPinRetriesReachedState) = + self.State.delete + +method getNextPrimaryState*(self: KeycardMaxPinRetriesReachedState, controller: Controller): State = + return createState(StateType.KeycardRecover, self.flowType(), self.getBackState) \ No newline at end of file diff --git a/src/app/modules/startup/internal/keycard_max_puk_retries_reached_state.nim b/src/app/modules/startup/internal/keycard_max_puk_retries_reached_state.nim new file mode 100644 index 0000000000..01f0ae4981 --- /dev/null +++ b/src/app/modules/startup/internal/keycard_max_puk_retries_reached_state.nim @@ -0,0 +1,40 @@ +type + KeycardMaxPukRetriesReachedState* = ref object of State + +proc newKeycardMaxPukRetriesReachedState*(flowType: FlowType, backState: State): KeycardMaxPukRetriesReachedState = + result = KeycardMaxPukRetriesReachedState() + result.setup(flowType, StateType.KeycardMaxPukRetriesReached, backState) + +proc delete*(self: KeycardMaxPukRetriesReachedState) = + self.State.delete + +method executePrimaryCommand*(self: KeycardMaxPukRetriesReachedState, controller: Controller) = + if self.flowType == FlowType.FirstRunOldUserKeycardImport: + controller.runLoadAccountFlow(true) + elif self.flowType == FlowType.AppLogin: + controller.runLoadAccountFlow(true) + +method executeSecondaryCommand*(self: KeycardMaxPukRetriesReachedState, controller: Controller) = + if self.flowType == FlowType.FirstRunOldUserKeycardImport: + controller.resumeCurrentFlow() + elif self.flowType == FlowType.AppLogin: + controller.resumeCurrentFlow() + +method resolveKeycardNextState*(self: KeycardMaxPukRetriesReachedState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + if self.flowType == FlowType.FirstRunOldUserKeycardImport: + if keycardFlowType == ResponseTypeValueKeycardFlowResult and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorConnection: + controller.resumeCurrentFlowLater() + return createState(StateType.KeycardPluginReader, FlowType.FirstRunNewUserImportSeedPhraseIntoKeycard, self.getBackState) + if keycardFlowType == ResponseTypeValueInsertCard: + return createState(StateType.KeycardInsertKeycard, FlowType.FirstRunNewUserImportSeedPhraseIntoKeycard, self.getBackState) + if self.flowType == FlowType.AppLogin: + if keycardFlowType == ResponseTypeValueKeycardFlowResult and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorConnection: + controller.resumeCurrentFlowLater() + return createState(StateType.KeycardPluginReader, self.flowType, self.getBackState) + if keycardFlowType == ResponseTypeValueInsertCard: + return createState(StateType.KeycardInsertKeycard, self.flowType, self.getBackState) \ No newline at end of file diff --git a/src/app/modules/startup/internal/keycard_not_empty_state.nim b/src/app/modules/startup/internal/keycard_not_empty_state.nim new file mode 100644 index 0000000000..a70c06b79d --- /dev/null +++ b/src/app/modules/startup/internal/keycard_not_empty_state.nim @@ -0,0 +1,31 @@ +type + KeycardNotEmptyState* = ref object of State + +proc newKeycardNotEmptyState*(flowType: FlowType, backState: State): KeycardNotEmptyState = + result = KeycardNotEmptyState() + result.setup(flowType, StateType.KeycardNotEmpty, backState) + +proc delete*(self: KeycardNotEmptyState) = + self.State.delete + +method executePrimaryCommand*(self: KeycardNotEmptyState, controller: Controller) = + if self.flowType == FlowType.FirstRunNewUserNewKeycardKeys or + self.flowType == FlowType.FirstRunNewUserImportSeedPhraseIntoKeycard: + controller.runLoadAccountFlow(true) + +method executeSecondaryCommand*(self: KeycardNotEmptyState, controller: Controller) = + if self.flowType == FlowType.FirstRunNewUserNewKeycardKeys: + controller.resumeCurrentFlow() + +method resolveKeycardNextState*(self: KeycardNotEmptyState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + if self.flowType == FlowType.FirstRunNewUserNewKeycardKeys: + if keycardFlowType == ResponseTypeValueEnterNewPIN and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorRequireInit: + return createState(StateType.KeycardCreatePin, self.flowType, self.getBackState) + if self.flowType == FlowType.FirstRunNewUserImportSeedPhraseIntoKeycard: + if keycardFlowType == ResponseTypeValueEnterNewPIN and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorRequireInit: + return createState(StateType.UserProfileEnterSeedPhrase, self.flowType, self.getBackState) \ No newline at end of file diff --git a/src/app/modules/startup/internal/keycard_pin_set_state.nim b/src/app/modules/startup/internal/keycard_pin_set_state.nim new file mode 100644 index 0000000000..3c408c42af --- /dev/null +++ b/src/app/modules/startup/internal/keycard_pin_set_state.nim @@ -0,0 +1,40 @@ +type + KeycardPinSetState* = ref object of State + +proc newKeycardPinSetState*(flowType: FlowType, backState: State): KeycardPinSetState = + result = KeycardPinSetState() + result.setup(flowType, StateType.KeycardPinSet, backState) + +proc delete*(self: KeycardPinSetState) = + self.State.delete + +method executeBackCommand*(self: KeycardPinSetState, controller: Controller) = + controller.setPin("") + controller.setPinMatch(false) + +method getNextPrimaryState*(self: KeycardPinSetState, controller: Controller): State = + if self.flowType == FlowType.FirstRunNewUserNewKeycardKeys: + return createState(StateType.KeycardDisplaySeedPhrase, self.flowType, self.getBackState) + if self.flowType == FirstRunNewUserImportSeedPhraseIntoKeycard: + return createState(StateType.UserProfileCreate, self.flowType, self.getBackState) + if self.flowType == FlowType.FirstRunOldUserKeycardImport: + if controller.getValidPuk(): + if not defined(macosx): + return nil + return createState(StateType.Biometrics, self.flowType, self.getBackState) + return createState(StateType.KeycardWrongPuk, self.flowType, self.getBackState) + if self.flowType == FlowType.AppLogin: + if controller.getRecoverUsingSeedPhraseWhileLogin(): + return createState(StateType.LoginKeycardReadingKeycard, self.flowType, nil) + if controller.getValidPuk(): + controller.loginAccountKeycard() + return nil + return createState(StateType.KeycardWrongPuk, self.flowType, self.getBackState) + +method executePrimaryCommand*(self: KeycardPinSetState, controller: Controller) = + if controller.getValidPuk() and not defined(macosx): + controller.setupKeycardAccount(false) + return + if self.flowType == FlowType.AppLogin: + if controller.getRecoverUsingSeedPhraseWhileLogin(): + controller.startLoginFlowAutomatically(controller.getPin()) \ No newline at end of file diff --git a/src/app/modules/startup/internal/keycard_plugin_reader_state.nim b/src/app/modules/startup/internal/keycard_plugin_reader_state.nim new file mode 100644 index 0000000000..28c9e78d74 --- /dev/null +++ b/src/app/modules/startup/internal/keycard_plugin_reader_state.nim @@ -0,0 +1,27 @@ +type + KeycardPluginReaderState* = ref object of State + +proc newKeycardPluginReaderState*(flowType: FlowType, backState: State): KeycardPluginReaderState = + result = KeycardPluginReaderState() + result.setup(flowType, StateType.KeycardPluginReader, backState) + +proc delete*(self: KeycardPluginReaderState) = + self.State.delete + +method executePrimaryCommand*(self: KeycardPluginReaderState, controller: Controller) = + if self.flowType == FlowType.FirstRunNewUserNewKeycardKeys: + controller.runLoadAccountFlow() + elif self.flowType == FlowType.FirstRunNewUserImportSeedPhraseIntoKeycard: + controller.runLoadAccountFlow() + elif self.flowType == FlowType.FirstRunOldUserKeycardImport: + controller.runRecoverAccountFlow() + +method resolveKeycardNextState*(self: KeycardPluginReaderState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + if keycardFlowType == ResponseTypeValueKeycardFlowResult and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorConnection: + controller.resumeCurrentFlowLater() + return nil + if keycardFlowType == ResponseTypeValueInsertCard: + return createState(StateType.KeycardInsertKeycard, self.flowType, self.getBackState) \ No newline at end of file diff --git a/src/app/modules/startup/internal/keycard_reading_keycard_state.nim b/src/app/modules/startup/internal/keycard_reading_keycard_state.nim new file mode 100644 index 0000000000..946c419a1b --- /dev/null +++ b/src/app/modules/startup/internal/keycard_reading_keycard_state.nim @@ -0,0 +1,72 @@ +type + KeycardReadingKeycardState* = ref object of State + +proc newKeycardReadingKeycardState*(flowType: FlowType, backState: State): KeycardReadingKeycardState = + result = KeycardReadingKeycardState() + result.setup(flowType, StateType.KeycardReadingKeycard, backState) + +proc delete*(self: KeycardReadingKeycardState) = + self.State.delete + +method resolveKeycardNextState*(self: KeycardReadingKeycardState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + if self.flowType == FlowType.FirstRunNewUserNewKeycardKeys: + if keycardFlowType == ResponseTypeValueEnterNewPIN and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorRequireInit: + return createState(StateType.KeycardCreatePin, self.flowType, self.getBackState) + if keycardFlowType == ResponseTypeValueEnterPIN and + keycardEvent.error.len == 0: + return createState(StateType.KeycardNotEmpty, self.flowType, self.getBackState) + if keycardFlowType == ResponseTypeValueSwapCard and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorHasKeys: + return createState(StateType.KeycardNotEmpty, self.flowType, self.getBackState) + + if self.flowType == FlowType.FirstRunNewUserImportSeedPhraseIntoKeycard: + if keycardFlowType == ResponseTypeValueEnterNewPIN and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorRequireInit: + return createState(StateType.UserProfileEnterSeedPhrase, self.flowType, self.getBackState) + if keycardFlowType == ResponseTypeValueEnterPIN and + keycardEvent.error.len == 0: + return createState(StateType.KeycardNotEmpty, self.flowType, self.getBackState) + if keycardFlowType == ResponseTypeValueSwapCard and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorHasKeys: + return createState(StateType.KeycardNotEmpty, self.flowType, self.getBackState) + + if self.flowType == FlowType.FirstRunOldUserKeycardImport: + if keycardFlowType == ResponseTypeValueEnterPIN and + keycardEvent.error.len == 0: + return createState(StateType.KeycardEnterPin, self.flowType, self.getBackState) + if keycardFlowType == ResponseTypeValueEnterPUK and + keycardEvent.error.len == 0: + if keycardEvent.pinRetries == 0 and keycardEvent.pukRetries > 0: + return createState(StateType.KeycardMaxPinRetriesReached, self.flowType, self.getBackState) + if keycardFlowType == ResponseTypeValueSwapCard and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorNoKeys: + return createState(StateType.KeycardEmpty, self.flowType, self.getBackState) + if keycardFlowType == ResponseTypeValueSwapCard and + keycardEvent.error.len > 0 and + keycardEvent.error == RequestParamPUKRetries: + return createState(StateType.KeycardMaxPukRetriesReached, self.flowType, self.getBackState) + if keycardFlowType == ResponseTypeValueSwapCard and + keycardEvent.error.len > 0 and + keycardEvent.error == RequestParamFreeSlots: + return createState(StateType.KeycardMaxPairingSlotsReached, self.flowType, self.getBackState) + if keycardFlowType == ResponseTypeValueEnterNewPIN and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorRequireInit: + return createState(StateType.KeycardCreatePin, self.flowType, self.getBackState) + + if self.flowType == FlowType.AppLogin: + if keycardFlowType == ResponseTypeValueSwapCard and + keycardEvent.error.len > 0 and + keycardEvent.error == RequestParamPUKRetries: + return createState(StateType.KeycardMaxPukRetriesReached, self.flowType, self.getBackState) + if keycardFlowType == ResponseTypeValueEnterNewPIN and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorRequireInit: + return createState(StateType.KeycardCreatePin, self.flowType, self.getBackState) \ No newline at end of file diff --git a/src/app/modules/startup/internal/keycard_recover_state.nim b/src/app/modules/startup/internal/keycard_recover_state.nim new file mode 100644 index 0000000000..db0bf8c561 --- /dev/null +++ b/src/app/modules/startup/internal/keycard_recover_state.nim @@ -0,0 +1,23 @@ +type + KeycardRecoverState* = ref object of State + +proc newKeycardRecoverState*(flowType: FlowType, backState: State): KeycardRecoverState = + result = KeycardRecoverState() + result.setup(flowType, StateType.KeycardRecover, backState) + +proc delete*(self: KeycardRecoverState) = + self.State.delete + +method getNextPrimaryState*(self: KeycardRecoverState, controller: Controller): State = + if self.flowType == FlowType.FirstRunOldUserKeycardImport: + return createState(StateType.UserProfileEnterSeedPhrase, self.flowType, self) + if self.flowType == FlowType.AppLogin: + controller.setRecoverUsingSeedPhraseWhileLogin(true) + return createState(StateType.UserProfileEnterSeedPhrase, self.flowType, self) + +method getNextSecondaryState*(self: KeycardRecoverState, controller: Controller): State = + if self.flowType == FlowType.FirstRunOldUserKeycardImport: + return createState(StateType.KeycardEnterPuk, self.flowType, self) + if self.flowType == FlowType.AppLogin: + controller.setRecoverUsingSeedPhraseWhileLogin(false) + return createState(StateType.KeycardEnterPuk, self.flowType, self) diff --git a/src/app/modules/startup/internal/keycard_repeat_pin_state.nim b/src/app/modules/startup/internal/keycard_repeat_pin_state.nim new file mode 100644 index 0000000000..b8a3f173d5 --- /dev/null +++ b/src/app/modules/startup/internal/keycard_repeat_pin_state.nim @@ -0,0 +1,65 @@ +type + KeycardRepeatPinState* = ref object of State + +proc newKeycardRepeatPinState*(flowType: FlowType, backState: State): KeycardRepeatPinState = + result = KeycardRepeatPinState() + result.setup(flowType, StateType.KeycardRepeatPin, backState) + +proc delete*(self: KeycardRepeatPinState) = + self.State.delete + +method executeBackCommand*(self: KeycardRepeatPinState, controller: Controller) = + controller.setPin("") + controller.setPinMatch(false) + +method executePrimaryCommand*(self: KeycardRepeatPinState, controller: Controller) = + if not controller.getPinMatch(): + return + if self.flowType == FlowType.FirstRunNewUserNewKeycardKeys: + controller.storePinToKeycard(controller.getPin(), controller.generateRandomPUK()) + elif self.flowType == FlowType.FirstRunNewUserImportSeedPhraseIntoKeycard: + controller.storePinToKeycard(controller.getPin(), controller.generateRandomPUK()) + elif self.flowType == FlowType.FirstRunOldUserKeycardImport: + controller.storePinToKeycard(controller.getPin(), puk = "") + elif self.flowType == FlowType.AppLogin: + controller.storePinToKeycard(controller.getPin(), puk = "") + +method resolveKeycardNextState*(self: KeycardRepeatPinState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + if self.flowType == FlowType.FirstRunNewUserNewKeycardKeys: + if keycardFlowType == ResponseTypeValueEnterMnemonic and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorLoadingKeys: + controller.buildSeedPhrasesFromIndexes(keycardEvent.seedPhraseIndexes) + return createState(StateType.KeycardPinSet, self.flowType, self.getBackState) + if self.flowType == FlowType.FirstRunNewUserImportSeedPhraseIntoKeycard: + if keycardFlowType == ResponseTypeValueKeycardFlowResult and + keycardEvent.keyUid.len > 0: + controller.setKeyUid(keycardEvent.keyUid) + return createState(StateType.KeycardPinSet, self.flowType, self.getBackState) + if self.flowType == FlowType.FirstRunOldUserKeycardImport: + if keycardFlowType == ResponseTypeValueEnterPUK and + keycardEvent.error.len > 0 and + keycardEvent.error == RequestParamPUK: + controller.setKeycardData($keycardEvent.pukRetries) + controller.setPukValid(false) + if keycardEvent.pukRetries > 0: + return createState(StateType.KeycardPinSet, self.flowType, self.getBackState) + return createState(StateType.KeycardMaxPukRetriesReached, self.flowType, self.getBackState) + if keycardFlowType == ResponseTypeValueKeycardFlowResult: + controller.setKeycardEvent(keycardEvent) + controller.setPukValid(true) + return createState(StateType.KeycardPinSet, self.flowType, self.getBackState) + if self.flowType == FlowType.AppLogin: + if keycardFlowType == ResponseTypeValueEnterPUK and + keycardEvent.error.len > 0 and + keycardEvent.error == RequestParamPUK: + controller.setKeycardData($keycardEvent.pukRetries) + controller.setPukValid(false) + if keycardEvent.pukRetries > 0: + return createState(StateType.KeycardPinSet, self.flowType, self.getBackState) + return createState(StateType.KeycardMaxPukRetriesReached, self.flowType, self.getBackState) + if keycardFlowType == ResponseTypeValueKeycardFlowResult: + controller.setKeycardEvent(keycardEvent) + controller.setPukValid(true) + return createState(StateType.KeycardPinSet, self.flowType, self.getBackState) \ No newline at end of file diff --git a/src/app/modules/startup/internal/keycard_wrong_pin_state.nim b/src/app/modules/startup/internal/keycard_wrong_pin_state.nim new file mode 100644 index 0000000000..b2398bbf78 --- /dev/null +++ b/src/app/modules/startup/internal/keycard_wrong_pin_state.nim @@ -0,0 +1,41 @@ +type + KeycardWrongPinState* = ref object of State + pinValid: bool + +proc newKeycardWrongPinState*(flowType: FlowType, backState: State): KeycardWrongPinState = + result = KeycardWrongPinState() + result.setup(flowType, StateType.KeycardWrongPin, backState) + result.pinValid = false + +proc delete*(self: KeycardWrongPinState) = + self.State.delete + +method executeBackCommand*(self: KeycardWrongPinState, controller: Controller) = + controller.setPin("") + +method executePrimaryCommand*(self: KeycardWrongPinState, controller: Controller) = + self.pinValid = controller.getPin().len == PINLengthForStatusApp + if self.pinValid: + controller.enterKeycardPin(controller.getPin()) + +method resolveKeycardNextState*(self: KeycardWrongPinState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + if self.flowType == FlowType.FirstRunOldUserKeycardImport: + if keycardFlowType == ResponseTypeValueEnterPIN and + keycardEvent.error.len > 0 and + keycardEvent.error == RequestParamPIN: + controller.setKeycardData($keycardEvent.pinRetries) + if keycardEvent.pinRetries > 0: + return self + return createState(StateType.KeycardMaxPinRetriesReached, self.flowType, self.getBackState) + if keycardFlowType == ResponseTypeValueEnterPUK and + keycardEvent.error.len == 0: + if keycardEvent.pinRetries == 0 and keycardEvent.pukRetries > 0: + return createState(StateType.KeycardMaxPinRetriesReached, self.flowType, self.getBackState) + return nil + if keycardFlowType == ResponseTypeValueKeycardFlowResult: + controller.setKeycardEvent(keycardEvent) + if not defined(macosx): + controller.setupKeycardAccount(false) + return nil + return createState(StateType.Biometrics, self.flowType, self) \ No newline at end of file diff --git a/src/app/modules/startup/internal/keycard_wrong_puk_state.nim b/src/app/modules/startup/internal/keycard_wrong_puk_state.nim new file mode 100644 index 0000000000..121d52f2aa --- /dev/null +++ b/src/app/modules/startup/internal/keycard_wrong_puk_state.nim @@ -0,0 +1,58 @@ +type + KeycardWrongPukState* = ref object of State + +proc newKeycardWrongPukState*(flowType: FlowType, backState: State): KeycardWrongPukState = + result = KeycardWrongPukState() + result.setup(flowType, StateType.KeycardWrongPuk, backState) + +proc delete*(self: KeycardWrongPukState) = + self.State.delete + +method executePrimaryCommand*(self: KeycardWrongPukState, controller: Controller) = + if self.flowType == FlowType.FirstRunOldUserKeycardImport: + if controller.getPuk().len == PUKLengthForStatusApp: + controller.enterKeycardPuk(controller.getPuk()) + elif self.flowType == FlowType.AppLogin: + if controller.getPuk().len == PUKLengthForStatusApp: + controller.enterKeycardPuk(controller.getPuk()) + +method resolveKeycardNextState*(self: KeycardWrongPukState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + if self.flowType == FlowType.FirstRunOldUserKeycardImport: + if keycardFlowType == ResponseTypeValueEnterPUK and + keycardEvent.error.len > 0 and + keycardEvent.error == RequestParamPUK: + controller.setKeycardData($keycardEvent.pukRetries) + controller.setPukValid(false) + if keycardEvent.pukRetries > 0: + return nil + return createState(StateType.KeycardMaxPukRetriesReached, self.flowType, self.getBackState) + if keycardFlowType == ResponseTypeValueSwapCard and + keycardEvent.error.len > 0 and + keycardEvent.error == RequestParamPUKRetries: + return createState(StateType.KeycardMaxPukRetriesReached, self.flowType, self.getBackState) + if keycardFlowType == ResponseTypeValueKeycardFlowResult: + controller.setKeycardEvent(keycardEvent) + controller.setPukValid(true) + if not defined(macosx): + controller.setupKeycardAccount(false) + return nil + return createState(StateType.Biometrics, self.flowType, self.getBackState) + if self.flowType == FlowType.AppLogin: + if keycardFlowType == ResponseTypeValueEnterPUK and + keycardEvent.error.len > 0 and + keycardEvent.error == RequestParamPUK: + controller.setKeycardData($keycardEvent.pukRetries) + controller.setPukValid(false) + if keycardEvent.pukRetries > 0: + return nil + return createState(StateType.KeycardMaxPukRetriesReached, self.flowType, self.getBackState) + if keycardFlowType == ResponseTypeValueSwapCard and + keycardEvent.error.len > 0 and + keycardEvent.error == RequestParamPUKRetries: + return createState(StateType.LoginKeycardMaxPukRetriesReached, self.flowType, self.getBackState) + if keycardFlowType == ResponseTypeValueKeycardFlowResult: + controller.setKeycardEvent(keycardEvent) + controller.setPukValid(true) + controller.loginAccountKeycard() + return nil \ No newline at end of file diff --git a/src/app/modules/startup/internal/login_keycard_empty_state.nim b/src/app/modules/startup/internal/login_keycard_empty_state.nim new file mode 100644 index 0000000000..dc11f7908a --- /dev/null +++ b/src/app/modules/startup/internal/login_keycard_empty_state.nim @@ -0,0 +1,30 @@ +type + LoginKeycardEmptyState* = ref object of State + +proc newLoginKeycardEmptyState*(flowType: FlowType, backState: State): LoginKeycardEmptyState = + result = LoginKeycardEmptyState() + result.setup(flowType, StateType.LoginKeycardEmpty, backState) + +proc delete*(self: LoginKeycardEmptyState) = + self.State.delete + +method executePrimaryCommand*(self: LoginKeycardEmptyState, controller: Controller) = + if self.flowType == FlowType.AppLogin: + controller.runLoadAccountFlow(true) + +method getNextSecondaryState*(self: LoginKeycardEmptyState, controller: Controller): State = + return createState(StateType.WelcomeNewStatusUser, self.flowType, self) + +method getNextTertiaryState*(self: LoginKeycardEmptyState, controller: Controller): State = + return createState(StateType.WelcomeOldStatusUser, self.flowType, self) + +method resolveKeycardNextState*(self: LoginKeycardEmptyState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + if self.flowType == FlowType.AppLogin: + if keycardFlowType == ResponseTypeValueKeycardFlowResult and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorConnection: + controller.resumeCurrentFlowLater() + return createState(StateType.KeycardPluginReader, FlowType.FirstRunNewUserNewKeycardKeys, self) + if keycardFlowType == ResponseTypeValueInsertCard: + return createState(StateType.KeycardInsertKeycard, FlowType.FirstRunNewUserNewKeycardKeys, self) diff --git a/src/app/modules/startup/internal/login_keycard_enter_pin_state.nim b/src/app/modules/startup/internal/login_keycard_enter_pin_state.nim new file mode 100644 index 0000000000..7fa44dc58a --- /dev/null +++ b/src/app/modules/startup/internal/login_keycard_enter_pin_state.nim @@ -0,0 +1,40 @@ +type + LoginKeycardEnterPinState* = ref object of State + +proc newLoginKeycardEnterPinState*(flowType: FlowType, backState: State): LoginKeycardEnterPinState = + result = LoginKeycardEnterPinState() + result.setup(flowType, StateType.LoginKeycardEnterPin, backState) + +proc delete*(self: LoginKeycardEnterPinState) = + self.State.delete + +method executePrimaryCommand*(self: LoginKeycardEnterPinState, controller: Controller) = + if self.flowType == FlowType.AppLogin: + if controller.getPin().len == PINLengthForStatusApp: + controller.enterKeycardPin(controller.getPin()) + +method getNextSecondaryState*(self: LoginKeycardEnterPinState, controller: Controller): State = + return createState(StateType.WelcomeNewStatusUser, self.flowType, self) + +method getNextTertiaryState*(self: LoginKeycardEnterPinState, controller: Controller): State = + return createState(StateType.WelcomeOldStatusUser, self.flowType, self) + +method resolveKeycardNextState*(self: LoginKeycardEnterPinState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + if self.flowType == FlowType.AppLogin: + if keycardFlowType == ResponseTypeValueKeycardFlowResult and + keycardEvent.error.len == 0: + controller.setKeycardEvent(keycardEvent) + controller.loginAccountKeycard() + return + if keycardFlowType == ResponseTypeValueEnterPIN and + keycardEvent.error.len > 0 and + keycardEvent.error == RequestParamPIN: + controller.setKeycardData($keycardEvent.pinRetries) + if keycardEvent.pinRetries > 0: + return createState(StateType.LoginKeycardWrongPin, self.flowType, nil) + return createState(StateType.LoginKeycardMaxPinRetriesReached, self.flowType, nil) + if keycardFlowType == ResponseTypeValueEnterPUK and + keycardEvent.error.len == 0: + if keycardEvent.pinRetries == 0 and keycardEvent.pukRetries > 0: + return createState(StateType.LoginKeycardMaxPinRetriesReached, self.flowType, nil) \ No newline at end of file diff --git a/src/app/modules/startup/internal/login_keycard_insert_keycard_state.nim b/src/app/modules/startup/internal/login_keycard_insert_keycard_state.nim new file mode 100644 index 0000000000..a587d18f1f --- /dev/null +++ b/src/app/modules/startup/internal/login_keycard_insert_keycard_state.nim @@ -0,0 +1,21 @@ +type + LoginKeycardInsertKeycardState* = ref object of State + +proc newLoginKeycardInsertKeycardState*(flowType: FlowType, backState: State): LoginKeycardInsertKeycardState = + result = LoginKeycardInsertKeycardState() + result.setup(flowType, StateType.LoginKeycardInsertKeycard, backState) + +proc delete*(self: LoginKeycardInsertKeycardState) = + self.State.delete + +method getNextSecondaryState*(self: LoginKeycardInsertKeycardState, controller: Controller): State = + return createState(StateType.WelcomeNewStatusUser, self.flowType, self) + +method getNextTertiaryState*(self: LoginKeycardInsertKeycardState, controller: Controller): State = + return createState(StateType.WelcomeOldStatusUser, self.flowType, self) + +method resolveKeycardNextState*(self: LoginKeycardInsertKeycardState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + if keycardFlowType == ResponseTypeValueCardInserted: + return createState(StateType.LoginKeycardReadingKeycard, self.flowType, nil) + return nil \ No newline at end of file diff --git a/src/app/modules/startup/internal/login_keycard_max_pin_retries_reached_state.nim b/src/app/modules/startup/internal/login_keycard_max_pin_retries_reached_state.nim new file mode 100644 index 0000000000..ba24c19de8 --- /dev/null +++ b/src/app/modules/startup/internal/login_keycard_max_pin_retries_reached_state.nim @@ -0,0 +1,22 @@ +type + LoginKeycardMaxPinRetriesReachedState* = ref object of State + +proc newLoginKeycardMaxPinRetriesReachedState*(flowType: FlowType, backState: State): LoginKeycardMaxPinRetriesReachedState = + result = LoginKeycardMaxPinRetriesReachedState() + result.setup(flowType, StateType.LoginKeycardMaxPinRetriesReached, backState) + +proc delete*(self: LoginKeycardMaxPinRetriesReachedState) = + self.State.delete + +method executeBackCommand*(self: LoginKeycardMaxPinRetriesReachedState, controller: Controller) = + if self.flowType == FlowType.AppLogin and controller.isKeycardCreatedAccountSelectedOne(): + controller.runLoginFlow() + +method getNextPrimaryState*(self: LoginKeycardMaxPinRetriesReachedState, controller: Controller): State = + return createState(StateType.KeycardRecover, self.flowType, self) + +method getNextSecondaryState*(self: LoginKeycardMaxPinRetriesReachedState, controller: Controller): State = + return createState(StateType.WelcomeNewStatusUser, self.flowType, self) + +method getNextTertiaryState*(self: LoginKeycardMaxPinRetriesReachedState, controller: Controller): State = + return createState(StateType.WelcomeOldStatusUser, self.flowType, self) \ No newline at end of file diff --git a/src/app/modules/startup/internal/login_keycard_max_puk_retries_reached_state.nim b/src/app/modules/startup/internal/login_keycard_max_puk_retries_reached_state.nim new file mode 100644 index 0000000000..d109d923ad --- /dev/null +++ b/src/app/modules/startup/internal/login_keycard_max_puk_retries_reached_state.nim @@ -0,0 +1,12 @@ +type + LoginKeycardMaxPukRetriesReachedState* = ref object of State + +proc newLoginKeycardMaxPukRetriesReachedState*(flowType: FlowType, backState: State): LoginKeycardMaxPukRetriesReachedState = + result = LoginKeycardMaxPukRetriesReachedState() + result.setup(flowType, StateType.LoginKeycardMaxPukRetriesReached, backState) + +proc delete*(self: LoginKeycardMaxPukRetriesReachedState) = + self.State.delete + +method getNextPrimaryState*(self: LoginKeycardMaxPukRetriesReachedState, controller: Controller): State = + return createState(StateType.UserProfileEnterSeedPhrase, self.flowType, nil) \ No newline at end of file diff --git a/src/app/modules/startup/internal/login_keycard_reading_keycard_state.nim b/src/app/modules/startup/internal/login_keycard_reading_keycard_state.nim new file mode 100644 index 0000000000..eb4a24f20d --- /dev/null +++ b/src/app/modules/startup/internal/login_keycard_reading_keycard_state.nim @@ -0,0 +1,55 @@ +import ../../../global/global_singleton + +type + LoginKeycardReadingKeycardState* = ref object of State + +proc newLoginKeycardReadingKeycardState*(flowType: FlowType, backState: State): LoginKeycardReadingKeycardState = + result = LoginKeycardReadingKeycardState() + result.setup(flowType, StateType.LoginKeycardReadingKeycard, backState) + +proc delete*(self: LoginKeycardReadingKeycardState) = + self.State.delete + +method executePrimaryCommand*(self: LoginKeycardReadingKeycardState, controller: Controller) = + if not controller.keychainErrorOccurred(): + controller.enterKeycardPin(controller.getPin()) + +method getNextPrimaryState*(self: LoginKeycardReadingKeycardState, controller: Controller): State = + if controller.keychainErrorOccurred(): + return createState(StateType.LoginKeycardEnterPin, self.flowType, nil) + +method getNextSecondaryState*(self: LoginKeycardReadingKeycardState, controller: Controller): State = + return createState(StateType.WelcomeNewStatusUser, self.flowType, self) + +method getNextTertiaryState*(self: LoginKeycardReadingKeycardState, controller: Controller): State = + return createState(StateType.WelcomeOldStatusUser, self.flowType, self) + +method resolveKeycardNextState*(self: LoginKeycardReadingKeycardState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + if self.flowType == FlowType.AppLogin: + if keycardFlowType == ResponseTypeValueKeycardFlowResult and + keycardEvent.error.len == 0: + controller.setKeycardEvent(keycardEvent) + controller.loginAccountKeycard() + return nil + if keycardFlowType == ResponseTypeValueEnterPIN and + keycardEvent.error.len == 0: + if not controller.keyUidMatch(keycardEvent.keyUid): + return createState(StateType.LoginKeycardWrongKeycard, self.flowType, nil) + let value = singletonInstance.localAccountSettings.getStoreToKeychainValue() + if value == LS_VALUE_STORE: + controller.tryToObtainDataFromKeychain() + return nil + return createState(StateType.LoginKeycardEnterPin, self.flowType, nil) + if keycardFlowType == ResponseTypeValueEnterPUK and + keycardEvent.error.len == 0: + if keycardEvent.pinRetries == 0 and keycardEvent.pukRetries > 0: + return createState(StateType.LoginKeycardMaxPinRetriesReached, self.flowType, nil) + if keycardFlowType == ResponseTypeValueSwapCard and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorNoKeys: + return createState(StateType.LoginKeycardEmpty, self.flowType, nil) + if keycardFlowType == ResponseTypeValueSwapCard and + keycardEvent.error.len > 0 and + keycardEvent.error == RequestParamPUKRetries: + return createState(StateType.LoginKeycardMaxPukRetriesReached, self.flowType, nil) \ No newline at end of file diff --git a/src/app/modules/startup/internal/login_keycard_wrong_keycard.nim b/src/app/modules/startup/internal/login_keycard_wrong_keycard.nim new file mode 100644 index 0000000000..227b2b872f --- /dev/null +++ b/src/app/modules/startup/internal/login_keycard_wrong_keycard.nim @@ -0,0 +1,15 @@ +type + LoginKeycardWrongKeycardState* = ref object of State + +proc newLoginKeycardWrongKeycardState*(flowType: FlowType, backState: State): LoginKeycardWrongKeycardState = + result = LoginKeycardWrongKeycardState() + result.setup(flowType, StateType.LoginKeycardWrongKeycard, backState) + +proc delete*(self: LoginKeycardWrongKeycardState) = + self.State.delete + +method getNextSecondaryState*(self: LoginKeycardWrongKeycardState, controller: Controller): State = + return createState(StateType.WelcomeNewStatusUser, self.flowType, self) + +method getNextTertiaryState*(self: LoginKeycardWrongKeycardState, controller: Controller): State = + return createState(StateType.WelcomeOldStatusUser, self.flowType, self) \ No newline at end of file diff --git a/src/app/modules/startup/internal/login_keycard_wrong_pin_state.nim b/src/app/modules/startup/internal/login_keycard_wrong_pin_state.nim new file mode 100644 index 0000000000..598d5c514f --- /dev/null +++ b/src/app/modules/startup/internal/login_keycard_wrong_pin_state.nim @@ -0,0 +1,43 @@ +type + LoginKeycardWrongPinState* = ref object of State + pinValid: bool + +proc newLoginKeycardWrongPinState*(flowType: FlowType, backState: State): LoginKeycardWrongPinState = + result = LoginKeycardWrongPinState() + result.setup(flowType, StateType.LoginKeycardWrongPin, backState) + result.pinValid = false + +proc delete*(self: LoginKeycardWrongPinState) = + self.State.delete + +method executePrimaryCommand*(self: LoginKeycardWrongPinState, controller: Controller) = + if self.flowType == FlowType.AppLogin: + if controller.getPin().len == PINLengthForStatusApp: + controller.enterKeycardPin(controller.getPin()) + +method getNextSecondaryState*(self: LoginKeycardWrongPinState, controller: Controller): State = + return createState(StateType.WelcomeNewStatusUser, self.flowType, self) + +method getNextTertiaryState*(self: LoginKeycardWrongPinState, controller: Controller): State = + return createState(StateType.WelcomeOldStatusUser, self.flowType, self) + +method resolveKeycardNextState*(self: LoginKeycardWrongPinState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + if self.flowType == FlowType.AppLogin: + if keycardFlowType == ResponseTypeValueEnterPIN and + keycardEvent.error.len > 0 and + keycardEvent.error == RequestParamPIN: + controller.setKeycardData($keycardEvent.pinRetries) + if keycardEvent.pinRetries > 0: + return nil + return createState(StateType.LoginKeycardMaxPinRetriesReached, self.flowType, nil) + if keycardFlowType == ResponseTypeValueEnterPUK and + keycardEvent.error.len == 0: + if keycardEvent.pinRetries == 0 and keycardEvent.pukRetries > 0: + return createState(StateType.LoginKeycardMaxPinRetriesReached, self.flowType, nil) + return nil + if keycardFlowType == ResponseTypeValueKeycardFlowResult and + keycardEvent.error.len == 0: + controller.setKeycardEvent(keycardEvent) + controller.loginAccountKeycard() + return nil \ No newline at end of file diff --git a/src/app/modules/startup/internal/login_state.nim b/src/app/modules/startup/internal/login_state.nim index 7a5760627b..38f07a94d5 100644 --- a/src/app/modules/startup/internal/login_state.nim +++ b/src/app/modules/startup/internal/login_state.nim @@ -1,7 +1,3 @@ -import state -import ../controller -import welcome_state_new_user, welcome_state_old_user - type LoginState* = ref object of State @@ -12,14 +8,22 @@ proc newLoginState*(flowType: FlowType, backState: State): LoginState = proc delete*(self: LoginState) = self.State.delete -method moveToNextPrimaryState*(self: LoginState): bool = - return false - method executePrimaryCommand*(self: LoginState, controller: Controller) = controller.login() -method getNextSecondaryState*(self: LoginState): State = - return newWelcomeStateNewUser(FlowType.General, self) +method getNextSecondaryState*(self: LoginState, controller: Controller): State = + return createState(StateType.WelcomeNewStatusUser, self.flowType, self) -method getNextTertiaryState*(self: LoginState): State = - return newWelcomeStateOldUser(FlowType.General, self) \ No newline at end of file +method getNextTertiaryState*(self: LoginState, controller: Controller): State = + return createState(StateType.WelcomeOldStatusUser, self.flowType, self) + +method resolveKeycardNextState*(self: LoginState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + if self.flowType == FlowType.AppLogin: + if keycardFlowType == ResponseTypeValueKeycardFlowResult and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorConnection: + controller.resumeCurrentFlowLater() + return nil + if keycardFlowType == ResponseTypeValueInsertCard: + return createState(StateType.LoginKeycardInsertKeycard, self.flowType, nil) \ No newline at end of file diff --git a/src/app/modules/startup/internal/notification_state.nim b/src/app/modules/startup/internal/notification_state.nim index 20278b8fbb..2ee9f63c2c 100644 --- a/src/app/modules/startup/internal/notification_state.nim +++ b/src/app/modules/startup/internal/notification_state.nim @@ -1,5 +1,3 @@ -import state, welcome_state - type NotificationState* = ref object of State @@ -10,5 +8,5 @@ proc newNotificationState*(flowType: FlowType, backState: State): NotificationSt proc delete*(self: NotificationState) = self.State.delete -method getNextPrimaryState*(self: NotificationState): State = - return newWelcomeState(FlowType.General, nil) +method getNextPrimaryState*(self: NotificationState, controller: Controller): State = + return createState(StateType.Welcome, FlowType.General, nil) diff --git a/src/app/modules/startup/internal/state.nim b/src/app/modules/startup/internal/state.nim index cad4a11337..5b68226c98 100644 --- a/src/app/modules/startup/internal/state.nim +++ b/src/app/modules/startup/internal/state.nim @@ -1,10 +1,14 @@ import ../controller +from ../../../../app_service/service/keycard/service import KeycardEvent, KeyDetails + +export KeycardEvent, KeyDetails type FlowType* {.pure.} = enum General = "General" FirstRunNewUserNewKeys = "FirstRunNewUserNewKeys" FirstRunNewUserNewKeycardKeys = "FirstRunNewUserNewKeycardKeys" FirstRunNewUserImportSeedPhrase = "FirstRunNewUserImportSeedPhrase" + FirstRunNewUserImportSeedPhraseIntoKeycard = "FirstRunNewUserImportSeedPhraseIntoKeycard" FirstRunOldUserSyncCode = "FirstRunOldUserSyncCode" FirstRunOldUserKeycardImport = "FirstRunOldUserKeycardImport" FirstRunOldUserImportSeedPhrase = "FirstRunOldUserImportSeedPhrase" @@ -23,7 +27,35 @@ type StateType* {.pure.} = enum UserProfileImportSeedPhrase = "UserProfileImportSeedPhrase" UserProfileEnterSeedPhrase = "UserProfileEnterSeedPhrase" Biometrics = "Biometrics" + KeycardPluginReader = "KeycardPluginReader" + KeycardInsertKeycard = "KeycardInsertKeycard" + KeycardReadingKeycard = "KeycardReadingKeycard" + KeycardCreatePin = "KeycardCreatePin" + KeycardRepeatPin = "KeycardRepeatPin" + KeycardPinSet = "KeycardPinSet" + KeycardEnterPin = "KeycardEnterPin" + KeycardWrongPin = "KeycardWrongPin" + KeycardEnterPuk = "KeycardEnterPuk" + KeycardWrongPuk = "KeycardWrongPuk" + KeycardDisplaySeedPhrase = "KeycardDisplaySeedPhrase" + KeycardEnterSeedPhraseWords = "KeycardEnterSeedPhraseWords" + KeycardNotEmpty = "KeycardNotEmpty" + KeycardEmpty = "KeycardEmpty" + KeycardLocked = "KeycardLocked" + KeycardRecover = "KeycardRecover" + KeycardMaxPairingSlotsReached = "KeycardMaxPairingSlotsReached" + KeycardMaxPinRetriesReached = "KeycardMaxPinRetriesReached" + KeycardMaxPukRetriesReached = "KeycardMaxPukRetriesReached" Login = "Login" + LoginKeycardInsertKeycard = "LoginKeycardInsertKeycard" + LoginKeycardReadingKeycard = "LoginKeycardReadingKeycard" + LoginKeycardEnterPin = "LoginKeycardEnterPin" + LoginKeycardWrongKeycard = "LoginKeycardWrongKeycard" + LoginKeycardWrongPin = "LoginKeycardWrongPin" + LoginKeycardMaxPinRetriesReached = "LoginKeycardMaxPinRetriesReached" + LoginKeycardMaxPukRetriesReached = "LoginKeycardMaxPukRetriesReached" + LoginKeycardEmpty = "LoginKeycardEmpty" + ## This is the base class for all state we may have in onboarding/login flow. ## We should not instance of this class (in c++ this will be an abstract class). @@ -67,15 +99,15 @@ method displayBackButton*(self: State): bool {.inline base.} = return not self.backState.isNil ## Returns next state instance in case the "primary" action is triggered -method getNextPrimaryState*(self: State): State {.inline base.} = +method getNextPrimaryState*(self: State, controller: Controller): State {.inline base.} = return nil ## Returns next state instance in case the "secondary" action is triggered -method getNextSecondaryState*(self: State): State {.inline base.} = +method getNextSecondaryState*(self: State, controller: Controller): State {.inline base.} = return nil ## Returns next state instance in case the "tertiary" action is triggered -method getNextTertiaryState*(self: State): State {.inline base.} = +method getNextTertiaryState*(self: State, controller: Controller): State {.inline base.} = return nil ## This method is executed in case "back" button is clicked @@ -94,18 +126,7 @@ method executeSecondaryCommand*(self: State, controller: Controller) {.inline ba method executeTertiaryCommand*(self: State, controller: Controller) {.inline base.} = discard -## Returns true if we should move from this state immediatelly when the "primary" action is triggered, -## in case we need to wait for some other action, or some aync event, this should return false -method moveToNextPrimaryState*(self: State): bool {.inline base.} = - return true - -## Returns true if we should move from this state immediatelly when the "secondary" action is triggered, -## in case we need to wait for some other action, or some aync event, this should return false -method moveToNextSecondaryState*(self: State): bool {.inline base.} = - return true - -## Returns true if we should move from this state immediatelly when the "tertiary" action is triggered, -## in case we need to wait for some other action, or some aync event, this should return false -method moveToNextTertiaryState*(self: State): bool {.inline base.} = - return true - +## This method is used for handling aync responses for keycard related states +method resolveKeycardNextState*(self: State, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State {.inline base.} = + return nil \ No newline at end of file diff --git a/src/app/modules/startup/internal/state_factory.nim b/src/app/modules/startup/internal/state_factory.nim new file mode 100644 index 0000000000..861696b283 --- /dev/null +++ b/src/app/modules/startup/internal/state_factory.nim @@ -0,0 +1,134 @@ +import chronicles +import ../../../../app_service/service/keycard/constants +import ../controller +from ../../../../app_service/service/keycard/service import PINLengthForStatusApp +from ../../../../app_service/service/keycard/service import PUKLengthForStatusApp +import state + +logScope: + topics = "startup-module-state-factory" + +# Forward declaration +proc createState*(stateToBeCreated: StateType, flowType: FlowType, backState: State): State + +include biometrics_state +include keycard_create_pin_state +include keycard_display_seed_phrase_state +include keycard_empty_state +include keycard_enter_pin_state +include keycard_enter_puk_state +include keycard_enter_seed_phrase_words_state +include keycard_insert_keycard_state +include keycard_locked_state +include keycard_max_pairing_slots_reached_state +include keycard_max_pin_retries_reached_state +include keycard_max_puk_retries_reached_state +include keycard_not_empty_state +include keycard_pin_set_state +include keycard_plugin_reader_state +include keycard_reading_keycard_state +include keycard_recover_state +include keycard_repeat_pin_state +include keycard_wrong_pin_state +include keycard_wrong_puk_state +include notification_state +include user_profile_chat_key_state +include user_profile_confirm_password_state +include user_profile_create_password_state +include user_profile_create_state +include user_profile_enter_seed_phrase_state +include user_profile_import_seed_phrase_state +include welcome_state_new_user +include welcome_state_old_user +include welcome_state +include login_state +include login_keycard_insert_keycard_state +include login_keycard_reading_keycard_state +include login_keycard_enter_pin_state +include login_keycard_wrong_keycard +include login_keycard_wrong_pin_state +include login_keycard_max_pin_retries_reached_state +include login_keycard_max_puk_retries_reached_state +include login_keycard_empty_state + +proc createState*(stateToBeCreated: StateType, flowType: FlowType, backState: State): State = + if stateToBeCreated == StateType.AllowNotifications: + return newNotificationState(flowType, backState) + if stateToBeCreated == StateType.Welcome: + return newWelcomeState(flowType, backState) + if stateToBeCreated == StateType.WelcomeNewStatusUser: + return newWelcomeStateNewUser(flowType, backState) + if stateToBeCreated == StateType.WelcomeOldStatusUser: + return newWelcomeStateOldUser(flowType, backState) + if stateToBeCreated == StateType.UserProfileCreate: + return newUserProfileCreateState(flowType, backState) + if stateToBeCreated == StateType.UserProfileChatKey: + return newUserProfileChatKeyState(flowType, backState) + if stateToBeCreated == StateType.UserProfileCreatePassword: + return newUserProfileCreatePasswordState(flowType, backState) + if stateToBeCreated == StateType.UserProfileConfirmPassword: + return newUserProfileConfirmPasswordState(flowType, backState) + if stateToBeCreated == StateType.UserProfileImportSeedPhrase: + return newUserProfileImportSeedPhraseState(flowType, backState) + if stateToBeCreated == StateType.UserProfileEnterSeedPhrase: + return newUserProfileEnterSeedPhraseState(flowType, backState) + if stateToBeCreated == StateType.Biometrics: + return newBiometricsState(flowType, backState) + if stateToBeCreated == StateType.KeycardPluginReader: + return newKeycardPluginReaderState(flowType, backState) + if stateToBeCreated == StateType.KeycardInsertKeycard: + return newKeycardInsertKeycardState(flowType, backState) + if stateToBeCreated == StateType.KeycardReadingKeycard: + return newKeycardReadingKeycardState(flowType, backState) + if stateToBeCreated == StateType.KeycardCreatePin: + return newKeycardCreatePinState(flowType, backState) + if stateToBeCreated == StateType.KeycardRepeatPin: + return newKeycardRepeatPinState(flowType, backState) + if stateToBeCreated == StateType.KeycardPinSet: + return newKeycardPinSetState(flowType, backState) + if stateToBeCreated == StateType.KeycardEnterPin: + return newKeycardEnterPinState(flowType, backState) + if stateToBeCreated == StateType.KeycardWrongPin: + return newKeycardWrongPinState(flowType, backState) + if stateToBeCreated == StateType.KeycardEnterPuk: + return newKeycardEnterPukState(flowType, backState) + if stateToBeCreated == StateType.KeycardWrongPuk: + return newKeycardWrongPukState(flowType, backState) + if stateToBeCreated == StateType.KeycardDisplaySeedPhrase: + return newKeycardDisplaySeedPhraseState(flowType, backState) + if stateToBeCreated == StateType.KeycardEnterSeedPhraseWords: + return newKeycardEnterSeedPhraseWordsState(flowType, backState) + if stateToBeCreated == StateType.KeycardNotEmpty: + return newKeycardNotEmptyState(flowType, backState) + if stateToBeCreated == StateType.KeycardEmpty: + return newKeycardEmptyState(flowType, backState) + if stateToBeCreated == StateType.KeycardLocked: + return newKeycardLockedState(flowType, backState) + if stateToBeCreated == StateType.KeycardRecover: + return newKeycardRecoverState(flowType, backState) + if stateToBeCreated == StateType.KeycardMaxPairingSlotsReached: + return newKeycardMaxPairingSlotsReachedState(flowType, backState) + if stateToBeCreated == StateType.KeycardMaxPinRetriesReached: + return newKeycardMaxPinRetriesReachedState(flowType, backState) + if stateToBeCreated == StateType.KeycardMaxPukRetriesReached: + return newKeycardMaxPukRetriesReachedState(flowType, backState) + if stateToBeCreated == StateType.Login: + return newLoginState(flowType, backState) + if stateToBeCreated == StateType.LoginKeycardInsertKeycard: + return newLoginKeycardInsertKeycardState(flowType, backState) + if stateToBeCreated == StateType.LoginKeycardReadingKeycard: + return newLoginKeycardReadingKeycardState(flowType, backState) + if stateToBeCreated == StateType.LoginKeycardEnterPin: + return newLoginKeycardEnterPinState(flowType, backState) + if stateToBeCreated == StateType.LoginKeycardWrongKeycard: + return newLoginKeycardWrongKeycardState(flowType, backState) + if stateToBeCreated == StateType.LoginKeycardWrongPin: + return newLoginKeycardWrongPinState(flowType, backState) + if stateToBeCreated == StateType.LoginKeycardMaxPinRetriesReached: + return newLoginKeycardMaxPinRetriesReachedState(flowType, backState) + if stateToBeCreated == StateType.LoginKeycardMaxPukRetriesReached: + return newLoginKeycardMaxPukRetriesReachedState(flowType, backState) + if stateToBeCreated == StateType.LoginKeycardEmpty: + return newLoginKeycardEmptyState(flowType, backState) + + error "No implementation available for state ", state=stateToBeCreated \ No newline at end of file diff --git a/src/app/modules/startup/internal/user_profile_chat_key_state.nim b/src/app/modules/startup/internal/user_profile_chat_key_state.nim index dd3649d832..f7ecfb317a 100644 --- a/src/app/modules/startup/internal/user_profile_chat_key_state.nim +++ b/src/app/modules/startup/internal/user_profile_chat_key_state.nim @@ -1,6 +1,3 @@ -import state -import user_profile_create_password_state - type UserProfileChatKeyState* = ref object of State @@ -11,5 +8,12 @@ proc newUserProfileChatKeyState*(flowType: FlowType, backState: State): UserProf proc delete*(self: UserProfileChatKeyState) = self.State.delete -method getNextPrimaryState*(self: UserProfileChatKeyState): State = - return newUserProfileCreatePasswordState(self.State.flowType, self) +method getNextPrimaryState*(self: UserProfileChatKeyState, controller: Controller): State = + if self.flowType == FlowType.FirstRunNewUserNewKeys or + self.flowType == FlowType.FirstRunNewUserImportSeedPhrase or + self.flowType == FlowType.FirstRunOldUserImportSeedPhrase: + return createState(StateType.UserProfileCreatePassword, self.flowType, self) + if self.flowType == FlowType.FirstRunNewUserNewKeycardKeys or + self.flowType == FlowType.FirstRunNewUserImportSeedPhraseIntoKeycard or + self.flowType == FlowType.FirstRunOldUserKeycardImport: + return createState(StateType.Biometrics, self.flowType, self) diff --git a/src/app/modules/startup/internal/user_profile_confirm_password_state.nim b/src/app/modules/startup/internal/user_profile_confirm_password_state.nim index 84950f722f..3cf981f9bc 100644 --- a/src/app/modules/startup/internal/user_profile_confirm_password_state.nim +++ b/src/app/modules/startup/internal/user_profile_confirm_password_state.nim @@ -1,7 +1,3 @@ -import state -import biometrics_state -import ../controller - type UserProfileConfirmPasswordState* = ref object of State @@ -12,23 +8,23 @@ proc newUserProfileConfirmPasswordState*(flowType: FlowType, backState: State): proc delete*(self: UserProfileConfirmPasswordState) = self.State.delete -method moveToNextPrimaryState*(self: UserProfileConfirmPasswordState): bool = - return defined(macosx) - -method getNextPrimaryState*(self: UserProfileConfirmPasswordState): State = - if not self.moveToNextPrimaryState(): +method getNextPrimaryState*(self: UserProfileConfirmPasswordState, controller: Controller): State = + if not defined(macosx): return nil - return newBiometricsState(self.State.flowType, nil) + return createState(StateType.Biometrics, self.flowType, self) method executePrimaryCommand*(self: UserProfileConfirmPasswordState, controller: Controller) = - if self.moveToNextPrimaryState(): + if defined(macosx): return + let storeToKeychain = false # false, cause we don't have keychain support for other than mac os if self.flowType == FlowType.FirstRunNewUserNewKeys: - controller.storeGeneratedAccountAndLogin(storeToKeychain = false) # false, cause we don't have keychain support for other than mac os + controller.storeGeneratedAccountAndLogin(storeToKeychain) elif self.flowType == FlowType.FirstRunNewUserImportSeedPhrase: - controller.storeImportedAccountAndLogin(storeToKeychain = false) # false, cause we don't have keychain support for other than mac os + controller.storeImportedAccountAndLogin(storeToKeychain) elif self.flowType == FlowType.FirstRunOldUserImportSeedPhrase: - ## This should not be the correct call for this flow, this is an issue, but since current implementation is like that - ## and this is not a bug fixing issue, left as it is. - controller.storeImportedAccountAndLogin(storeToKeychain = false) # false, cause we don't have keychain support for other than mac os + ## This should not be the correct call for this flow, this is an issue, + ## but since current implementation is like that and this is not a bug fixing issue, left as it is. + controller.storeImportedAccountAndLogin(storeToKeychain) + elif self.flowType == FlowType.FirstRunNewUserNewKeycardKeys: + controller.storeKeycardAccountAndLogin(storeToKeychain) diff --git a/src/app/modules/startup/internal/user_profile_create_password_state.nim b/src/app/modules/startup/internal/user_profile_create_password_state.nim index ff6febf90e..a61fb81fe7 100644 --- a/src/app/modules/startup/internal/user_profile_create_password_state.nim +++ b/src/app/modules/startup/internal/user_profile_create_password_state.nim @@ -1,7 +1,3 @@ -import state -import ../controller -import user_profile_confirm_password_state - type UserProfileCreatePasswordState* = ref object of State @@ -12,8 +8,8 @@ proc newUserProfileCreatePasswordState*(flowType: FlowType, backState: State): U proc delete*(self: UserProfileCreatePasswordState) = self.State.delete -method getNextPrimaryState*(self: UserProfileCreatePasswordState): State = - return newUserProfileConfirmPasswordState(self.State.flowType, self) +method getNextPrimaryState*(self: UserProfileCreatePasswordState, controller: Controller): State = + return createState(StateType.UserProfileConfirmPassword, self.flowType, self) method executeBackCommand*(self: UserProfileCreatePasswordState, controller: Controller) = controller.setPassword("") \ No newline at end of file diff --git a/src/app/modules/startup/internal/user_profile_create_state.nim b/src/app/modules/startup/internal/user_profile_create_state.nim index 6eca532399..03033a5736 100644 --- a/src/app/modules/startup/internal/user_profile_create_state.nim +++ b/src/app/modules/startup/internal/user_profile_create_state.nim @@ -1,7 +1,3 @@ -import state -import ../controller -import user_profile_chat_key_state - type UserProfileCreateState* = ref object of State @@ -12,8 +8,8 @@ proc newUserProfileCreateState*(flowType: FlowType, backState: State): UserProfi proc delete*(self: UserProfileCreateState) = self.State.delete -method getNextPrimaryState*(self: UserProfileCreateState): State = - return newUserProfileChatKeyState(self.State.flowType, self) +method getNextPrimaryState*(self: UserProfileCreateState, controller: Controller): State = + return createState(StateType.UserProfileChatKey, self.flowType, self) method executeBackCommand*(self: UserProfileCreateState, controller: Controller) = controller.setDisplayName("") \ No newline at end of file diff --git a/src/app/modules/startup/internal/user_profile_enter_seed_phrase_state.nim b/src/app/modules/startup/internal/user_profile_enter_seed_phrase_state.nim index 33b41151fa..5eb181d5f5 100644 --- a/src/app/modules/startup/internal/user_profile_enter_seed_phrase_state.nim +++ b/src/app/modules/startup/internal/user_profile_enter_seed_phrase_state.nim @@ -1,7 +1,3 @@ -import state -import ../controller -import user_profile_create_state - type UserProfileEnterSeedPhraseState* = ref object of State successfulImport: bool @@ -14,13 +10,44 @@ proc newUserProfileEnterSeedPhraseState*(flowType: FlowType, backState: State): proc delete*(self: UserProfileEnterSeedPhraseState) = self.State.delete -method moveToNextPrimaryState*(self: UserProfileEnterSeedPhraseState): bool = - return self.successfulImport - -method getNextPrimaryState*(self: UserProfileEnterSeedPhraseState): State = - if not self.moveToNextPrimaryState(): +method getNextPrimaryState*(self: UserProfileEnterSeedPhraseState, controller: Controller): State = + if not self.successfulImport: return nil - return newUserProfileCreateState(self.State.flowType, self) + if self.flowType == FlowType.FirstRunOldUserImportSeedPhrase or + self.flowType == FlowType.FirstRunNewUserImportSeedPhrase: + return createState(StateType.UserProfileCreate, self.flowType, self) method executePrimaryCommand*(self: UserProfileEnterSeedPhraseState, controller: Controller) = - self.successfulImport = controller.importMnemonic() \ No newline at end of file + if self.flowType == FlowType.AppLogin: + controller.runLoadAccountFlowWithSeedPhrase(controller.getSeedPhraseLength(), controller.getSeedPhrase(), true) + else: + self.successfulImport = controller.importMnemonic() + if self.successfulImport: + if self.flowType == FlowType.FirstRunNewUserImportSeedPhraseIntoKeycard: + controller.storeSeedPhraseToKeycard(controller.getSeedPhraseLength(), controller.getSeedPhrase()) + elif self.flowType == FlowType.FirstRunOldUserKeycardImport: + controller.runLoadAccountFlowWithSeedPhrase(controller.getSeedPhraseLength(), controller.getSeedPhrase(), true) + +method resolveKeycardNextState*(self: UserProfileEnterSeedPhraseState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + if self.flowType == FlowType.FirstRunNewUserImportSeedPhraseIntoKeycard: + if keycardFlowType == ResponseTypeValueEnterNewPIN and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorRequireInit: + return createState(StateType.KeycardCreatePin, self.flowType, self.getBackState) + if self.flowType == FlowType.FirstRunOldUserKeycardImport: + if keycardFlowType == ResponseTypeValueKeycardFlowResult and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorConnection: + controller.resumeCurrentFlowLater() + return createState(StateType.KeycardPluginReader, self.flowType, self) + if keycardFlowType == ResponseTypeValueInsertCard: + return createState(StateType.KeycardInsertKeycard, self.flowType, self.getBackState) + if self.flowType == FlowType.AppLogin: + if keycardFlowType == ResponseTypeValueKeycardFlowResult and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorConnection: + controller.resumeCurrentFlowLater() + return createState(StateType.KeycardPluginReader, self.flowType, self) + if keycardFlowType == ResponseTypeValueInsertCard: + return createState(StateType.KeycardInsertKeycard, self.flowType, self.getBackState) diff --git a/src/app/modules/startup/internal/user_profile_import_seed_phrase_state.nim b/src/app/modules/startup/internal/user_profile_import_seed_phrase_state.nim index ac56a7a365..dc278fd202 100644 --- a/src/app/modules/startup/internal/user_profile_import_seed_phrase_state.nim +++ b/src/app/modules/startup/internal/user_profile_import_seed_phrase_state.nim @@ -1,6 +1,3 @@ -import state -import user_profile_enter_seed_phrase_state - type UserProfileImportSeedPhraseState* = ref object of State @@ -11,5 +8,8 @@ proc newUserProfileImportSeedPhraseState*(flowType: FlowType, backState: State): proc delete*(self: UserProfileImportSeedPhraseState) = self.State.delete -method getNextPrimaryState*(self: UserProfileImportSeedPhraseState): State = - return newUserProfileEnterSeedPhraseState(self.State.flowType, self) \ No newline at end of file +method getNextPrimaryState*(self: UserProfileImportSeedPhraseState, controller: Controller): State = + return createState(StateType.UserProfileEnterSeedPhrase, FlowType.FirstRunNewUserImportSeedPhrase, self) + +method getNextSecondaryState*(self: UserProfileImportSeedPhraseState, controller: Controller): State = + return createState(StateType.KeycardPluginReader, FlowType.FirstRunNewUserImportSeedPhraseIntoKeycard, self) \ No newline at end of file diff --git a/src/app/modules/startup/internal/welcome_state.nim b/src/app/modules/startup/internal/welcome_state.nim index d229ec7d5e..2cfff9eb6d 100644 --- a/src/app/modules/startup/internal/welcome_state.nim +++ b/src/app/modules/startup/internal/welcome_state.nim @@ -1,6 +1,3 @@ -import state -import welcome_state_new_user, welcome_state_old_user - type WelcomeState* = ref object of State @@ -11,8 +8,8 @@ proc newWelcomeState*(flowType: FlowType, backState: State): WelcomeState = proc delete*(self: WelcomeState) = self.State.delete -method getNextPrimaryState*(self: WelcomeState): State = - return newWelcomeStateNewUser(FlowType.General, self) +method getNextPrimaryState*(self: WelcomeState, controller: Controller): State = + return createState(StateType.WelcomeNewStatusUser, FlowType.General, self) -method getNextSecondaryState*(self: WelcomeState): State = - return newWelcomeStateOldUser(FlowType.General, self) +method getNextSecondaryState*(self: WelcomeState, controller: Controller): State = + return createState(StateType.WelcomeOldStatusUser, FlowType.General, self) diff --git a/src/app/modules/startup/internal/welcome_state_new_user.nim b/src/app/modules/startup/internal/welcome_state_new_user.nim index 5b80da0329..6d87b014a0 100644 --- a/src/app/modules/startup/internal/welcome_state_new_user.nim +++ b/src/app/modules/startup/internal/welcome_state_new_user.nim @@ -1,6 +1,3 @@ -import state -import user_profile_create_state, user_profile_import_seed_phrase_state - type WelcomeStateNewUser* = ref object of State @@ -11,14 +8,15 @@ proc newWelcomeStateNewUser*(flowType: FlowType, backState: State): WelcomeState proc delete*(self: WelcomeStateNewUser) = self.State.delete -method getNextPrimaryState*(self: WelcomeStateNewUser): State = - return newUserProfileCreateState(FlowType.FirstRunNewUserNewKeys, self) +method executeBackCommand*(self: WelcomeStateNewUser, controller: Controller) = + if self.flowType == FlowType.AppLogin and controller.isKeycardCreatedAccountSelectedOne(): + controller.runLoginFlow() -method getNextSecondaryState*(self: WelcomeStateNewUser): State = - # We will handle here a click on `Generate keys for a new Keycard` - discard - -method getNextTertiaryState*(self: WelcomeStateNewUser): State = - return newUserProfileImportSeedPhraseState(FlowType.FirstRunNewUserImportSeedPhrase, self) +method getNextPrimaryState*(self: WelcomeStateNewUser, controller: Controller): State = + return createState(StateType.UserProfileCreate, FlowType.FirstRunNewUserNewKeys, self) +method getNextSecondaryState*(self: WelcomeStateNewUser, controller: Controller): State = + return createState(StateType.KeycardPluginReader, FlowType.FirstRunNewUserNewKeycardKeys, self) +method getNextTertiaryState*(self: WelcomeStateNewUser, controller: Controller): State = + return createState(StateType.UserProfileImportSeedPhrase, FlowType.General, self) \ No newline at end of file diff --git a/src/app/modules/startup/internal/welcome_state_old_user.nim b/src/app/modules/startup/internal/welcome_state_old_user.nim index 070549a09a..8d4396a77d 100644 --- a/src/app/modules/startup/internal/welcome_state_old_user.nim +++ b/src/app/modules/startup/internal/welcome_state_old_user.nim @@ -1,6 +1,3 @@ -import state -import user_profile_enter_seed_phrase_state - type WelcomeStateOldUser* = ref object of State @@ -11,19 +8,17 @@ proc newWelcomeStateOldUser*(flowType: FlowType, backState: State): WelcomeState proc delete*(self: WelcomeStateOldUser) = self.State.delete -method getNextPrimaryState*(self: WelcomeStateOldUser): State = +method executeBackCommand*(self: WelcomeStateOldUser, controller: Controller) = + if self.flowType == FlowType.AppLogin and controller.isKeycardCreatedAccountSelectedOne(): + controller.runLoginFlow() + +method getNextPrimaryState*(self: WelcomeStateOldUser, controller: Controller): State = # We will handle here a click on `Scan sync code` discard -method getNextSecondaryState*(self: WelcomeStateOldUser): State = - # We will handle here a click on `Login with Keycard` - discard +method getNextSecondaryState*(self: WelcomeStateOldUser, controller: Controller): State = + return createState(StateType.KeycardPluginReader, FlowType.FirstRunOldUserKeycardImport, self) -method getNextTertiaryState*(self: WelcomeStateOldUser): State = - ## This is added as next state in case of import seed for an old user, but this doesn't match the flow - ## in the design. Need to be fixed correctly. - ## Why it's not fixed now??? - ## -> Cause this is just a improving and moving to a better form what we currently have, fixing will be done in another issue - ## and need to be discussed as we haven't had that flow implemented ever before - return newUserProfileEnterSeedPhraseState(FlowType.FirstRunOldUserImportSeedPhrase, self) +method getNextTertiaryState*(self: WelcomeStateOldUser, controller: Controller): State = + return createState(StateType.UserProfileEnterSeedPhrase, FlowType.FirstRunOldUserImportSeedPhrase, self) diff --git a/src/app/modules/startup/io_interface.nim b/src/app/modules/startup/io_interface.nim index 47ccb6849e..3eff98cb22 100644 --- a/src/app/modules/startup/io_interface.nim +++ b/src/app/modules/startup/io_interface.nim @@ -1,5 +1,7 @@ -import ../../../app_service/service/accounts/service +import ../../../app_service/service/accounts/service as accounts_service import models/login_account_item as login_acc_item +from ../../../app_service/service/keycard/service import KeycardEvent, KeyDetails + type AccessInterface* {.pure inheritable.} = ref object of RootObj @@ -37,6 +39,9 @@ method getImportedAccount*(self: AccessInterface): GeneratedAccountDto {.base.} method generateImage*(self: AccessInterface, imageUrl: string, aX: int, aY: int, bX: int, bY: int): string {.base.} = raise newException(ValueError, "No implementation available") +method getCroppedProfileImage*(self: AccessInterface): string {.base.} = + raise newException(ValueError, "No implementation available") + method setDisplayName*(self: AccessInterface, value: string) {.base.} = raise newException(ValueError, "No implementation available") @@ -49,6 +54,15 @@ method setPassword*(self: AccessInterface, value: string) {.base.} = method getPassword*(self: AccessInterface): string {.base.} = raise newException(ValueError, "No implementation available") +method setPin*(self: AccessInterface, value: string) {.base.} = + raise newException(ValueError, "No implementation available") + +method setPuk*(self: AccessInterface, value: string) {.base.} = + raise newException(ValueError, "No implementation available") + +method getPin*(self: AccessInterface): string {.base.} = + raise newException(ValueError, "No implementation available") + method getPasswordStrengthScore*(self: AccessInterface, password: string, userName: string): int {.base.} = raise newException(ValueError, "No implementation available") @@ -73,12 +87,24 @@ method onNodeLogin*(self: AccessInterface, error: string) {.base.} = method emitAccountLoginError*(self: AccessInterface, error: string) {.base.} = raise newException(ValueError, "No implementation available") -method emitObtainingPasswordError*(self: AccessInterface, errorDescription: string) {.base.} = +method emitObtainingPasswordError*(self: AccessInterface, errorDescription: string, errorType: string) {.base.} = raise newException(ValueError, "No implementation available") method emitObtainingPasswordSuccess*(self: AccessInterface, password: string) {.base.} = raise newException(ValueError, "No implementation available") +method onKeycardResponse*(self: AccessInterface, keycardFlowType: string, keycardEvent: KeycardEvent) {.base.} = + raise newException(ValueError, "No implementation available") + +method checkRepeatedKeycardPinWhileTyping*(self: AccessInterface, pin: string): bool {.base.} = + raise newException(ValueError, "No implementation available") + +method getSeedPhrase*(self: AccessInterface): string {.base.} = + raise newException(ValueError, "No implementation available") + +method setKeycardData*(self: AccessInterface, value: string) {.base.} = + raise newException(ValueError, "No implementation available") + # This way (using concepts) is used only for the modules managed by AppController type DelegateInterface* = concept c diff --git a/src/app/modules/startup/models/login_account_item.nim b/src/app/modules/startup/models/login_account_item.nim index c80f54c8ae..4cbc70d5c8 100644 --- a/src/app/modules/startup/models/login_account_item.nim +++ b/src/app/modules/startup/models/login_account_item.nim @@ -12,8 +12,10 @@ type colorHash: color_hash_model.Model colorHashVariant: QVariant colorId: int + keycardPairing: string -proc initItem*(name, thumbnailImage, largeImage, keyUid: string, colorHash: seq[ColorHashSegment], colorId: int): +proc initItem*(name, thumbnailImage, largeImage, keyUid: string, colorHash: seq[ColorHashSegment], colorId: int, + keycardPairing: string): Item = result.name = name result.thumbnailImage = thumbnailImage @@ -23,6 +25,7 @@ proc initItem*(name, thumbnailImage, largeImage, keyUid: string, colorHash: seq[ result.colorHash.setItems(map(colorHash, x => color_hash_item.initItem(x.len, x.colorIdx))) result.colorHashVariant = newQVariant(result.colorHash) result.colorId = colorId + result.keycardPairing = keycardPairing proc getName*(self: Item): string = return self.name @@ -44,3 +47,9 @@ proc getColorHashVariant*(self: Item): QVariant = proc getColorId*(self: Item): int = return self.colorId + +proc getKeycardPairing*(self: Item): string = + return self.keycardPairing + +proc getKeycardCreatedAccount*(self: Item): bool = + return self.keycardPairing.len > 0 \ No newline at end of file diff --git a/src/app/modules/startup/models/login_account_model.nim b/src/app/modules/startup/models/login_account_model.nim index 97939650a7..d2729d7eac 100644 --- a/src/app/modules/startup/models/login_account_model.nim +++ b/src/app/modules/startup/models/login_account_model.nim @@ -10,6 +10,8 @@ type KeyUid ColorHash ColorId + KeycardPairing + KeycardCreatedAccount QtObject: type @@ -37,7 +39,9 @@ QtObject: ModelRole.LargeImage.int:"largeImage", ModelRole.KeyUid.int:"keyUid", ModelRole.ColorHash.int:"colorHash", - ModelRole.ColorId.int:"colorId" + ModelRole.ColorId.int:"colorId", + ModelRole.KeycardPairing.int:"keycardPairing", + ModelRole.KeycardCreatedAccount.int:"keycardCreatedAccount" }.toTable method data(self: Model, index: QModelIndex, role: int): QVariant = @@ -63,6 +67,10 @@ QtObject: result = newQVariant(item.getColorHash()) of ModelRole.ColorId: result = newQVariant(item.getColorId()) + of ModelRole.KeycardPairing: + result = newQVariant(item.getKeycardPairing()) + of ModelRole.KeycardCreatedAccount: + result = newQVariant(item.getKeycardCreatedAccount()) proc setItems*(self: Model, items: seq[Item]) = self.beginResetModel() diff --git a/src/app/modules/startup/module.nim b/src/app/modules/startup/module.nim index 2518ab14ea..64ed97fd82 100644 --- a/src/app/modules/startup/module.nim +++ b/src/app/modules/startup/module.nim @@ -2,7 +2,7 @@ import NimQml, chronicles import io_interface import view, controller -import internal/[state, notification_state, welcome_state, login_state] +import internal/[state, state_factory] import models/generated_account_item as gen_acc_item import models/login_account_item as login_acc_item import ../../global/global_singleton @@ -12,6 +12,7 @@ import ../../../app_service/service/keychain/service as keychain_service import ../../../app_service/service/accounts/service as accounts_service import ../../../app_service/service/general/service as general_service import ../../../app_service/service/profile/service as profile_service +import ../../../app_service/service/keycard/service as keycard_service export io_interface @@ -30,14 +31,15 @@ proc newModule*[T](delegate: T, keychainService: keychain_service.Service, accountsService: accounts_service.Service, generalService: general_service.Service, - profileService: profile_service.Service): + profileService: profile_service.Service, + keycardService: keycard_service.Service): Module[T] = result = Module[T]() result.delegate = delegate result.view = view.newView(result) result.viewVariant = newQVariant(result.view) result.controller = controller.newController(result, events, generalService, accountsService, keychainService, - profileService) + profileService, keycardService) method delete*[T](self: Module[T]) = self.view.delete @@ -69,19 +71,21 @@ method load*[T](self: Module[T]) = self.view.setCurrentStartupState(newWelcomeState(FlowType.General, nil)) else: let openedAccounts = self.controller.getOpenedAccounts() - if(openedAccounts.len > 0): - var items: seq[login_acc_item.Item] - for acc in openedAccounts: - var thumbnailImage: string - var largeImage: string - self.extractImages(acc, thumbnailImage, largeImage) - items.add(login_acc_item.initItem(acc.name, thumbnailImage, largeImage, acc.keyUid, acc.colorHash, acc.colorId)) - self.view.setLoginAccountsModelItems(items) - # set the first account as slected one - if items.len == 0: - error "cannot run the app in login flow cause list of login accounts is empty" - quit() # quit the app - self.setSelectedLoginAccount(items[0]) + var items: seq[login_acc_item.Item] + for acc in openedAccounts: + var thumbnailImage: string + var largeImage: string + self.extractImages(acc, thumbnailImage, largeImage) + items.add(login_acc_item.initItem(acc.name, thumbnailImage, largeImage, acc.keyUid, acc.colorHash, acc.colorId, + acc.keycardPairing)) + self.view.setLoginAccountsModelItems(items) + # set the first account as slected one + if items.len == 0: + # we should never be here, since else block of `if (shouldStartWithOnboardingScreen)` + # ensures that `openedAccounts` is not empty array + error "cannot run the app in login flow cause list of login accounts is empty" + quit() # quit the app + self.setSelectedLoginAccount(items[0]) self.view.setCurrentStartupState(newLoginState(FlowType.AppLogin, nil)) self.delegate.startupDidLoad() @@ -96,35 +100,54 @@ method emitLogOut*[T](self: Module[T]) = method onBackActionClicked*[T](self: Module[T]) = let currStateObj = self.view.currentStartupStateObj() - if not currStateObj.isNil: - currStateObj.executeBackCommand(self.controller) - let backState = currStateObj.getBackState() - self.view.setCurrentStartupState(backState) - currStateObj.delete() + if currStateObj.isNil: + error "cannot resolve current state" + return + debug "back_action", currFlow=currStateObj.flowType(), currState=currStateObj.stateType() + currStateObj.executeBackCommand(self.controller) + let backState = currStateObj.getBackState() + self.view.setCurrentStartupState(backState) + debug "back_action - set state", setCurrFlow=backState.flowType(), newCurrState=backState.stateType() + currStateObj.delete() method onPrimaryActionClicked*[T](self: Module[T]) = let currStateObj = self.view.currentStartupStateObj() - if not currStateObj.isNil: - currStateObj.executePrimaryCommand(self.controller) - if currStateObj.moveToNextPrimaryState(): - let nextState = currStateObj.getNextPrimaryState() - self.view.setCurrentStartupState(nextState) + if currStateObj.isNil: + error "cannot resolve current state" + return + debug "primary_action", currFlow=currStateObj.flowType(), currState=currStateObj.stateType() + currStateObj.executePrimaryCommand(self.controller) + let nextState = currStateObj.getNextPrimaryState(self.controller) + if nextState.isNil: + return + self.view.setCurrentStartupState(nextState) + debug "primary_action - set state", setCurrFlow=nextState.flowType(), setCurrState=nextState.stateType() method onSecondaryActionClicked*[T](self: Module[T]) = let currStateObj = self.view.currentStartupStateObj() - if not currStateObj.isNil: - currStateObj.executeSecondaryCommand(self.controller) - if currStateObj.moveToNextSecondaryState(): - let nextState = currStateObj.getNextSecondaryState() - self.view.setCurrentStartupState(nextState) + if currStateObj.isNil: + error "cannot resolve current state" + return + debug "secondary_action", currFlow=currStateObj.flowType(), currState=currStateObj.stateType() + currStateObj.executeSecondaryCommand(self.controller) + let nextState = currStateObj.getNextSecondaryState(self.controller) + if nextState.isNil: + return + self.view.setCurrentStartupState(nextState) + debug "secondary_action - set state", setCurrFlow=nextState.flowType(), setCurrState=nextState.stateType() method onTertiaryActionClicked*[T](self: Module[T]) = let currStateObj = self.view.currentStartupStateObj() - if not currStateObj.isNil: - currStateObj.executeTertiaryCommand(self.controller) - if currStateObj.moveToNextTertiaryState(): - let nextState = currStateObj.getNextTertiaryState() - self.view.setCurrentStartupState(nextState) + if currStateObj.isNil: + error "cannot resolve current state" + return + debug "tertiary_action", currFlow=currStateObj.flowType(), currState=currStateObj.stateType() + currStateObj.executeTertiaryCommand(self.controller) + let nextState = currStateObj.getNextTertiaryState(self.controller) + if nextState.isNil: + return + self.view.setCurrentStartupState(nextState) + debug "tertiary_action - set state", setCurrFlow=nextState.flowType(), setCurrState=nextState.stateType() method getImportedAccount*[T](self: Module[T]): GeneratedAccountDto = return self.controller.getImportedAccount() @@ -132,6 +155,9 @@ method getImportedAccount*[T](self: Module[T]): GeneratedAccountDto = method generateImage*[T](self: Module[T], imageUrl: string, aX: int, aY: int, bX: int, bY: int): string = return self.controller.generateImage(imageUrl, aX, aY, bX, bY) +method getCroppedProfileImage*[T](self: Module[T]): string = + return self.controller.getCroppedProfileImage() + method setDisplayName*[T](self: Module[T], value: string) = self.controller.setDisplayName(value) @@ -144,6 +170,15 @@ method setPassword*[T](self: Module[T], value: string) = method getPassword*[T](self: Module[T]): string = return self.controller.getPassword() +method setPin*[T](self: Module[T], value: string) = + self.controller.setPin(value) + +method setPuk*[T](self: Module[T], value: string) = + self.controller.setPuk(value) + +method getPin*[T](self: Module[T]): string = + return self.controller.getPin() + method getPasswordStrengthScore*[T](self: Module[T], password, userName: string): int = return self.controller.getPasswordStrengthScore(password, userName) @@ -161,13 +196,18 @@ method importAccountSuccess*[T](self: Module[T]) = method setSelectedLoginAccount*[T](self: Module[T], item: login_acc_item.Item) = self.controller.setSelectedLoginAccountKeyUid(item.getKeyUid()) + if item.getKeycardCreatedAccount(): + self.view.setCurrentStartupState(newLoginState(FlowType.AppLogin, nil)) # nim garbage collector will handle all abandoned state objects + self.controller.runLoginFlow() + else: + self.controller.tryToObtainDataFromKeychain() self.view.setSelectedLoginAccount(item) method emitAccountLoginError*[T](self: Module[T], error: string) = self.view.emitAccountLoginError(error) -method emitObtainingPasswordError*[T](self: Module[T], errorDescription: string) = - self.view.emitObtainingPasswordError(errorDescription) +method emitObtainingPasswordError*[T](self: Module[T], errorDescription: string, errorType: string) = + self.view.emitObtainingPasswordError(errorDescription, errorType) method emitObtainingPasswordSuccess*[T](self: Module[T], password: string) = self.view.emitObtainingPasswordSuccess(password) @@ -175,7 +215,7 @@ method emitObtainingPasswordSuccess*[T](self: Module[T], password: string) = method onNodeLogin*[T](self: Module[T], error: string) = let currStateObj = self.view.currentStartupStateObj() if currStateObj.isNil: - error "error: cannot determine current startup state" + error "cannot determine current startup state" quit() # quit the app if error.len == 0: @@ -187,4 +227,37 @@ method onNodeLogin*[T](self: Module[T], error: string) = self.emitAccountLoginError(error) else: self.setupAccountError(error) - error "error: ", methodName="onNodeLogin", errDesription =error \ No newline at end of file + error "login error", methodName="onNodeLogin", errDesription =error + +method onKeycardResponse*[T](self: Module[T], keycardFlowType: string, keycardEvent: KeycardEvent) = + let currStateObj = self.view.currentStartupStateObj() + if currStateObj.isNil: + error "cannot resolve current state" + return + debug "on_keycard_response", currFlow=currStateObj.flowType(), currState=currStateObj.stateType() + let nextState = currStateObj.resolveKeycardNextState(keycardFlowType, keycardEvent, self.controller) + if nextState.isNil: + return + self.view.setCurrentStartupState(nextState) + debug "on_keycard_response - set state", setCurrFlow=nextState.flowType(), setCurrState=nextState.stateType() + +method checkRepeatedKeycardPinWhileTyping*[T](self: Module[T], pin: string): bool = + self.controller.setPinMatch(false) + let storedPin = self.controller.getPin() + if pin.len > storedPin.len: + return false + elif pin.len < storedPin.len: + for i in 0 ..< pin.len: + if pin[i] != storedPin[i]: + return false + return true + else: + let match = pin == storedPin + self.controller.setPinMatch(match) + return match + +method getSeedPhrase*[T](self: Module[T]): string = + return self.controller.getSeedPhrase() + +method setKeycardData*[T](self: Module[T], value: string) = + self.view.setKeycardData(value) \ No newline at end of file diff --git a/src/app/modules/startup/selected_login_account.nim b/src/app/modules/startup/selected_login_account.nim index 9f81f82bd4..00ce0c01ca 100644 --- a/src/app/modules/startup/selected_login_account.nim +++ b/src/app/modules/startup/selected_login_account.nim @@ -26,30 +26,35 @@ QtObject: proc getKeyUid(self: SelectedLoginAccount): string {.slot.} = return self.item.getKeyUid() - QtProperty[string] keyUid: read = getKeyUid + proc getKeycardPairing(self: SelectedLoginAccount): string {.slot.} = + return self.item.getKeycardPairing() + QtProperty[string] keycardPairing: + read = getKeycardPairing + + proc getKeycardCreatedAccount(self: SelectedLoginAccount): bool {.slot.} = + return self.item.getKeycardPairing().len > 0 + QtProperty[bool] keycardCreatedAccount: + read = getKeycardCreatedAccount + proc getColorHash(self: SelectedLoginAccount): QVariant {.slot.} = return self.item.getColorHashVariant() - QtProperty[QVariant] colorHash: read = getColorHash proc getColorId(self: SelectedLoginAccount): int {.slot.} = return self.item.getColorId() - QtProperty[int] colorId: read = getColorId proc getThumbnailImage(self: SelectedLoginAccount): string {.slot.} = return self.item.getThumbnailImage() - QtProperty[string] thumbnailImage: read = getThumbnailImage proc getLargeImage(self: SelectedLoginAccount): string {.slot.} = return self.item.getLargeImage() - QtProperty[string] largeImage: - read = getLargeImage + read = getLargeImage \ No newline at end of file diff --git a/src/app/modules/startup/view.nim b/src/app/modules/startup/view.nim index fba19023e9..4173f7f278 100644 --- a/src/app/modules/startup/view.nim +++ b/src/app/modules/startup/view.nim @@ -26,6 +26,7 @@ QtObject: loginAccountsModel: login_acc_model.Model loginAccountsModelVariant: QVariant appState: AppState + keycardData: string # used to temporary store the data coming from keycard, depends on current state different data may be stored proc delete*(self: View) = self.currentStartupStateVariant.delete @@ -133,7 +134,10 @@ QtObject: notify = importedAccountChanged proc generateImage*(self: View, imageUrl: string, aX: int, aY: int, bX: int, bY: int): string {.slot.} = - self.delegate.generateImage(imageUrl, aX, aY, bX, bY) + return self.delegate.generateImage(imageUrl, aX, aY, bX, bY) + + proc getCroppedProfileImage*(self: View): string {.slot.} = + return self.delegate.getCroppedProfileImage() proc setDisplayName*(self: View, value: string) {.slot.} = self.delegate.setDisplayName(value) @@ -147,6 +151,15 @@ QtObject: proc getPassword*(self: View): string {.slot.} = return self.delegate.getPassword() + proc setPin*(self: View, value: string) {.slot.} = + self.delegate.setPin(value) + + proc getPin*(self: View): string {.slot.} = + return self.delegate.getPin() + + proc setPuk*(self: View, value: string) {.slot.} = + self.delegate.setPuk(value) + proc getPasswordStrengthScore*(self: View, password: string, userName: string): int {.slot.} = return self.delegate.getPasswordStrengthScore(password, userName) @@ -194,10 +207,28 @@ QtObject: proc emitAccountLoginError*(self: View, error: string) = self.accountLoginError(error) - proc obtainingPasswordError*(self:View, errorDescription: string) {.signal.} - proc emitObtainingPasswordError*(self: View, errorDescription: string) = - self.obtainingPasswordError(errorDescription) + proc obtainingPasswordError*(self:View, errorDescription: string, errorType: string) {.signal.} + proc emitObtainingPasswordError*(self: View, errorDescription: string, errorType: string) = + self.obtainingPasswordError(errorDescription, errorType) proc obtainingPasswordSuccess*(self:View, password: string) {.signal.} proc emitObtainingPasswordSuccess*(self: View, password: string) = - self.obtainingPasswordSuccess(password) \ No newline at end of file + self.obtainingPasswordSuccess(password) + + proc checkRepeatedKeycardPinWhileTyping*(self: View, pin: string): bool {.slot.} = + return self.delegate.checkRepeatedKeycardPinWhileTyping(pin) + + proc getSeedPhrase*(self: View): string {.slot.} = + return self.delegate.getSeedPhrase() + + proc keycardDataChanged*(self: View) {.signal.} + proc setKeycardData*(self: View, value: string) = + if self.keycardData == value: + return + self.keycardData = value + self.keycardDataChanged() + proc getKeycardData*(self: View): string {.slot.} = + return self.keycardData + QtProperty[string] keycardData: + read = getKeycardData + notify = keycardDataChanged \ No newline at end of file diff --git a/src/app_service/common/mnemonics.nim b/src/app_service/common/mnemonics.nim new file mode 100644 index 0000000000..d0a81ae4d5 --- /dev/null +++ b/src/app_service/common/mnemonics.nim @@ -0,0 +1,8 @@ +const englishWords*: seq[string] = @["abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", "accident", "account", "accuse", "achieve", "acid", "acoustic", "acquire", "across", "act", "action", "actor", "actress", "actual", "adapt", "add", "addict", "address", "adjust", "admit", "adult", "advance", "advice", "aerobic", "affair", "afford", "afraid", "again", "age", "agent", "agree", "ahead", "aim", "air", "airport", "aisle", "alarm", "album", "alcohol", "alert", "alien", "all", "alley", "allow", "almost", "alone", "alpha", "already", "also", "alter", "always", "amateur", "amazing", "among", "amount", "amused", "analyst", "anchor", "ancient", "anger", "angle", "angry", "animal", "ankle", "announce", "annual", "another", "answer", "antenna", "antique", "anxiety", "any", "apart", "apology", "appear", "apple", "approve", "april", "arch", "arctic", "area", "arena", "argue", "arm", "armed", "armor", "army", "around", "arrange", "arrest", "arrive", "arrow", "art", "artefact", "artist", "artwork", "ask", "aspect", "assault", "asset", "assist", "assume", "asthma", "athlete", "atom", "attack", "attend", "attitude", "attract", "auction", "audit", "august", "aunt", "author", "auto", "autumn", "average", "avocado", "avoid", "awake", "aware", "away", "awesome", "awful", "awkward", "axis", "baby", "bachelor", "bacon", "badge", "bag", "balance", "balcony", "ball", "bamboo", "banana", "banner", "bar", "barely", "bargain", "barrel", "base", "basic", "basket", "battle", "beach", "bean", "beauty", "because", "become", "beef", "before", "begin", "behave", "behind", "believe", "below", "belt", "bench", "benefit", "best", "betray", "better", "between", "beyond", "bicycle", "bid", "bike", "bind", "biology", "bird", "birth", "bitter", "black", "blade", "blame", "blanket", "blast", "bleak", "bless", "blind", "blood", "blossom", "blouse", "blue", "blur", "blush", "board", "boat", "body", "boil", "bomb", "bone", "bonus", "book", "boost", "border", "boring", "borrow", "boss", "bottom", "bounce", "box", "boy", "bracket", "brain", "brand", "brass", "brave", "bread", "breeze", "brick", "bridge", "brief", "bright", "bring", "brisk", "broccoli", "broken", "bronze", "broom", "brother", "brown", "brush", "bubble", "buddy", "budget", "buffalo", "build", "bulb", "bulk", "bullet", "bundle", "bunker", "burden", "burger", "burst", "bus", "business", "busy", "butter", "buyer", "buzz", "cabbage", "cabin", "cable", "cactus", "cage", "cake", "call", "calm", "camera", "camp", "can", "canal", "cancel", "candy", "cannon", "canoe", "canvas", "canyon", "capable", "capital", "captain", "car", "carbon", "card", "cargo", "carpet", "carry", "cart", "case", "cash", "casino", "castle", "casual", "cat", "catalog", "catch", "category", "cattle", "caught", "cause", "caution", "cave", "ceiling", "celery", "cement", "census", "century", "cereal", "certain", "chair", "chalk", "champion", "change", "chaos", "chapter", "charge", "chase", "chat", "cheap", "check", "cheese", "chef", "cherry", "chest", "chicken", "chief", "child", "chimney", "choice", "choose", "chronic", "chuckle", "chunk", "churn", "cigar", "cinnamon", "circle", "citizen", "city", "civil", "claim", "clap", "clarify", "claw", "clay", "clean", "clerk", "clever", "click", "client", "cliff", "climb", "clinic", "clip", "clock", "clog", "close", "cloth", "cloud", "clown", "club", "clump", "cluster", "clutch", "coach", "coast", "coconut", "code", "coffee", "coil", "coin", "collect", "color", "column", "combine", "come", "comfort", "comic", "common", "company", "concert", "conduct", "confirm", "congress", "connect", "consider", "control", "convince", "cook", "cool", "copper", "copy", "coral", "core", "corn", "correct", "cost", "cotton", "couch", "country", "couple", "course", "cousin", "cover", "coyote", "crack", "cradle", "craft", "cram", "crane", "crash", "crater", "crawl", "crazy", "cream", "credit", "creek", "crew", "cricket", "crime", "crisp", "critic", "crop", "cross", "crouch", "crowd", "crucial", "cruel", "cruise", "crumble", "crunch", "crush", "cry", "crystal", "cube", "culture", "cup", "cupboard", "curious", "current", "curtain", "curve", "cushion", "custom", "cute", "cycle", "dad", "damage", "damp", "dance", "danger", "daring", "dash", "daughter", "dawn", "day", "deal", "debate", "debris", "decade", "december", "decide", "decline", "decorate", "decrease", "deer", "defense", "define", "defy", "degree", "delay", "deliver", "demand", "demise", "denial", "dentist", "deny", "depart", "depend", "deposit", "depth", "deputy", "derive", "describe", "desert", "design", "desk", "despair", "destroy", "detail", "detect", "develop", "device", "devote", "diagram", "dial", "diamond", "diary", "dice", "diesel", "diet", "differ", "digital", "dignity", "dilemma", "dinner", "dinosaur", "direct", "dirt", "disagree", "discover", "disease", "dish", "dismiss", "disorder", "display", "distance", "divert", "divide", "divorce", "dizzy", "doctor", "document", "dog", "doll", "dolphin", "domain", "donate", "donkey", "donor", "door", "dose", "double", "dove", "draft", "dragon", "drama", "drastic", "draw", "dream", "dress", "drift", "drill", "drink", "drip", "drive", "drop", "drum", "dry", "duck", "dumb", "dune", "during", "dust", "dutch", "duty", "dwarf", "dynamic", "eager", "eagle", "early", "earn", "earth", "easily", "east", "easy", "echo", "ecology", "economy", "edge", "edit", "educate", "effort", "egg", "eight", "either", "elbow", "elder", "electric", "elegant", "element", "elephant", "elevator", "elite", "else", "embark", "embody", "embrace", "emerge", "emotion", "employ", "empower", "empty", "enable", "enact", "end", "endless", "endorse", "enemy", "energy", "enforce", "engage", "engine", "enhance", "enjoy", "enlist", "enough", "enrich", "enroll", "ensure", "enter", "entire", "entry", "envelope", "episode", "equal", "equip", "era", "erase", "erode", "erosion", "error", "erupt", "escape", "essay", "essence", "estate", "eternal", "ethics", "evidence", "evil", "evoke", "evolve", "exact", "example", "excess", "exchange", "excite", "exclude", "excuse", "execute", "exercise", "exhaust", "exhibit", "exile", "exist", "exit", "exotic", "expand", "expect", "expire", "explain", "expose", "express", "extend", "extra", "eye", "eyebrow", "fabric", "face", "faculty", "fade", "faint", "faith", "fall", "false", "fame", "family", "famous", "fan", "fancy", "fantasy", "farm", "fashion", "fat", "fatal", "father", "fatigue", "fault", "favorite", "feature", "february", "federal", "fee", "feed", "feel", "female", "fence", "festival", "fetch", "fever", "few", "fiber", "fiction", "field", "figure", "file", "film", "filter", "final", "find", "fine", "finger", "finish", "fire", "firm", "first", "fiscal", "fish", "fit", "fitness", "fix", "flag", "flame", "flash", "flat", "flavor", "flee", "flight", "flip", "float", "flock", "floor", "flower", "fluid", "flush", "fly", "foam", "focus", "fog", "foil", "fold", "follow", "food", "foot", "force", "forest", "forget", "fork", "fortune", "forum", "forward", "fossil", "foster", "found", "fox", "fragile", "frame", "frequent", "fresh", "friend", "fringe", "frog", "front", "frost", "frown", "frozen", "fruit", "fuel", "fun", "funny", "furnace", "fury", "future", "gadget", "gain", "galaxy", "gallery", "game", "gap", "garage", "garbage", "garden", "garlic", "garment", "gas", "gasp", "gate", "gather", "gauge", "gaze", "general", "genius", "genre", "gentle", "genuine", "gesture", "ghost", "giant", "gift", "giggle", "ginger", "giraffe", "girl", "give", "glad", "glance", "glare", "glass", "glide", "glimpse", "globe", "gloom", "glory", "glove", "glow", "glue", "goat", "goddess", "gold", "good", "goose", "gorilla", "gospel", "gossip", "govern", "gown", "grab", "grace", "grain", "grant", "grape", "grass", "gravity", "great", "green", "grid", "grief", "grit", "grocery", "group", "grow", "grunt", "guard", "guess", "guide", "guilt", "guitar", "gun", "gym", "habit", "hair", "half", "hammer", "hamster", "hand", "happy", "harbor", "hard", "harsh", "harvest", "hat", "have", "hawk", "hazard", "head", "health", "heart", "heavy", "hedgehog", "height", "hello", "helmet", "help", "hen", "hero", "hidden", "high", "hill", "hint", "hip", "hire", "history", "hobby", "hockey", "hold", "hole", "holiday", "hollow", "home", "honey", "hood", "hope", "horn", "horror", "horse", "hospital", "host", "hotel", "hour", "hover", "hub", "huge", "human", "humble", "humor", "hundred", "hungry", "hunt", "hurdle", "hurry", "hurt", "husband", "hybrid", "ice", "icon", "idea", "identify", "idle", "ignore", "ill", "illegal", "illness", "image", "imitate", "immense", "immune", "impact", "impose", "improve", "impulse", "inch", "include", "income", "increase", "index", "indicate", "indoor", "industry", "infant", "inflict", "inform", "inhale", "inherit", "initial", "inject", "injury", "inmate", "inner", "innocent", "input", "inquiry", "insane", "insect", "inside", "inspire", "install", "intact", "interest", "into", "invest", "invite", "involve", "iron", "island", "isolate", "issue", "item", "ivory", "jacket", "jaguar", "jar", "jazz", "jealous", "jeans", "jelly", "jewel", "job", "join", "joke", "journey", "joy", "judge", "juice", "jump", "jungle", "junior", "junk", "just", "kangaroo", "keen", "keep", "ketchup", "key", "kick", "kid", "kidney", "kind", "kingdom", "kiss", "kit", "kitchen", "kite", "kitten", "kiwi", "knee", "knife", "knock", "know", "lab", "label", "labor", "ladder", "lady", "lake", "lamp", "language", "laptop", "large", "later", "latin", "laugh", "laundry", "lava", "law", "lawn", "lawsuit", "layer", "lazy", "leader", "leaf", "learn", "leave", "lecture", "left", "leg", "legal", "legend", "leisure", "lemon", "lend", "length", "lens", "leopard", "lesson", "letter", "level", "liar", "liberty", "library", "license", "life", "lift", "light", "like", "limb", "limit", "link", "lion", "liquid", "list", "little", "live", "lizard", "load", "loan", "lobster", "local", "lock", "logic", "lonely", "long", "loop", "lottery", "loud", "lounge", "love", "loyal", "lucky", "luggage", "lumber", "lunar", "lunch", "luxury", "lyrics", "machine", "mad", "magic", "magnet", "maid", "mail", "main", "major", "make", "mammal", "man", "manage", "mandate", "mango", "mansion", "manual", "maple", "marble", "march", "margin", "marine", "market", "marriage", "mask", "mass", "master", "match", "material", "math", "matrix", "matter", "maximum", "maze", "meadow", "mean", "measure", "meat", "mechanic", "medal", "media", "melody", "melt", "member", "memory", "mention", "menu", "mercy", "merge", "merit", "merry", "mesh", "message", "metal", "method", "middle", "midnight", "milk", "million", "mimic", "mind", "minimum", "minor", "minute", "miracle", "mirror", "misery", "miss", "mistake", "mix", "mixed", "mixture", "mobile", "model", "modify", "mom", "moment", "monitor", "monkey", "monster", "month", "moon", "moral", "more", "morning", "mosquito", "mother", "motion", "motor", "mountain", "mouse", "move", "movie", "much", "muffin", "mule", "multiply", "muscle", "museum", "mushroom", "music", "must", "mutual", "myself", "mystery", "myth", "naive", "name", "napkin", "narrow", "nasty", "nation", "nature", "near", "neck", "need", "negative", "neglect", "neither", "nephew", "nerve", "nest", "net", "network", "neutral", "never", "news", "next", "nice", "night", "noble", "noise", "nominee", "noodle", "normal", "north", "nose", "notable", "note", "nothing", "notice", "novel", "now", "nuclear", "number", "nurse", "nut", "oak", "obey", "object", "oblige", "obscure", "observe", "obtain", "obvious", "occur", "ocean", "october", "odor", "off", "offer", "office", "often", "oil", "okay", "old", "olive", "olympic", "omit", "once", "one", "onion", "online", "only", "open", "opera", "opinion", "oppose", "option", "orange", "orbit", "orchard", "order", "ordinary", "organ", "orient", "original", "orphan", "ostrich", "other", "outdoor", "outer", "output", "outside", "oval", "oven", "over", "own", "owner", "oxygen", "oyster", "ozone", "pact", "paddle", "page", "pair", "palace", "palm", "panda", "panel", "panic", "panther", "paper", "parade", "parent", "park", "parrot", "party", "pass", "patch", "path", "patient", "patrol", "pattern", "pause", "pave", "payment", "peace", "peanut", "pear", "peasant", "pelican", "pen", "penalty", "pencil", "people", "pepper", "perfect", "permit", "person", "pet", "phone", "photo", "phrase", "physical", "piano", "picnic", "picture", "piece", "pig", "pigeon", "pill", "pilot", "pink", "pioneer", "pipe", "pistol", "pitch", "pizza", "place", "planet", "plastic", "plate", "play", "please", "pledge", "pluck", "plug", "plunge", "poem", "poet", "point", "polar", "pole", "police", "pond", "pony", "pool", "popular", "portion", "position", "possible", "post", "potato", "pottery", "poverty", "powder", "power", "practice", "praise", "predict", "prefer", "prepare", "present", "pretty", "prevent", "price", "pride", "primary", "print", "priority", "prison", "private", "prize", "problem", "process", "produce", "profit", "program", "project", "promote", "proof", "property", "prosper", "protect", "proud", "provide", "public", "pudding", "pull", "pulp", "pulse", "pumpkin", "punch", "pupil", "puppy", "purchase", "purity", "purpose", "purse", "push", "put", "puzzle", "pyramid", "quality", "quantum", "quarter", "question", "quick", "quit", "quiz", "quote", "rabbit", "raccoon", "race", "rack", "radar", "radio", "rail", "rain", "raise", "rally", "ramp", "ranch", "random", "range", "rapid", "rare", "rate", "rather", "raven", "raw", "razor", "ready", "real", "reason", "rebel", "rebuild", "recall", "receive", "recipe", "record", "recycle", "reduce", "reflect", "reform", "refuse", "region", "regret", "regular", "reject", "relax", "release", "relief", "rely", "remain", "remember", "remind", "remove", "render", "renew", "rent", "reopen", "repair", "repeat", "replace", "report", "require", "rescue", "resemble", "resist", "resource", "response", "result", "retire", "retreat", "return", "reunion", "reveal", "review", "reward", "rhythm", "rib", "ribbon", "rice", "rich", "ride", "ridge", "rifle", "right", "rigid", "ring", "riot", "ripple", "risk", "ritual", "rival", "river", "road", "roast", "robot", "robust", "rocket", "romance", "roof", "rookie", "room", "rose", "rotate", "rough", "round", "route", "royal", "rubber", "rude", "rug", "rule", "run", "runway", "rural", "sad", "saddle", "sadness", "safe", "sail", "salad", "salmon", "salon", "salt", "salute", "same", "sample", "sand", "satisfy", "satoshi", "sauce", "sausage", "save", "say", "scale", "scan", "scare", "scatter", "scene", "scheme", "school", "science", "scissors", "scorpion", "scout", "scrap", "screen", "script", "scrub", "sea", "search", "season", "seat", "second", "secret", "section", "security", "seed", "seek", "segment", "select", "sell", "seminar", "senior", "sense", "sentence", "series", "service", "session", "settle", "setup", "seven", "shadow", "shaft", "shallow", "share", "shed", "shell", "sheriff", "shield", "shift", "shine", "ship", "shiver", "shock", "shoe", "shoot", "shop", "short", "shoulder", "shove", "shrimp", "shrug", "shuffle", "shy", "sibling", "sick", "side", "siege", "sight", "sign", "silent", "silk", "silly", "silver", "similar", "simple", "since", "sing", "siren", "sister", "situate", "six", "size", "skate", "sketch", "ski", "skill", "skin", "skirt", "skull", "slab", "slam", "sleep", "slender", "slice", "slide", "slight", "slim", "slogan", "slot", "slow", "slush", "small", "smart", "smile", "smoke", "smooth", "snack", "snake", "snap", "sniff", "snow", "soap", "soccer", "social", "sock", "soda", "soft", "solar", "soldier", "solid", "solution", "solve", "someone", "song", "soon", "sorry", "sort", "soul", "sound", "soup", "source", "south", "space", "spare", "spatial", "spawn", "speak", "special", "speed", "spell", "spend", "sphere", "spice", "spider", "spike", "spin", "spirit", "split", "spoil", "sponsor", "spoon", "sport", "spot", "spray", "spread", "spring", "spy", "square", "squeeze", "squirrel", "stable", "stadium", "staff", "stage", "stairs", "stamp", "stand", "start", "state", "stay", "steak", "steel", "stem", "step", "stereo", "stick", "still", "sting", "stock", "stomach", "stone", "stool", "story", "stove", "strategy", "street", "strike", "strong", "struggle", "student", "stuff", "stumble", "style", "subject", "submit", "subway", "success", "such", "sudden", "suffer", "sugar", "suggest", "suit", "summer", "sun", "sunny", "sunset", "super", "supply", "supreme", "sure", "surface", "surge", "surprise", "surround", "survey", "suspect", "sustain", "swallow", "swamp", "swap", "swarm", "swear", "sweet", "swift", "swim", "swing", "switch", "sword", "symbol", "symptom", "syrup", "system", "table", "tackle", "tag", "tail", "talent", "talk", "tank", "tape", "target", "task", "taste", "tattoo", "taxi", "teach", "team", "tell", "ten", "tenant", "tennis", "tent", "term", "test", "text", "thank", "that", "theme", "then", "theory", "there", "they", "thing", "this", "thought", "three", "thrive", "throw", "thumb", "thunder", "ticket", "tide", "tiger", "tilt", "timber", "time", "tiny", "tip", "tired", "tissue", "title", "toast", "tobacco", "today", "toddler", "toe", "together", "toilet", "token", "tomato", "tomorrow", "tone", "tongue", "tonight", "tool", "tooth", "top", "topic", "topple", "torch", "tornado", "tortoise", "toss", "total", "tourist", "toward", "tower", "town", "toy", "track", "trade", "traffic", "tragic", "train", "transfer", "trap", "trash", "travel", "tray", "treat", "tree", "trend", "trial", "tribe", "trick", "trigger", "trim", "trip", "trophy", "trouble", "truck", "true", "truly", "trumpet", "trust", "truth", "try", "tube", "tuition", "tumble", "tuna", "tunnel", "turkey", "turn", "turtle", "twelve", "twenty", "twice", "twin", "twist", "two", "type", "typical", "ugly", "umbrella", "unable", "unaware", "uncle", "uncover", "under", "undo", "unfair", "unfold", "unhappy", "uniform", "unique", "unit", "universe", "unknown", "unlock", "until", "unusual", "unveil", "update", "upgrade", "uphold", "upon", "upper", "upset", "urban", "urge", "usage", "use", "used", "useful", "useless", "usual", "utility", "vacant", "vacuum", "vague", "valid", "valley", "valve", "van", "vanish", "vapor", "various", "vast", "vault", "vehicle", "velvet", "vendor", "venture", "venue", "verb", "verify", "version", "very", "vessel", "veteran", "viable", "vibrant", "vicious", "victory", "video", "view", "village", "vintage", "violin", "virtual", "virus", "visa", "visit", "visual", "vital", "vivid", "vocal", "voice", "void", "volcano", "volume", "vote", "voyage", "wage", "wagon", "wait", "walk", "wall", "walnut", "want", "warfare", "warm", "warrior", "wash", "wasp", "waste", "water", "wave", "way", "wealth", "weapon", "wear", "weasel", "weather", "web", "wedding", "weekend", "weird", "welcome", "west", "wet", "whale", "what", "wheat", "wheel", "when", "where", "whip", "whisper", "wide", "width", "wife", "wild", "will", "win", "window", "wine", "wing", "wink", "winner", "winter", "wire", "wisdom", "wise", "wish", "witness", "wolf", "woman", "wonder", "wood", "wool", "word", "work", "world", "worry", "worth", "wrap", "wreck", "wrestle", "wrist", "write", "wrong", "yard", "year", "yellow", "you", "young", "youth", "zebra", "zero", "zone", "zoo"] +const chineseSimplifiedWords*: seq[string] = @["的", "一", "是", "在", "不", "了", "有", "和", "人", "这", "中", "大", "为", "上", "个", "国", "我", "以", "要", "他", "时", "来", "用", "们", "生", "到", "作", "地", "于", "出", "就", "分", "对", "成", "会", "可", "主", "发", "年", "动", "同", "工", "也", "能", "下", "过", "子", "说", "产", "种", "面", "而", "方", "后", "多", "定", "行", "学", "法", "所", "民", "得", "经", "十", "三", "之", "进", "着", "等", "部", "度", "家", "电", "力", "里", "如", "水", "化", "高", "自", "二", "理", "起", "小", "物", "现", "实", "加", "量", "都", "两", "体", "制", "机", "当", "使", "点", "从", "业", "本", "去", "把", "性", "好", "应", "开", "它", "合", "还", "因", "由", "其", "些", "然", "前", "外", "天", "政", "四", "日", "那", "社", "义", "事", "平", "形", "相", "全", "表", "间", "样", "与", "关", "各", "重", "新", "线", "内", "数", "正", "心", "反", "你", "明", "看", "原", "又", "么", "利", "比", "或", "但", "质", "气", "第", "向", "道", "命", "此", "变", "条", "只", "没", "结", "解", "问", "意", "建", "月", "公", "无", "系", "军", "很", "情", "者", "最", "立", "代", "想", "已", "通", "并", "提", "直", "题", "党", "程", "展", "五", "果", "料", "象", "员", "革", "位", "入", "常", "文", "总", "次", "品", "式", "活", "设", "及", "管", "特", "件", "长", "求", "老", "头", "基", "资", "边", "流", "路", "级", "少", "图", "山", "统", "接", "知", "较", "将", "组", "见", "计", "别", "她", "手", "角", "期", "根", "论", "运", "农", "指", "几", "九", "区", "强", "放", "决", "西", "被", "干", "做", "必", "战", "先", "回", "则", "任", "取", "据", "处", "队", "南", "给", "色", "光", "门", "即", "保", "治", "北", "造", "百", "规", "热", "领", "七", "海", "口", "东", "导", "器", "压", "志", "世", "金", "增", "争", "济", "阶", "油", "思", "术", "极", "交", "受", "联", "什", "认", "六", "共", "权", "收", "证", "改", "清", "美", "再", "采", "转", "更", "单", "风", "切", "打", "白", "教", "速", "花", "带", "安", "场", "身", "车", "例", "真", "务", "具", "万", "每", "目", "至", "达", "走", "积", "示", "议", "声", "报", "斗", "完", "类", "八", "离", "华", "名", "确", "才", "科", "张", "信", "马", "节", "话", "米", "整", "空", "元", "况", "今", "集", "温", "传", "土", "许", "步", "群", "广", "石", "记", "需", "段", "研", "界", "拉", "林", "律", "叫", "且", "究", "观", "越", "织", "装", "影", "算", "低", "持", "音", "众", "书", "布", "复", "容", "儿", "须", "际", "商", "非", "验", "连", "断", "深", "难", "近", "矿", "千", "周", "委", "素", "技", "备", "半", "办", "青", "省", "列", "习", "响", "约", "支", "般", "史", "感", "劳", "便", "团", "往", "酸", "历", "市", "克", "何", "除", "消", "构", "府", "称", "太", "准", "精", "值", "号", "率", "族", "维", "划", "选", "标", "写", "存", "候", "毛", "亲", "快", "效", "斯", "院", "查", "江", "型", "眼", "王", "按", "格", "养", "易", "置", "派", "层", "片", "始", "却", "专", "状", "育", "厂", "京", "识", "适", "属", "圆", "包", "火", "住", "调", "满", "县", "局", "照", "参", "红", "细", "引", "听", "该", "铁", "价", "严", "首", "底", "液", "官", "德", "随", "病", "苏", "失", "尔", "死", "讲", "配", "女", "黄", "推", "显", "谈", "罪", "神", "艺", "呢", "席", "含", "企", "望", "密", "批", "营", "项", "防", "举", "球", "英", "氧", "势", "告", "李", "台", "落", "木", "帮", "轮", "破", "亚", "师", "围", "注", "远", "字", "材", "排", "供", "河", "态", "封", "另", "施", "减", "树", "溶", "怎", "止", "案", "言", "士", "均", "武", "固", "叶", "鱼", "波", "视", "仅", "费", "紧", "爱", "左", "章", "早", "朝", "害", "续", "轻", "服", "试", "食", "充", "兵", "源", "判", "护", "司", "足", "某", "练", "差", "致", "板", "田", "降", "黑", "犯", "负", "击", "范", "继", "兴", "似", "余", "坚", "曲", "输", "修", "故", "城", "夫", "够", "送", "笔", "船", "占", "右", "财", "吃", "富", "春", "职", "觉", "汉", "画", "功", "巴", "跟", "虽", "杂", "飞", "检", "吸", "助", "升", "阳", "互", "初", "创", "抗", "考", "投", "坏", "策", "古", "径", "换", "未", "跑", "留", "钢", "曾", "端", "责", "站", "简", "述", "钱", "副", "尽", "帝", "射", "草", "冲", "承", "独", "令", "限", "阿", "宣", "环", "双", "请", "超", "微", "让", "控", "州", "良", "轴", "找", "否", "纪", "益", "依", "优", "顶", "础", "载", "倒", "房", "突", "坐", "粉", "敌", "略", "客", "袁", "冷", "胜", "绝", "析", "块", "剂", "测", "丝", "协", "诉", "念", "陈", "仍", "罗", "盐", "友", "洋", "错", "苦", "夜", "刑", "移", "频", "逐", "靠", "混", "母", "短", "皮", "终", "聚", "汽", "村", "云", "哪", "既", "距", "卫", "停", "烈", "央", "察", "烧", "迅", "境", "若", "印", "洲", "刻", "括", "激", "孔", "搞", "甚", "室", "待", "核", "校", "散", "侵", "吧", "甲", "游", "久", "菜", "味", "旧", "模", "湖", "货", "损", "预", "阻", "毫", "普", "稳", "乙", "妈", "植", "息", "扩", "银", "语", "挥", "酒", "守", "拿", "序", "纸", "医", "缺", "雨", "吗", "针", "刘", "啊", "急", "唱", "误", "训", "愿", "审", "附", "获", "茶", "鲜", "粮", "斤", "孩", "脱", "硫", "肥", "善", "龙", "演", "父", "渐", "血", "欢", "械", "掌", "歌", "沙", "刚", "攻", "谓", "盾", "讨", "晚", "粒", "乱", "燃", "矛", "乎", "杀", "药", "宁", "鲁", "贵", "钟", "煤", "读", "班", "伯", "香", "介", "迫", "句", "丰", "培", "握", "兰", "担", "弦", "蛋", "沉", "假", "穿", "执", "答", "乐", "谁", "顺", "烟", "缩", "征", "脸", "喜", "松", "脚", "困", "异", "免", "背", "星", "福", "买", "染", "井", "概", "慢", "怕", "磁", "倍", "祖", "皇", "促", "静", "补", "评", "翻", "肉", "践", "尼", "衣", "宽", "扬", "棉", "希", "伤", "操", "垂", "秋", "宜", "氢", "套", "督", "振", "架", "亮", "末", "宪", "庆", "编", "牛", "触", "映", "雷", "销", "诗", "座", "居", "抓", "裂", "胞", "呼", "娘", "景", "威", "绿", "晶", "厚", "盟", "衡", "鸡", "孙", "延", "危", "胶", "屋", "乡", "临", "陆", "顾", "掉", "呀", "灯", "岁", "措", "束", "耐", "剧", "玉", "赵", "跳", "哥", "季", "课", "凯", "胡", "额", "款", "绍", "卷", "齐", "伟", "蒸", "殖", "永", "宗", "苗", "川", "炉", "岩", "弱", "零", "杨", "奏", "沿", "露", "杆", "探", "滑", "镇", "饭", "浓", "航", "怀", "赶", "库", "夺", "伊", "灵", "税", "途", "灭", "赛", "归", "召", "鼓", "播", "盘", "裁", "险", "康", "唯", "录", "菌", "纯", "借", "糖", "盖", "横", "符", "私", "努", "堂", "域", "枪", "润", "幅", "哈", "竟", "熟", "虫", "泽", "脑", "壤", "碳", "欧", "遍", "侧", "寨", "敢", "彻", "虑", "斜", "薄", "庭", "纳", "弹", "饲", "伸", "折", "麦", "湿", "暗", "荷", "瓦", "塞", "床", "筑", "恶", "户", "访", "塔", "奇", "透", "梁", "刀", "旋", "迹", "卡", "氯", "遇", "份", "毒", "泥", "退", "洗", "摆", "灰", "彩", "卖", "耗", "夏", "择", "忙", "铜", "献", "硬", "予", "繁", "圈", "雪", "函", "亦", "抽", "篇", "阵", "阴", "丁", "尺", "追", "堆", "雄", "迎", "泛", "爸", "楼", "避", "谋", "吨", "野", "猪", "旗", "累", "偏", "典", "馆", "索", "秦", "脂", "潮", "爷", "豆", "忽", "托", "惊", "塑", "遗", "愈", "朱", "替", "纤", "粗", "倾", "尚", "痛", "楚", "谢", "奋", "购", "磨", "君", "池", "旁", "碎", "骨", "监", "捕", "弟", "暴", "割", "贯", "殊", "释", "词", "亡", "壁", "顿", "宝", "午", "尘", "闻", "揭", "炮", "残", "冬", "桥", "妇", "警", "综", "招", "吴", "付", "浮", "遭", "徐", "您", "摇", "谷", "赞", "箱", "隔", "订", "男", "吹", "园", "纷", "唐", "败", "宋", "玻", "巨", "耕", "坦", "荣", "闭", "湾", "键", "凡", "驻", "锅", "救", "恩", "剥", "凝", "碱", "齿", "截", "炼", "麻", "纺", "禁", "废", "盛", "版", "缓", "净", "睛", "昌", "婚", "涉", "筒", "嘴", "插", "岸", "朗", "庄", "街", "藏", "姑", "贸", "腐", "奴", "啦", "惯", "乘", "伙", "恢", "匀", "纱", "扎", "辩", "耳", "彪", "臣", "亿", "璃", "抵", "脉", "秀", "萨", "俄", "网", "舞", "店", "喷", "纵", "寸", "汗", "挂", "洪", "贺", "闪", "柬", "爆", "烯", "津", "稻", "墙", "软", "勇", "像", "滚", "厘", "蒙", "芳", "肯", "坡", "柱", "荡", "腿", "仪", "旅", "尾", "轧", "冰", "贡", "登", "黎", "削", "钻", "勒", "逃", "障", "氨", "郭", "峰", "币", "港", "伏", "轨", "亩", "毕", "擦", "莫", "刺", "浪", "秘", "援", "株", "健", "售", "股", "岛", "甘", "泡", "睡", "童", "铸", "汤", "阀", "休", "汇", "舍", "牧", "绕", "炸", "哲", "磷", "绩", "朋", "淡", "尖", "启", "陷", "柴", "呈", "徒", "颜", "泪", "稍", "忘", "泵", "蓝", "拖", "洞", "授", "镜", "辛", "壮", "锋", "贫", "虚", "弯", "摩", "泰", "幼", "廷", "尊", "窗", "纲", "弄", "隶", "疑", "氏", "宫", "姐", "震", "瑞", "怪", "尤", "琴", "循", "描", "膜", "违", "夹", "腰", "缘", "珠", "穷", "森", "枝", "竹", "沟", "催", "绳", "忆", "邦", "剩", "幸", "浆", "栏", "拥", "牙", "贮", "礼", "滤", "钠", "纹", "罢", "拍", "咱", "喊", "袖", "埃", "勤", "罚", "焦", "潜", "伍", "墨", "欲", "缝", "姓", "刊", "饱", "仿", "奖", "铝", "鬼", "丽", "跨", "默", "挖", "链", "扫", "喝", "袋", "炭", "污", "幕", "诸", "弧", "励", "梅", "奶", "洁", "灾", "舟", "鉴", "苯", "讼", "抱", "毁", "懂", "寒", "智", "埔", "寄", "届", "跃", "渡", "挑", "丹", "艰", "贝", "碰", "拔", "爹", "戴", "码", "梦", "芽", "熔", "赤", "渔", "哭", "敬", "颗", "奔", "铅", "仲", "虎", "稀", "妹", "乏", "珍", "申", "桌", "遵", "允", "隆", "螺", "仓", "魏", "锐", "晓", "氮", "兼", "隐", "碍", "赫", "拨", "忠", "肃", "缸", "牵", "抢", "博", "巧", "壳", "兄", "杜", "讯", "诚", "碧", "祥", "柯", "页", "巡", "矩", "悲", "灌", "龄", "伦", "票", "寻", "桂", "铺", "圣", "恐", "恰", "郑", "趣", "抬", "荒", "腾", "贴", "柔", "滴", "猛", "阔", "辆", "妻", "填", "撤", "储", "签", "闹", "扰", "紫", "砂", "递", "戏", "吊", "陶", "伐", "喂", "疗", "瓶", "婆", "抚", "臂", "摸", "忍", "虾", "蜡", "邻", "胸", "巩", "挤", "偶", "弃", "槽", "劲", "乳", "邓", "吉", "仁", "烂", "砖", "租", "乌", "舰", "伴", "瓜", "浅", "丙", "暂", "燥", "橡", "柳", "迷", "暖", "牌", "秧", "胆", "详", "簧", "踏", "瓷", "谱", "呆", "宾", "糊", "洛", "辉", "愤", "竞", "隙", "怒", "粘", "乃", "绪", "肩", "籍", "敏", "涂", "熙", "皆", "侦", "悬", "掘", "享", "纠", "醒", "狂", "锁", "淀", "恨", "牲", "霸", "爬", "赏", "逆", "玩", "陵", "祝", "秒", "浙", "貌", "役", "彼", "悉", "鸭", "趋", "凤", "晨", "畜", "辈", "秩", "卵", "署", "梯", "炎", "滩", "棋", "驱", "筛", "峡", "冒", "啥", "寿", "译", "浸", "泉", "帽", "迟", "硅", "疆", "贷", "漏", "稿", "冠", "嫩", "胁", "芯", "牢", "叛", "蚀", "奥", "鸣", "岭", "羊", "凭", "串", "塘", "绘", "酵", "融", "盆", "锡", "庙", "筹", "冻", "辅", "摄", "袭", "筋", "拒", "僚", "旱", "钾", "鸟", "漆", "沈", "眉", "疏", "添", "棒", "穗", "硝", "韩", "逼", "扭", "侨", "凉", "挺", "碗", "栽", "炒", "杯", "患", "馏", "劝", "豪", "辽", "勃", "鸿", "旦", "吏", "拜", "狗", "埋", "辊", "掩", "饮", "搬", "骂", "辞", "勾", "扣", "估", "蒋", "绒", "雾", "丈", "朵", "姆", "拟", "宇", "辑", "陕", "雕", "偿", "蓄", "崇", "剪", "倡", "厅", "咬", "驶", "薯", "刷", "斥", "番", "赋", "奉", "佛", "浇", "漫", "曼", "扇", "钙", "桃", "扶", "仔", "返", "俗", "亏", "腔", "鞋", "棱", "覆", "框", "悄", "叔", "撞", "骗", "勘", "旺", "沸", "孤", "吐", "孟", "渠", "屈", "疾", "妙", "惜", "仰", "狠", "胀", "谐", "抛", "霉", "桑", "岗", "嘛", "衰", "盗", "渗", "脏", "赖", "涌", "甜", "曹", "阅", "肌", "哩", "厉", "烃", "纬", "毅", "昨", "伪", "症", "煮", "叹", "钉", "搭", "茎", "笼", "酷", "偷", "弓", "锥", "恒", "杰", "坑", "鼻", "翼", "纶", "叙", "狱", "逮", "罐", "络", "棚", "抑", "膨", "蔬", "寺", "骤", "穆", "冶", "枯", "册", "尸", "凸", "绅", "坯", "牺", "焰", "轰", "欣", "晋", "瘦", "御", "锭", "锦", "丧", "旬", "锻", "垄", "搜", "扑", "邀", "亭", "酯", "迈", "舒", "脆", "酶", "闲", "忧", "酚", "顽", "羽", "涨", "卸", "仗", "陪", "辟", "惩", "杭", "姚", "肚", "捉", "飘", "漂", "昆", "欺", "吾", "郎", "烷", "汁", "呵", "饰", "萧", "雅", "邮", "迁", "燕", "撒", "姻", "赴", "宴", "烦", "债", "帐", "斑", "铃", "旨", "醇", "董", "饼", "雏", "姿", "拌", "傅", "腹", "妥", "揉", "贤", "拆", "歪", "葡", "胺", "丢", "浩", "徽", "昂", "垫", "挡", "览", "贪", "慰", "缴", "汪", "慌", "冯", "诺", "姜", "谊", "凶", "劣", "诬", "耀", "昏", "躺", "盈", "骑", "乔", "溪", "丛", "卢", "抹", "闷", "咨", "刮", "驾", "缆", "悟", "摘", "铒", "掷", "颇", "幻", "柄", "惠", "惨", "佳", "仇", "腊", "窝", "涤", "剑", "瞧", "堡", "泼", "葱", "罩", "霍", "捞", "胎", "苍", "滨", "俩", "捅", "湘", "砍", "霞", "邵", "萄", "疯", "淮", "遂", "熊", "粪", "烘", "宿", "档", "戈", "驳", "嫂", "裕", "徙", "箭", "捐", "肠", "撑", "晒", "辨", "殿", "莲", "摊", "搅", "酱", "屏", "疫", "哀", "蔡", "堵", "沫", "皱", "畅", "叠", "阁", "莱", "敲", "辖", "钩", "痕", "坝", "巷", "饿", "祸", "丘", "玄", "溜", "曰", "逻", "彭", "尝", "卿", "妨", "艇", "吞", "韦", "怨", "矮", "歇"] +const chineseTraditionalWords*: seq[string] = @["的", "一", "是", "在", "不", "了", "有", "和", "人", "這", "中", "大", "為", "上", "個", "國", "我", "以", "要", "他", "時", "來", "用", "們", "生", "到", "作", "地", "於", "出", "就", "分", "對", "成", "會", "可", "主", "發", "年", "動", "同", "工", "也", "能", "下", "過", "子", "說", "產", "種", "面", "而", "方", "後", "多", "定", "行", "學", "法", "所", "民", "得", "經", "十", "三", "之", "進", "著", "等", "部", "度", "家", "電", "力", "裡", "如", "水", "化", "高", "自", "二", "理", "起", "小", "物", "現", "實", "加", "量", "都", "兩", "體", "制", "機", "當", "使", "點", "從", "業", "本", "去", "把", "性", "好", "應", "開", "它", "合", "還", "因", "由", "其", "些", "然", "前", "外", "天", "政", "四", "日", "那", "社", "義", "事", "平", "形", "相", "全", "表", "間", "樣", "與", "關", "各", "重", "新", "線", "內", "數", "正", "心", "反", "你", "明", "看", "原", "又", "麼", "利", "比", "或", "但", "質", "氣", "第", "向", "道", "命", "此", "變", "條", "只", "沒", "結", "解", "問", "意", "建", "月", "公", "無", "系", "軍", "很", "情", "者", "最", "立", "代", "想", "已", "通", "並", "提", "直", "題", "黨", "程", "展", "五", "果", "料", "象", "員", "革", "位", "入", "常", "文", "總", "次", "品", "式", "活", "設", "及", "管", "特", "件", "長", "求", "老", "頭", "基", "資", "邊", "流", "路", "級", "少", "圖", "山", "統", "接", "知", "較", "將", "組", "見", "計", "別", "她", "手", "角", "期", "根", "論", "運", "農", "指", "幾", "九", "區", "強", "放", "決", "西", "被", "幹", "做", "必", "戰", "先", "回", "則", "任", "取", "據", "處", "隊", "南", "給", "色", "光", "門", "即", "保", "治", "北", "造", "百", "規", "熱", "領", "七", "海", "口", "東", "導", "器", "壓", "志", "世", "金", "增", "爭", "濟", "階", "油", "思", "術", "極", "交", "受", "聯", "什", "認", "六", "共", "權", "收", "證", "改", "清", "美", "再", "採", "轉", "更", "單", "風", "切", "打", "白", "教", "速", "花", "帶", "安", "場", "身", "車", "例", "真", "務", "具", "萬", "每", "目", "至", "達", "走", "積", "示", "議", "聲", "報", "鬥", "完", "類", "八", "離", "華", "名", "確", "才", "科", "張", "信", "馬", "節", "話", "米", "整", "空", "元", "況", "今", "集", "溫", "傳", "土", "許", "步", "群", "廣", "石", "記", "需", "段", "研", "界", "拉", "林", "律", "叫", "且", "究", "觀", "越", "織", "裝", "影", "算", "低", "持", "音", "眾", "書", "布", "复", "容", "兒", "須", "際", "商", "非", "驗", "連", "斷", "深", "難", "近", "礦", "千", "週", "委", "素", "技", "備", "半", "辦", "青", "省", "列", "習", "響", "約", "支", "般", "史", "感", "勞", "便", "團", "往", "酸", "歷", "市", "克", "何", "除", "消", "構", "府", "稱", "太", "準", "精", "值", "號", "率", "族", "維", "劃", "選", "標", "寫", "存", "候", "毛", "親", "快", "效", "斯", "院", "查", "江", "型", "眼", "王", "按", "格", "養", "易", "置", "派", "層", "片", "始", "卻", "專", "狀", "育", "廠", "京", "識", "適", "屬", "圓", "包", "火", "住", "調", "滿", "縣", "局", "照", "參", "紅", "細", "引", "聽", "該", "鐵", "價", "嚴", "首", "底", "液", "官", "德", "隨", "病", "蘇", "失", "爾", "死", "講", "配", "女", "黃", "推", "顯", "談", "罪", "神", "藝", "呢", "席", "含", "企", "望", "密", "批", "營", "項", "防", "舉", "球", "英", "氧", "勢", "告", "李", "台", "落", "木", "幫", "輪", "破", "亞", "師", "圍", "注", "遠", "字", "材", "排", "供", "河", "態", "封", "另", "施", "減", "樹", "溶", "怎", "止", "案", "言", "士", "均", "武", "固", "葉", "魚", "波", "視", "僅", "費", "緊", "愛", "左", "章", "早", "朝", "害", "續", "輕", "服", "試", "食", "充", "兵", "源", "判", "護", "司", "足", "某", "練", "差", "致", "板", "田", "降", "黑", "犯", "負", "擊", "范", "繼", "興", "似", "餘", "堅", "曲", "輸", "修", "故", "城", "夫", "夠", "送", "筆", "船", "佔", "右", "財", "吃", "富", "春", "職", "覺", "漢", "畫", "功", "巴", "跟", "雖", "雜", "飛", "檢", "吸", "助", "昇", "陽", "互", "初", "創", "抗", "考", "投", "壞", "策", "古", "徑", "換", "未", "跑", "留", "鋼", "曾", "端", "責", "站", "簡", "述", "錢", "副", "盡", "帝", "射", "草", "衝", "承", "獨", "令", "限", "阿", "宣", "環", "雙", "請", "超", "微", "讓", "控", "州", "良", "軸", "找", "否", "紀", "益", "依", "優", "頂", "礎", "載", "倒", "房", "突", "坐", "粉", "敵", "略", "客", "袁", "冷", "勝", "絕", "析", "塊", "劑", "測", "絲", "協", "訴", "念", "陳", "仍", "羅", "鹽", "友", "洋", "錯", "苦", "夜", "刑", "移", "頻", "逐", "靠", "混", "母", "短", "皮", "終", "聚", "汽", "村", "雲", "哪", "既", "距", "衛", "停", "烈", "央", "察", "燒", "迅", "境", "若", "印", "洲", "刻", "括", "激", "孔", "搞", "甚", "室", "待", "核", "校", "散", "侵", "吧", "甲", "遊", "久", "菜", "味", "舊", "模", "湖", "貨", "損", "預", "阻", "毫", "普", "穩", "乙", "媽", "植", "息", "擴", "銀", "語", "揮", "酒", "守", "拿", "序", "紙", "醫", "缺", "雨", "嗎", "針", "劉", "啊", "急", "唱", "誤", "訓", "願", "審", "附", "獲", "茶", "鮮", "糧", "斤", "孩", "脫", "硫", "肥", "善", "龍", "演", "父", "漸", "血", "歡", "械", "掌", "歌", "沙", "剛", "攻", "謂", "盾", "討", "晚", "粒", "亂", "燃", "矛", "乎", "殺", "藥", "寧", "魯", "貴", "鐘", "煤", "讀", "班", "伯", "香", "介", "迫", "句", "豐", "培", "握", "蘭", "擔", "弦", "蛋", "沉", "假", "穿", "執", "答", "樂", "誰", "順", "煙", "縮", "徵", "臉", "喜", "松", "腳", "困", "異", "免", "背", "星", "福", "買", "染", "井", "概", "慢", "怕", "磁", "倍", "祖", "皇", "促", "靜", "補", "評", "翻", "肉", "踐", "尼", "衣", "寬", "揚", "棉", "希", "傷", "操", "垂", "秋", "宜", "氫", "套", "督", "振", "架", "亮", "末", "憲", "慶", "編", "牛", "觸", "映", "雷", "銷", "詩", "座", "居", "抓", "裂", "胞", "呼", "娘", "景", "威", "綠", "晶", "厚", "盟", "衡", "雞", "孫", "延", "危", "膠", "屋", "鄉", "臨", "陸", "顧", "掉", "呀", "燈", "歲", "措", "束", "耐", "劇", "玉", "趙", "跳", "哥", "季", "課", "凱", "胡", "額", "款", "紹", "卷", "齊", "偉", "蒸", "殖", "永", "宗", "苗", "川", "爐", "岩", "弱", "零", "楊", "奏", "沿", "露", "桿", "探", "滑", "鎮", "飯", "濃", "航", "懷", "趕", "庫", "奪", "伊", "靈", "稅", "途", "滅", "賽", "歸", "召", "鼓", "播", "盤", "裁", "險", "康", "唯", "錄", "菌", "純", "借", "糖", "蓋", "橫", "符", "私", "努", "堂", "域", "槍", "潤", "幅", "哈", "竟", "熟", "蟲", "澤", "腦", "壤", "碳", "歐", "遍", "側", "寨", "敢", "徹", "慮", "斜", "薄", "庭", "納", "彈", "飼", "伸", "折", "麥", "濕", "暗", "荷", "瓦", "塞", "床", "築", "惡", "戶", "訪", "塔", "奇", "透", "梁", "刀", "旋", "跡", "卡", "氯", "遇", "份", "毒", "泥", "退", "洗", "擺", "灰", "彩", "賣", "耗", "夏", "擇", "忙", "銅", "獻", "硬", "予", "繁", "圈", "雪", "函", "亦", "抽", "篇", "陣", "陰", "丁", "尺", "追", "堆", "雄", "迎", "泛", "爸", "樓", "避", "謀", "噸", "野", "豬", "旗", "累", "偏", "典", "館", "索", "秦", "脂", "潮", "爺", "豆", "忽", "托", "驚", "塑", "遺", "愈", "朱", "替", "纖", "粗", "傾", "尚", "痛", "楚", "謝", "奮", "購", "磨", "君", "池", "旁", "碎", "骨", "監", "捕", "弟", "暴", "割", "貫", "殊", "釋", "詞", "亡", "壁", "頓", "寶", "午", "塵", "聞", "揭", "炮", "殘", "冬", "橋", "婦", "警", "綜", "招", "吳", "付", "浮", "遭", "徐", "您", "搖", "谷", "贊", "箱", "隔", "訂", "男", "吹", "園", "紛", "唐", "敗", "宋", "玻", "巨", "耕", "坦", "榮", "閉", "灣", "鍵", "凡", "駐", "鍋", "救", "恩", "剝", "凝", "鹼", "齒", "截", "煉", "麻", "紡", "禁", "廢", "盛", "版", "緩", "淨", "睛", "昌", "婚", "涉", "筒", "嘴", "插", "岸", "朗", "莊", "街", "藏", "姑", "貿", "腐", "奴", "啦", "慣", "乘", "夥", "恢", "勻", "紗", "扎", "辯", "耳", "彪", "臣", "億", "璃", "抵", "脈", "秀", "薩", "俄", "網", "舞", "店", "噴", "縱", "寸", "汗", "掛", "洪", "賀", "閃", "柬", "爆", "烯", "津", "稻", "牆", "軟", "勇", "像", "滾", "厘", "蒙", "芳", "肯", "坡", "柱", "盪", "腿", "儀", "旅", "尾", "軋", "冰", "貢", "登", "黎", "削", "鑽", "勒", "逃", "障", "氨", "郭", "峰", "幣", "港", "伏", "軌", "畝", "畢", "擦", "莫", "刺", "浪", "秘", "援", "株", "健", "售", "股", "島", "甘", "泡", "睡", "童", "鑄", "湯", "閥", "休", "匯", "舍", "牧", "繞", "炸", "哲", "磷", "績", "朋", "淡", "尖", "啟", "陷", "柴", "呈", "徒", "顏", "淚", "稍", "忘", "泵", "藍", "拖", "洞", "授", "鏡", "辛", "壯", "鋒", "貧", "虛", "彎", "摩", "泰", "幼", "廷", "尊", "窗", "綱", "弄", "隸", "疑", "氏", "宮", "姐", "震", "瑞", "怪", "尤", "琴", "循", "描", "膜", "違", "夾", "腰", "緣", "珠", "窮", "森", "枝", "竹", "溝", "催", "繩", "憶", "邦", "剩", "幸", "漿", "欄", "擁", "牙", "貯", "禮", "濾", "鈉", "紋", "罷", "拍", "咱", "喊", "袖", "埃", "勤", "罰", "焦", "潛", "伍", "墨", "欲", "縫", "姓", "刊", "飽", "仿", "獎", "鋁", "鬼", "麗", "跨", "默", "挖", "鏈", "掃", "喝", "袋", "炭", "污", "幕", "諸", "弧", "勵", "梅", "奶", "潔", "災", "舟", "鑑", "苯", "訟", "抱", "毀", "懂", "寒", "智", "埔", "寄", "屆", "躍", "渡", "挑", "丹", "艱", "貝", "碰", "拔", "爹", "戴", "碼", "夢", "芽", "熔", "赤", "漁", "哭", "敬", "顆", "奔", "鉛", "仲", "虎", "稀", "妹", "乏", "珍", "申", "桌", "遵", "允", "隆", "螺", "倉", "魏", "銳", "曉", "氮", "兼", "隱", "礙", "赫", "撥", "忠", "肅", "缸", "牽", "搶", "博", "巧", "殼", "兄", "杜", "訊", "誠", "碧", "祥", "柯", "頁", "巡", "矩", "悲", "灌", "齡", "倫", "票", "尋", "桂", "鋪", "聖", "恐", "恰", "鄭", "趣", "抬", "荒", "騰", "貼", "柔", "滴", "猛", "闊", "輛", "妻", "填", "撤", "儲", "簽", "鬧", "擾", "紫", "砂", "遞", "戲", "吊", "陶", "伐", "餵", "療", "瓶", "婆", "撫", "臂", "摸", "忍", "蝦", "蠟", "鄰", "胸", "鞏", "擠", "偶", "棄", "槽", "勁", "乳", "鄧", "吉", "仁", "爛", "磚", "租", "烏", "艦", "伴", "瓜", "淺", "丙", "暫", "燥", "橡", "柳", "迷", "暖", "牌", "秧", "膽", "詳", "簧", "踏", "瓷", "譜", "呆", "賓", "糊", "洛", "輝", "憤", "競", "隙", "怒", "粘", "乃", "緒", "肩", "籍", "敏", "塗", "熙", "皆", "偵", "懸", "掘", "享", "糾", "醒", "狂", "鎖", "淀", "恨", "牲", "霸", "爬", "賞", "逆", "玩", "陵", "祝", "秒", "浙", "貌", "役", "彼", "悉", "鴨", "趨", "鳳", "晨", "畜", "輩", "秩", "卵", "署", "梯", "炎", "灘", "棋", "驅", "篩", "峽", "冒", "啥", "壽", "譯", "浸", "泉", "帽", "遲", "矽", "疆", "貸", "漏", "稿", "冠", "嫩", "脅", "芯", "牢", "叛", "蝕", "奧", "鳴", "嶺", "羊", "憑", "串", "塘", "繪", "酵", "融", "盆", "錫", "廟", "籌", "凍", "輔", "攝", "襲", "筋", "拒", "僚", "旱", "鉀", "鳥", "漆", "沈", "眉", "疏", "添", "棒", "穗", "硝", "韓", "逼", "扭", "僑", "涼", "挺", "碗", "栽", "炒", "杯", "患", "餾", "勸", "豪", "遼", "勃", "鴻", "旦", "吏", "拜", "狗", "埋", "輥", "掩", "飲", "搬", "罵", "辭", "勾", "扣", "估", "蔣", "絨", "霧", "丈", "朵", "姆", "擬", "宇", "輯", "陝", "雕", "償", "蓄", "崇", "剪", "倡", "廳", "咬", "駛", "薯", "刷", "斥", "番", "賦", "奉", "佛", "澆", "漫", "曼", "扇", "鈣", "桃", "扶", "仔", "返", "俗", "虧", "腔", "鞋", "棱", "覆", "框", "悄", "叔", "撞", "騙", "勘", "旺", "沸", "孤", "吐", "孟", "渠", "屈", "疾", "妙", "惜", "仰", "狠", "脹", "諧", "拋", "黴", "桑", "崗", "嘛", "衰", "盜", "滲", "臟", "賴", "湧", "甜", "曹", "閱", "肌", "哩", "厲", "烴", "緯", "毅", "昨", "偽", "症", "煮", "嘆", "釘", "搭", "莖", "籠", "酷", "偷", "弓", "錐", "恆", "傑", "坑", "鼻", "翼", "綸", "敘", "獄", "逮", "罐", "絡", "棚", "抑", "膨", "蔬", "寺", "驟", "穆", "冶", "枯", "冊", "屍", "凸", "紳", "坯", "犧", "焰", "轟", "欣", "晉", "瘦", "禦", "錠", "錦", "喪", "旬", "鍛", "壟", "搜", "撲", "邀", "亭", "酯", "邁", "舒", "脆", "酶", "閒", "憂", "酚", "頑", "羽", "漲", "卸", "仗", "陪", "闢", "懲", "杭", "姚", "肚", "捉", "飄", "漂", "昆", "欺", "吾", "郎", "烷", "汁", "呵", "飾", "蕭", "雅", "郵", "遷", "燕", "撒", "姻", "赴", "宴", "煩", "債", "帳", "斑", "鈴", "旨", "醇", "董", "餅", "雛", "姿", "拌", "傅", "腹", "妥", "揉", "賢", "拆", "歪", "葡", "胺", "丟", "浩", "徽", "昂", "墊", "擋", "覽", "貪", "慰", "繳", "汪", "慌", "馮", "諾", "姜", "誼", "兇", "劣", "誣", "耀", "昏", "躺", "盈", "騎", "喬", "溪", "叢", "盧", "抹", "悶", "諮", "刮", "駕", "纜", "悟", "摘", "鉺", "擲", "頗", "幻", "柄", "惠", "慘", "佳", "仇", "臘", "窩", "滌", "劍", "瞧", "堡", "潑", "蔥", "罩", "霍", "撈", "胎", "蒼", "濱", "倆", "捅", "湘", "砍", "霞", "邵", "萄", "瘋", "淮", "遂", "熊", "糞", "烘", "宿", "檔", "戈", "駁", "嫂", "裕", "徙", "箭", "捐", "腸", "撐", "曬", "辨", "殿", "蓮", "攤", "攪", "醬", "屏", "疫", "哀", "蔡", "堵", "沫", "皺", "暢", "疊", "閣", "萊", "敲", "轄", "鉤", "痕", "壩", "巷", "餓", "禍", "丘", "玄", "溜", "曰", "邏", "彭", "嘗", "卿", "妨", "艇", "吞", "韋", "怨", "矮", "歇"] +const frenchWords*: seq[string] = @["abaisser", "abandon", "abdiquer", "abeille", "abolir", "aborder", "aboutir", "aboyer", "abrasif", "abreuver", "abriter", "abroger", "abrupt", "absence", "absolu", "absurde", "abusif", "abyssal", "académie", "acajou", "acarien", "accabler", "accepter", "acclamer", "accolade", "accroche", "accuser", "acerbe", "achat", "acheter", "aciduler", "acier", "acompte", "acquérir", "acronyme", "acteur", "actif", "actuel", "adepte", "adéquat", "adhésif", "adjectif", "adjuger", "admettre", "admirer", "adopter", "adorer", "adoucir", "adresse", "adroit", "adulte", "adverbe", "aérer", "aéronef", "affaire", "affecter", "affiche", "affreux", "affubler", "agacer", "agencer", "agile", "agiter", "agrafer", "agréable", "agrume", "aider", "aiguille", "ailier", "aimable", "aisance", "ajouter", "ajuster", "alarmer", "alchimie", "alerte", "algèbre", "algue", "aliéner", "aliment", "alléger", "alliage", "allouer", "allumer", "alourdir", "alpaga", "altesse", "alvéole", "amateur", "ambigu", "ambre", "aménager", "amertume", "amidon", "amiral", "amorcer", "amour", "amovible", "amphibie", "ampleur", "amusant", "analyse", "anaphore", "anarchie", "anatomie", "ancien", "anéantir", "angle", "angoisse", "anguleux", "animal", "annexer", "annonce", "annuel", "anodin", "anomalie", "anonyme", "anormal", "antenne", "antidote", "anxieux", "apaiser", "apéritif", "aplanir", "apologie", "appareil", "appeler", "apporter", "appuyer", "aquarium", "aqueduc", "arbitre", "arbuste", "ardeur", "ardoise", "argent", "arlequin", "armature", "armement", "armoire", "armure", "arpenter", "arracher", "arriver", "arroser", "arsenic", "artériel", "article", "aspect", "asphalte", "aspirer", "assaut", "asservir", "assiette", "associer", "assurer", "asticot", "astre", "astuce", "atelier", "atome", "atrium", "atroce", "attaque", "attentif", "attirer", "attraper", "aubaine", "auberge", "audace", "audible", "augurer", "aurore", "automne", "autruche", "avaler", "avancer", "avarice", "avenir", "averse", "aveugle", "aviateur", "avide", "avion", "aviser", "avoine", "avouer", "avril", "axial", "axiome", "badge", "bafouer", "bagage", "baguette", "baignade", "balancer", "balcon", "baleine", "balisage", "bambin", "bancaire", "bandage", "banlieue", "bannière", "banquier", "barbier", "baril", "baron", "barque", "barrage", "bassin", "bastion", "bataille", "bateau", "batterie", "baudrier", "bavarder", "belette", "bélier", "belote", "bénéfice", "berceau", "berger", "berline", "bermuda", "besace", "besogne", "bétail", "beurre", "biberon", "bicycle", "bidule", "bijou", "bilan", "bilingue", "billard", "binaire", "biologie", "biopsie", "biotype", "biscuit", "bison", "bistouri", "bitume", "bizarre", "blafard", "blague", "blanchir", "blessant", "blinder", "blond", "bloquer", "blouson", "bobard", "bobine", "boire", "boiser", "bolide", "bonbon", "bondir", "bonheur", "bonifier", "bonus", "bordure", "borne", "botte", "boucle", "boueux", "bougie", "boulon", "bouquin", "bourse", "boussole", "boutique", "boxeur", "branche", "brasier", "brave", "brebis", "brèche", "breuvage", "bricoler", "brigade", "brillant", "brioche", "brique", "brochure", "broder", "bronzer", "brousse", "broyeur", "brume", "brusque", "brutal", "bruyant", "buffle", "buisson", "bulletin", "bureau", "burin", "bustier", "butiner", "butoir", "buvable", "buvette", "cabanon", "cabine", "cachette", "cadeau", "cadre", "caféine", "caillou", "caisson", "calculer", "calepin", "calibre", "calmer", "calomnie", "calvaire", "camarade", "caméra", "camion", "campagne", "canal", "caneton", "canon", "cantine", "canular", "capable", "caporal", "caprice", "capsule", "capter", "capuche", "carabine", "carbone", "caresser", "caribou", "carnage", "carotte", "carreau", "carton", "cascade", "casier", "casque", "cassure", "causer", "caution", "cavalier", "caverne", "caviar", "cédille", "ceinture", "céleste", "cellule", "cendrier", "censurer", "central", "cercle", "cérébral", "cerise", "cerner", "cerveau", "cesser", "chagrin", "chaise", "chaleur", "chambre", "chance", "chapitre", "charbon", "chasseur", "chaton", "chausson", "chavirer", "chemise", "chenille", "chéquier", "chercher", "cheval", "chien", "chiffre", "chignon", "chimère", "chiot", "chlorure", "chocolat", "choisir", "chose", "chouette", "chrome", "chute", "cigare", "cigogne", "cimenter", "cinéma", "cintrer", "circuler", "cirer", "cirque", "citerne", "citoyen", "citron", "civil", "clairon", "clameur", "claquer", "classe", "clavier", "client", "cligner", "climat", "clivage", "cloche", "clonage", "cloporte", "cobalt", "cobra", "cocasse", "cocotier", "coder", "codifier", "coffre", "cogner", "cohésion", "coiffer", "coincer", "colère", "colibri", "colline", "colmater", "colonel", "combat", "comédie", "commande", "compact", "concert", "conduire", "confier", "congeler", "connoter", "consonne", "contact", "convexe", "copain", "copie", "corail", "corbeau", "cordage", "corniche", "corpus", "correct", "cortège", "cosmique", "costume", "coton", "coude", "coupure", "courage", "couteau", "couvrir", "coyote", "crabe", "crainte", "cravate", "crayon", "créature", "créditer", "crémeux", "creuser", "crevette", "cribler", "crier", "cristal", "critère", "croire", "croquer", "crotale", "crucial", "cruel", "crypter", "cubique", "cueillir", "cuillère", "cuisine", "cuivre", "culminer", "cultiver", "cumuler", "cupide", "curatif", "curseur", "cyanure", "cycle", "cylindre", "cynique", "daigner", "damier", "danger", "danseur", "dauphin", "débattre", "débiter", "déborder", "débrider", "débutant", "décaler", "décembre", "déchirer", "décider", "déclarer", "décorer", "décrire", "décupler", "dédale", "déductif", "déesse", "défensif", "défiler", "défrayer", "dégager", "dégivrer", "déglutir", "dégrafer", "déjeuner", "délice", "déloger", "demander", "demeurer", "démolir", "dénicher", "dénouer", "dentelle", "dénuder", "départ", "dépenser", "déphaser", "déplacer", "déposer", "déranger", "dérober", "désastre", "descente", "désert", "désigner", "désobéir", "dessiner", "destrier", "détacher", "détester", "détourer", "détresse", "devancer", "devenir", "deviner", "devoir", "diable", "dialogue", "diamant", "dicter", "différer", "digérer", "digital", "digne", "diluer", "dimanche", "diminuer", "dioxyde", "directif", "diriger", "discuter", "disposer", "dissiper", "distance", "divertir", "diviser", "docile", "docteur", "dogme", "doigt", "domaine", "domicile", "dompter", "donateur", "donjon", "donner", "dopamine", "dortoir", "dorure", "dosage", "doseur", "dossier", "dotation", "douanier", "double", "douceur", "douter", "doyen", "dragon", "draper", "dresser", "dribbler", "droiture", "duperie", "duplexe", "durable", "durcir", "dynastie", "éblouir", "écarter", "écharpe", "échelle", "éclairer", "éclipse", "éclore", "écluse", "école", "économie", "écorce", "écouter", "écraser", "écrémer", "écrivain", "écrou", "écume", "écureuil", "édifier", "éduquer", "effacer", "effectif", "effigie", "effort", "effrayer", "effusion", "égaliser", "égarer", "éjecter", "élaborer", "élargir", "électron", "élégant", "éléphant", "élève", "éligible", "élitisme", "éloge", "élucider", "éluder", "emballer", "embellir", "embryon", "émeraude", "émission", "emmener", "émotion", "émouvoir", "empereur", "employer", "emporter", "emprise", "émulsion", "encadrer", "enchère", "enclave", "encoche", "endiguer", "endosser", "endroit", "enduire", "énergie", "enfance", "enfermer", "enfouir", "engager", "engin", "englober", "énigme", "enjamber", "enjeu", "enlever", "ennemi", "ennuyeux", "enrichir", "enrobage", "enseigne", "entasser", "entendre", "entier", "entourer", "entraver", "énumérer", "envahir", "enviable", "envoyer", "enzyme", "éolien", "épaissir", "épargne", "épatant", "épaule", "épicerie", "épidémie", "épier", "épilogue", "épine", "épisode", "épitaphe", "époque", "épreuve", "éprouver", "épuisant", "équerre", "équipe", "ériger", "érosion", "erreur", "éruption", "escalier", "espadon", "espèce", "espiègle", "espoir", "esprit", "esquiver", "essayer", "essence", "essieu", "essorer", "estime", "estomac", "estrade", "étagère", "étaler", "étanche", "étatique", "éteindre", "étendoir", "éternel", "éthanol", "éthique", "ethnie", "étirer", "étoffer", "étoile", "étonnant", "étourdir", "étrange", "étroit", "étude", "euphorie", "évaluer", "évasion", "éventail", "évidence", "éviter", "évolutif", "évoquer", "exact", "exagérer", "exaucer", "exceller", "excitant", "exclusif", "excuse", "exécuter", "exemple", "exercer", "exhaler", "exhorter", "exigence", "exiler", "exister", "exotique", "expédier", "explorer", "exposer", "exprimer", "exquis", "extensif", "extraire", "exulter", "fable", "fabuleux", "facette", "facile", "facture", "faiblir", "falaise", "fameux", "famille", "farceur", "farfelu", "farine", "farouche", "fasciner", "fatal", "fatigue", "faucon", "fautif", "faveur", "favori", "fébrile", "féconder", "fédérer", "félin", "femme", "fémur", "fendoir", "féodal", "fermer", "féroce", "ferveur", "festival", "feuille", "feutre", "février", "fiasco", "ficeler", "fictif", "fidèle", "figure", "filature", "filetage", "filière", "filleul", "filmer", "filou", "filtrer", "financer", "finir", "fiole", "firme", "fissure", "fixer", "flairer", "flamme", "flasque", "flatteur", "fléau", "flèche", "fleur", "flexion", "flocon", "flore", "fluctuer", "fluide", "fluvial", "folie", "fonderie", "fongible", "fontaine", "forcer", "forgeron", "formuler", "fortune", "fossile", "foudre", "fougère", "fouiller", "foulure", "fourmi", "fragile", "fraise", "franchir", "frapper", "frayeur", "frégate", "freiner", "frelon", "frémir", "frénésie", "frère", "friable", "friction", "frisson", "frivole", "froid", "fromage", "frontal", "frotter", "fruit", "fugitif", "fuite", "fureur", "furieux", "furtif", "fusion", "futur", "gagner", "galaxie", "galerie", "gambader", "garantir", "gardien", "garnir", "garrigue", "gazelle", "gazon", "géant", "gélatine", "gélule", "gendarme", "général", "génie", "genou", "gentil", "géologie", "géomètre", "géranium", "germe", "gestuel", "geyser", "gibier", "gicler", "girafe", "givre", "glace", "glaive", "glisser", "globe", "gloire", "glorieux", "golfeur", "gomme", "gonfler", "gorge", "gorille", "goudron", "gouffre", "goulot", "goupille", "gourmand", "goutte", "graduel", "graffiti", "graine", "grand", "grappin", "gratuit", "gravir", "grenat", "griffure", "griller", "grimper", "grogner", "gronder", "grotte", "groupe", "gruger", "grutier", "gruyère", "guépard", "guerrier", "guide", "guimauve", "guitare", "gustatif", "gymnaste", "gyrostat", "habitude", "hachoir", "halte", "hameau", "hangar", "hanneton", "haricot", "harmonie", "harpon", "hasard", "hélium", "hématome", "herbe", "hérisson", "hermine", "héron", "hésiter", "heureux", "hiberner", "hibou", "hilarant", "histoire", "hiver", "homard", "hommage", "homogène", "honneur", "honorer", "honteux", "horde", "horizon", "horloge", "hormone", "horrible", "houleux", "housse", "hublot", "huileux", "humain", "humble", "humide", "humour", "hurler", "hydromel", "hygiène", "hymne", "hypnose", "idylle", "ignorer", "iguane", "illicite", "illusion", "image", "imbiber", "imiter", "immense", "immobile", "immuable", "impact", "impérial", "implorer", "imposer", "imprimer", "imputer", "incarner", "incendie", "incident", "incliner", "incolore", "indexer", "indice", "inductif", "inédit", "ineptie", "inexact", "infini", "infliger", "informer", "infusion", "ingérer", "inhaler", "inhiber", "injecter", "injure", "innocent", "inoculer", "inonder", "inscrire", "insecte", "insigne", "insolite", "inspirer", "instinct", "insulter", "intact", "intense", "intime", "intrigue", "intuitif", "inutile", "invasion", "inventer", "inviter", "invoquer", "ironique", "irradier", "irréel", "irriter", "isoler", "ivoire", "ivresse", "jaguar", "jaillir", "jambe", "janvier", "jardin", "jauger", "jaune", "javelot", "jetable", "jeton", "jeudi", "jeunesse", "joindre", "joncher", "jongler", "joueur", "jouissif", "journal", "jovial", "joyau", "joyeux", "jubiler", "jugement", "junior", "jupon", "juriste", "justice", "juteux", "juvénile", "kayak", "kimono", "kiosque", "label", "labial", "labourer", "lacérer", "lactose", "lagune", "laine", "laisser", "laitier", "lambeau", "lamelle", "lampe", "lanceur", "langage", "lanterne", "lapin", "largeur", "larme", "laurier", "lavabo", "lavoir", "lecture", "légal", "léger", "légume", "lessive", "lettre", "levier", "lexique", "lézard", "liasse", "libérer", "libre", "licence", "licorne", "liège", "lièvre", "ligature", "ligoter", "ligue", "limer", "limite", "limonade", "limpide", "linéaire", "lingot", "lionceau", "liquide", "lisière", "lister", "lithium", "litige", "littoral", "livreur", "logique", "lointain", "loisir", "lombric", "loterie", "louer", "lourd", "loutre", "louve", "loyal", "lubie", "lucide", "lucratif", "lueur", "lugubre", "luisant", "lumière", "lunaire", "lundi", "luron", "lutter", "luxueux", "machine", "magasin", "magenta", "magique", "maigre", "maillon", "maintien", "mairie", "maison", "majorer", "malaxer", "maléfice", "malheur", "malice", "mallette", "mammouth", "mandater", "maniable", "manquant", "manteau", "manuel", "marathon", "marbre", "marchand", "mardi", "maritime", "marqueur", "marron", "marteler", "mascotte", "massif", "matériel", "matière", "matraque", "maudire", "maussade", "mauve", "maximal", "méchant", "méconnu", "médaille", "médecin", "méditer", "méduse", "meilleur", "mélange", "mélodie", "membre", "mémoire", "menacer", "mener", "menhir", "mensonge", "mentor", "mercredi", "mérite", "merle", "messager", "mesure", "métal", "météore", "méthode", "métier", "meuble", "miauler", "microbe", "miette", "mignon", "migrer", "milieu", "million", "mimique", "mince", "minéral", "minimal", "minorer", "minute", "miracle", "miroiter", "missile", "mixte", "mobile", "moderne", "moelleux", "mondial", "moniteur", "monnaie", "monotone", "monstre", "montagne", "monument", "moqueur", "morceau", "morsure", "mortier", "moteur", "motif", "mouche", "moufle", "moulin", "mousson", "mouton", "mouvant", "multiple", "munition", "muraille", "murène", "murmure", "muscle", "muséum", "musicien", "mutation", "muter", "mutuel", "myriade", "myrtille", "mystère", "mythique", "nageur", "nappe", "narquois", "narrer", "natation", "nation", "nature", "naufrage", "nautique", "navire", "nébuleux", "nectar", "néfaste", "négation", "négliger", "négocier", "neige", "nerveux", "nettoyer", "neurone", "neutron", "neveu", "niche", "nickel", "nitrate", "niveau", "noble", "nocif", "nocturne", "noirceur", "noisette", "nomade", "nombreux", "nommer", "normatif", "notable", "notifier", "notoire", "nourrir", "nouveau", "novateur", "novembre", "novice", "nuage", "nuancer", "nuire", "nuisible", "numéro", "nuptial", "nuque", "nutritif", "obéir", "objectif", "obliger", "obscur", "observer", "obstacle", "obtenir", "obturer", "occasion", "occuper", "océan", "octobre", "octroyer", "octupler", "oculaire", "odeur", "odorant", "offenser", "officier", "offrir", "ogive", "oiseau", "oisillon", "olfactif", "olivier", "ombrage", "omettre", "onctueux", "onduler", "onéreux", "onirique", "opale", "opaque", "opérer", "opinion", "opportun", "opprimer", "opter", "optique", "orageux", "orange", "orbite", "ordonner", "oreille", "organe", "orgueil", "orifice", "ornement", "orque", "ortie", "osciller", "osmose", "ossature", "otarie", "ouragan", "ourson", "outil", "outrager", "ouvrage", "ovation", "oxyde", "oxygène", "ozone", "paisible", "palace", "palmarès", "palourde", "palper", "panache", "panda", "pangolin", "paniquer", "panneau", "panorama", "pantalon", "papaye", "papier", "papoter", "papyrus", "paradoxe", "parcelle", "paresse", "parfumer", "parler", "parole", "parrain", "parsemer", "partager", "parure", "parvenir", "passion", "pastèque", "paternel", "patience", "patron", "pavillon", "pavoiser", "payer", "paysage", "peigne", "peintre", "pelage", "pélican", "pelle", "pelouse", "peluche", "pendule", "pénétrer", "pénible", "pensif", "pénurie", "pépite", "péplum", "perdrix", "perforer", "période", "permuter", "perplexe", "persil", "perte", "peser", "pétale", "petit", "pétrir", "peuple", "pharaon", "phobie", "phoque", "photon", "phrase", "physique", "piano", "pictural", "pièce", "pierre", "pieuvre", "pilote", "pinceau", "pipette", "piquer", "pirogue", "piscine", "piston", "pivoter", "pixel", "pizza", "placard", "plafond", "plaisir", "planer", "plaque", "plastron", "plateau", "pleurer", "plexus", "pliage", "plomb", "plonger", "pluie", "plumage", "pochette", "poésie", "poète", "pointe", "poirier", "poisson", "poivre", "polaire", "policier", "pollen", "polygone", "pommade", "pompier", "ponctuel", "pondérer", "poney", "portique", "position", "posséder", "posture", "potager", "poteau", "potion", "pouce", "poulain", "poumon", "pourpre", "poussin", "pouvoir", "prairie", "pratique", "précieux", "prédire", "préfixe", "prélude", "prénom", "présence", "prétexte", "prévoir", "primitif", "prince", "prison", "priver", "problème", "procéder", "prodige", "profond", "progrès", "proie", "projeter", "prologue", "promener", "propre", "prospère", "protéger", "prouesse", "proverbe", "prudence", "pruneau", "psychose", "public", "puceron", "puiser", "pulpe", "pulsar", "punaise", "punitif", "pupitre", "purifier", "puzzle", "pyramide", "quasar", "querelle", "question", "quiétude", "quitter", "quotient", "racine", "raconter", "radieux", "ragondin", "raideur", "raisin", "ralentir", "rallonge", "ramasser", "rapide", "rasage", "ratisser", "ravager", "ravin", "rayonner", "réactif", "réagir", "réaliser", "réanimer", "recevoir", "réciter", "réclamer", "récolter", "recruter", "reculer", "recycler", "rédiger", "redouter", "refaire", "réflexe", "réformer", "refrain", "refuge", "régalien", "région", "réglage", "régulier", "réitérer", "rejeter", "rejouer", "relatif", "relever", "relief", "remarque", "remède", "remise", "remonter", "remplir", "remuer", "renard", "renfort", "renifler", "renoncer", "rentrer", "renvoi", "replier", "reporter", "reprise", "reptile", "requin", "réserve", "résineux", "résoudre", "respect", "rester", "résultat", "rétablir", "retenir", "réticule", "retomber", "retracer", "réunion", "réussir", "revanche", "revivre", "révolte", "révulsif", "richesse", "rideau", "rieur", "rigide", "rigoler", "rincer", "riposter", "risible", "risque", "rituel", "rival", "rivière", "rocheux", "romance", "rompre", "ronce", "rondin", "roseau", "rosier", "rotatif", "rotor", "rotule", "rouge", "rouille", "rouleau", "routine", "royaume", "ruban", "rubis", "ruche", "ruelle", "rugueux", "ruiner", "ruisseau", "ruser", "rustique", "rythme", "sabler", "saboter", "sabre", "sacoche", "safari", "sagesse", "saisir", "salade", "salive", "salon", "saluer", "samedi", "sanction", "sanglier", "sarcasme", "sardine", "saturer", "saugrenu", "saumon", "sauter", "sauvage", "savant", "savonner", "scalpel", "scandale", "scélérat", "scénario", "sceptre", "schéma", "science", "scinder", "score", "scrutin", "sculpter", "séance", "sécable", "sécher", "secouer", "sécréter", "sédatif", "séduire", "seigneur", "séjour", "sélectif", "semaine", "sembler", "semence", "séminal", "sénateur", "sensible", "sentence", "séparer", "séquence", "serein", "sergent", "sérieux", "serrure", "sérum", "service", "sésame", "sévir", "sevrage", "sextuple", "sidéral", "siècle", "siéger", "siffler", "sigle", "signal", "silence", "silicium", "simple", "sincère", "sinistre", "siphon", "sirop", "sismique", "situer", "skier", "social", "socle", "sodium", "soigneux", "soldat", "soleil", "solitude", "soluble", "sombre", "sommeil", "somnoler", "sonde", "songeur", "sonnette", "sonore", "sorcier", "sortir", "sosie", "sottise", "soucieux", "soudure", "souffle", "soulever", "soupape", "source", "soutirer", "souvenir", "spacieux", "spatial", "spécial", "sphère", "spiral", "stable", "station", "sternum", "stimulus", "stipuler", "strict", "studieux", "stupeur", "styliste", "sublime", "substrat", "subtil", "subvenir", "succès", "sucre", "suffixe", "suggérer", "suiveur", "sulfate", "superbe", "supplier", "surface", "suricate", "surmener", "surprise", "sursaut", "survie", "suspect", "syllabe", "symbole", "symétrie", "synapse", "syntaxe", "système", "tabac", "tablier", "tactile", "tailler", "talent", "talisman", "talonner", "tambour", "tamiser", "tangible", "tapis", "taquiner", "tarder", "tarif", "tartine", "tasse", "tatami", "tatouage", "taupe", "taureau", "taxer", "témoin", "temporel", "tenaille", "tendre", "teneur", "tenir", "tension", "terminer", "terne", "terrible", "tétine", "texte", "thème", "théorie", "thérapie", "thorax", "tibia", "tiède", "timide", "tirelire", "tiroir", "tissu", "titane", "titre", "tituber", "toboggan", "tolérant", "tomate", "tonique", "tonneau", "toponyme", "torche", "tordre", "tornade", "torpille", "torrent", "torse", "tortue", "totem", "toucher", "tournage", "tousser", "toxine", "traction", "trafic", "tragique", "trahir", "train", "trancher", "travail", "trèfle", "tremper", "trésor", "treuil", "triage", "tribunal", "tricoter", "trilogie", "triomphe", "tripler", "triturer", "trivial", "trombone", "tronc", "tropical", "troupeau", "tuile", "tulipe", "tumulte", "tunnel", "turbine", "tuteur", "tutoyer", "tuyau", "tympan", "typhon", "typique", "tyran", "ubuesque", "ultime", "ultrason", "unanime", "unifier", "union", "unique", "unitaire", "univers", "uranium", "urbain", "urticant", "usage", "usine", "usuel", "usure", "utile", "utopie", "vacarme", "vaccin", "vagabond", "vague", "vaillant", "vaincre", "vaisseau", "valable", "valise", "vallon", "valve", "vampire", "vanille", "vapeur", "varier", "vaseux", "vassal", "vaste", "vecteur", "vedette", "végétal", "véhicule", "veinard", "véloce", "vendredi", "vénérer", "venger", "venimeux", "ventouse", "verdure", "vérin", "vernir", "verrou", "verser", "vertu", "veston", "vétéran", "vétuste", "vexant", "vexer", "viaduc", "viande", "victoire", "vidange", "vidéo", "vignette", "vigueur", "vilain", "village", "vinaigre", "violon", "vipère", "virement", "virtuose", "virus", "visage", "viseur", "vision", "visqueux", "visuel", "vital", "vitesse", "viticole", "vitrine", "vivace", "vivipare", "vocation", "voguer", "voile", "voisin", "voiture", "volaille", "volcan", "voltiger", "volume", "vorace", "vortex", "voter", "vouloir", "voyage", "voyelle", "wagon", "xénon", "yacht", "zèbre", "zénith", "zeste", "zoologie"] +const spanishWords*: seq[string] = @["ábaco", "abdomen", "abeja", "abierto", "abogado", "abono", "aborto", "abrazo", "abrir", "abuelo", "abuso", "acabar", "academia", "acceso", "acción", "aceite", "acelga", "acento", "aceptar", "ácido", "aclarar", "acné", "acoger", "acoso", "activo", "acto", "actriz", "actuar", "acudir", "acuerdo", "acusar", "adicto", "admitir", "adoptar", "adorno", "aduana", "adulto", "aéreo", "afectar", "afición", "afinar", "afirmar", "ágil", "agitar", "agonía", "agosto", "agotar", "agregar", "agrio", "agua", "agudo", "águila", "aguja", "ahogo", "ahorro", "aire", "aislar", "ajedrez", "ajeno", "ajuste", "alacrán", "alambre", "alarma", "alba", "álbum", "alcalde", "aldea", "alegre", "alejar", "alerta", "aleta", "alfiler", "alga", "algodón", "aliado", "aliento", "alivio", "alma", "almeja", "almíbar", "altar", "alteza", "altivo", "alto", "altura", "alumno", "alzar", "amable", "amante", "amapola", "amargo", "amasar", "ámbar", "ámbito", "ameno", "amigo", "amistad", "amor", "amparo", "amplio", "ancho", "anciano", "ancla", "andar", "andén", "anemia", "ángulo", "anillo", "ánimo", "anís", "anotar", "antena", "antiguo", "antojo", "anual", "anular", "anuncio", "añadir", "añejo", "año", "apagar", "aparato", "apetito", "apio", "aplicar", "apodo", "aporte", "apoyo", "aprender", "aprobar", "apuesta", "apuro", "arado", "araña", "arar", "árbitro", "árbol", "arbusto", "archivo", "arco", "arder", "ardilla", "arduo", "área", "árido", "aries", "armonía", "arnés", "aroma", "arpa", "arpón", "arreglo", "arroz", "arruga", "arte", "artista", "asa", "asado", "asalto", "ascenso", "asegurar", "aseo", "asesor", "asiento", "asilo", "asistir", "asno", "asombro", "áspero", "astilla", "astro", "astuto", "asumir", "asunto", "atajo", "ataque", "atar", "atento", "ateo", "ático", "atleta", "átomo", "atraer", "atroz", "atún", "audaz", "audio", "auge", "aula", "aumento", "ausente", "autor", "aval", "avance", "avaro", "ave", "avellana", "avena", "avestruz", "avión", "aviso", "ayer", "ayuda", "ayuno", "azafrán", "azar", "azote", "azúcar", "azufre", "azul", "baba", "babor", "bache", "bahía", "baile", "bajar", "balanza", "balcón", "balde", "bambú", "banco", "banda", "baño", "barba", "barco", "barniz", "barro", "báscula", "bastón", "basura", "batalla", "batería", "batir", "batuta", "baúl", "bazar", "bebé", "bebida", "bello", "besar", "beso", "bestia", "bicho", "bien", "bingo", "blanco", "bloque", "blusa", "boa", "bobina", "bobo", "boca", "bocina", "boda", "bodega", "boina", "bola", "bolero", "bolsa", "bomba", "bondad", "bonito", "bono", "bonsái", "borde", "borrar", "bosque", "bote", "botín", "bóveda", "bozal", "bravo", "brazo", "brecha", "breve", "brillo", "brinco", "brisa", "broca", "broma", "bronce", "brote", "bruja", "brusco", "bruto", "buceo", "bucle", "bueno", "buey", "bufanda", "bufón", "búho", "buitre", "bulto", "burbuja", "burla", "burro", "buscar", "butaca", "buzón", "caballo", "cabeza", "cabina", "cabra", "cacao", "cadáver", "cadena", "caer", "café", "caída", "caimán", "caja", "cajón", "cal", "calamar", "calcio", "caldo", "calidad", "calle", "calma", "calor", "calvo", "cama", "cambio", "camello", "camino", "campo", "cáncer", "candil", "canela", "canguro", "canica", "canto", "caña", "cañón", "caoba", "caos", "capaz", "capitán", "capote", "captar", "capucha", "cara", "carbón", "cárcel", "careta", "carga", "cariño", "carne", "carpeta", "carro", "carta", "casa", "casco", "casero", "caspa", "castor", "catorce", "catre", "caudal", "causa", "cazo", "cebolla", "ceder", "cedro", "celda", "célebre", "celoso", "célula", "cemento", "ceniza", "centro", "cerca", "cerdo", "cereza", "cero", "cerrar", "certeza", "césped", "cetro", "chacal", "chaleco", "champú", "chancla", "chapa", "charla", "chico", "chiste", "chivo", "choque", "choza", "chuleta", "chupar", "ciclón", "ciego", "cielo", "cien", "cierto", "cifra", "cigarro", "cima", "cinco", "cine", "cinta", "ciprés", "circo", "ciruela", "cisne", "cita", "ciudad", "clamor", "clan", "claro", "clase", "clave", "cliente", "clima", "clínica", "cobre", "cocción", "cochino", "cocina", "coco", "código", "codo", "cofre", "coger", "cohete", "cojín", "cojo", "cola", "colcha", "colegio", "colgar", "colina", "collar", "colmo", "columna", "combate", "comer", "comida", "cómodo", "compra", "conde", "conejo", "conga", "conocer", "consejo", "contar", "copa", "copia", "corazón", "corbata", "corcho", "cordón", "corona", "correr", "coser", "cosmos", "costa", "cráneo", "cráter", "crear", "crecer", "creído", "crema", "cría", "crimen", "cripta", "crisis", "cromo", "crónica", "croqueta", "crudo", "cruz", "cuadro", "cuarto", "cuatro", "cubo", "cubrir", "cuchara", "cuello", "cuento", "cuerda", "cuesta", "cueva", "cuidar", "culebra", "culpa", "culto", "cumbre", "cumplir", "cuna", "cuneta", "cuota", "cupón", "cúpula", "curar", "curioso", "curso", "curva", "cutis", "dama", "danza", "dar", "dardo", "dátil", "deber", "débil", "década", "decir", "dedo", "defensa", "definir", "dejar", "delfín", "delgado", "delito", "demora", "denso", "dental", "deporte", "derecho", "derrota", "desayuno", "deseo", "desfile", "desnudo", "destino", "desvío", "detalle", "detener", "deuda", "día", "diablo", "diadema", "diamante", "diana", "diario", "dibujo", "dictar", "diente", "dieta", "diez", "difícil", "digno", "dilema", "diluir", "dinero", "directo", "dirigir", "disco", "diseño", "disfraz", "diva", "divino", "doble", "doce", "dolor", "domingo", "don", "donar", "dorado", "dormir", "dorso", "dos", "dosis", "dragón", "droga", "ducha", "duda", "duelo", "dueño", "dulce", "dúo", "duque", "durar", "dureza", "duro", "ébano", "ebrio", "echar", "eco", "ecuador", "edad", "edición", "edificio", "editor", "educar", "efecto", "eficaz", "eje", "ejemplo", "elefante", "elegir", "elemento", "elevar", "elipse", "élite", "elixir", "elogio", "eludir", "embudo", "emitir", "emoción", "empate", "empeño", "empleo", "empresa", "enano", "encargo", "enchufe", "encía", "enemigo", "enero", "enfado", "enfermo", "engaño", "enigma", "enlace", "enorme", "enredo", "ensayo", "enseñar", "entero", "entrar", "envase", "envío", "época", "equipo", "erizo", "escala", "escena", "escolar", "escribir", "escudo", "esencia", "esfera", "esfuerzo", "espada", "espejo", "espía", "esposa", "espuma", "esquí", "estar", "este", "estilo", "estufa", "etapa", "eterno", "ética", "etnia", "evadir", "evaluar", "evento", "evitar", "exacto", "examen", "exceso", "excusa", "exento", "exigir", "exilio", "existir", "éxito", "experto", "explicar", "exponer", "extremo", "fábrica", "fábula", "fachada", "fácil", "factor", "faena", "faja", "falda", "fallo", "falso", "faltar", "fama", "familia", "famoso", "faraón", "farmacia", "farol", "farsa", "fase", "fatiga", "fauna", "favor", "fax", "febrero", "fecha", "feliz", "feo", "feria", "feroz", "fértil", "fervor", "festín", "fiable", "fianza", "fiar", "fibra", "ficción", "ficha", "fideo", "fiebre", "fiel", "fiera", "fiesta", "figura", "fijar", "fijo", "fila", "filete", "filial", "filtro", "fin", "finca", "fingir", "finito", "firma", "flaco", "flauta", "flecha", "flor", "flota", "fluir", "flujo", "flúor", "fobia", "foca", "fogata", "fogón", "folio", "folleto", "fondo", "forma", "forro", "fortuna", "forzar", "fosa", "foto", "fracaso", "frágil", "franja", "frase", "fraude", "freír", "freno", "fresa", "frío", "frito", "fruta", "fuego", "fuente", "fuerza", "fuga", "fumar", "función", "funda", "furgón", "furia", "fusil", "fútbol", "futuro", "gacela", "gafas", "gaita", "gajo", "gala", "galería", "gallo", "gamba", "ganar", "gancho", "ganga", "ganso", "garaje", "garza", "gasolina", "gastar", "gato", "gavilán", "gemelo", "gemir", "gen", "género", "genio", "gente", "geranio", "gerente", "germen", "gesto", "gigante", "gimnasio", "girar", "giro", "glaciar", "globo", "gloria", "gol", "golfo", "goloso", "golpe", "goma", "gordo", "gorila", "gorra", "gota", "goteo", "gozar", "grada", "gráfico", "grano", "grasa", "gratis", "grave", "grieta", "grillo", "gripe", "gris", "grito", "grosor", "grúa", "grueso", "grumo", "grupo", "guante", "guapo", "guardia", "guerra", "guía", "guiño", "guion", "guiso", "guitarra", "gusano", "gustar", "haber", "hábil", "hablar", "hacer", "hacha", "hada", "hallar", "hamaca", "harina", "haz", "hazaña", "hebilla", "hebra", "hecho", "helado", "helio", "hembra", "herir", "hermano", "héroe", "hervir", "hielo", "hierro", "hígado", "higiene", "hijo", "himno", "historia", "hocico", "hogar", "hoguera", "hoja", "hombre", "hongo", "honor", "honra", "hora", "hormiga", "horno", "hostil", "hoyo", "hueco", "huelga", "huerta", "hueso", "huevo", "huida", "huir", "humano", "húmedo", "humilde", "humo", "hundir", "huracán", "hurto", "icono", "ideal", "idioma", "ídolo", "iglesia", "iglú", "igual", "ilegal", "ilusión", "imagen", "imán", "imitar", "impar", "imperio", "imponer", "impulso", "incapaz", "índice", "inerte", "infiel", "informe", "ingenio", "inicio", "inmenso", "inmune", "innato", "insecto", "instante", "interés", "íntimo", "intuir", "inútil", "invierno", "ira", "iris", "ironía", "isla", "islote", "jabalí", "jabón", "jamón", "jarabe", "jardín", "jarra", "jaula", "jazmín", "jefe", "jeringa", "jinete", "jornada", "joroba", "joven", "joya", "juerga", "jueves", "juez", "jugador", "jugo", "juguete", "juicio", "junco", "jungla", "junio", "juntar", "júpiter", "jurar", "justo", "juvenil", "juzgar", "kilo", "koala", "labio", "lacio", "lacra", "lado", "ladrón", "lagarto", "lágrima", "laguna", "laico", "lamer", "lámina", "lámpara", "lana", "lancha", "langosta", "lanza", "lápiz", "largo", "larva", "lástima", "lata", "látex", "latir", "laurel", "lavar", "lazo", "leal", "lección", "leche", "lector", "leer", "legión", "legumbre", "lejano", "lengua", "lento", "leña", "león", "leopardo", "lesión", "letal", "letra", "leve", "leyenda", "libertad", "libro", "licor", "líder", "lidiar", "lienzo", "liga", "ligero", "lima", "límite", "limón", "limpio", "lince", "lindo", "línea", "lingote", "lino", "linterna", "líquido", "liso", "lista", "litera", "litio", "litro", "llaga", "llama", "llanto", "llave", "llegar", "llenar", "llevar", "llorar", "llover", "lluvia", "lobo", "loción", "loco", "locura", "lógica", "logro", "lombriz", "lomo", "lonja", "lote", "lucha", "lucir", "lugar", "lujo", "luna", "lunes", "lupa", "lustro", "luto", "luz", "maceta", "macho", "madera", "madre", "maduro", "maestro", "mafia", "magia", "mago", "maíz", "maldad", "maleta", "malla", "malo", "mamá", "mambo", "mamut", "manco", "mando", "manejar", "manga", "maniquí", "manjar", "mano", "manso", "manta", "mañana", "mapa", "máquina", "mar", "marco", "marea", "marfil", "margen", "marido", "mármol", "marrón", "martes", "marzo", "masa", "máscara", "masivo", "matar", "materia", "matiz", "matriz", "máximo", "mayor", "mazorca", "mecha", "medalla", "medio", "médula", "mejilla", "mejor", "melena", "melón", "memoria", "menor", "mensaje", "mente", "menú", "mercado", "merengue", "mérito", "mes", "mesón", "meta", "meter", "método", "metro", "mezcla", "miedo", "miel", "miembro", "miga", "mil", "milagro", "militar", "millón", "mimo", "mina", "minero", "mínimo", "minuto", "miope", "mirar", "misa", "miseria", "misil", "mismo", "mitad", "mito", "mochila", "moción", "moda", "modelo", "moho", "mojar", "molde", "moler", "molino", "momento", "momia", "monarca", "moneda", "monja", "monto", "moño", "morada", "morder", "moreno", "morir", "morro", "morsa", "mortal", "mosca", "mostrar", "motivo", "mover", "móvil", "mozo", "mucho", "mudar", "mueble", "muela", "muerte", "muestra", "mugre", "mujer", "mula", "muleta", "multa", "mundo", "muñeca", "mural", "muro", "músculo", "museo", "musgo", "música", "muslo", "nácar", "nación", "nadar", "naipe", "naranja", "nariz", "narrar", "nasal", "natal", "nativo", "natural", "náusea", "naval", "nave", "navidad", "necio", "néctar", "negar", "negocio", "negro", "neón", "nervio", "neto", "neutro", "nevar", "nevera", "nicho", "nido", "niebla", "nieto", "niñez", "niño", "nítido", "nivel", "nobleza", "noche", "nómina", "noria", "norma", "norte", "nota", "noticia", "novato", "novela", "novio", "nube", "nuca", "núcleo", "nudillo", "nudo", "nuera", "nueve", "nuez", "nulo", "número", "nutria", "oasis", "obeso", "obispo", "objeto", "obra", "obrero", "observar", "obtener", "obvio", "oca", "ocaso", "océano", "ochenta", "ocho", "ocio", "ocre", "octavo", "octubre", "oculto", "ocupar", "ocurrir", "odiar", "odio", "odisea", "oeste", "ofensa", "oferta", "oficio", "ofrecer", "ogro", "oído", "oír", "ojo", "ola", "oleada", "olfato", "olivo", "olla", "olmo", "olor", "olvido", "ombligo", "onda", "onza", "opaco", "opción", "ópera", "opinar", "oponer", "optar", "óptica", "opuesto", "oración", "orador", "oral", "órbita", "orca", "orden", "oreja", "órgano", "orgía", "orgullo", "oriente", "origen", "orilla", "oro", "orquesta", "oruga", "osadía", "oscuro", "osezno", "oso", "ostra", "otoño", "otro", "oveja", "óvulo", "óxido", "oxígeno", "oyente", "ozono", "pacto", "padre", "paella", "página", "pago", "país", "pájaro", "palabra", "palco", "paleta", "pálido", "palma", "paloma", "palpar", "pan", "panal", "pánico", "pantera", "pañuelo", "papá", "papel", "papilla", "paquete", "parar", "parcela", "pared", "parir", "paro", "párpado", "parque", "párrafo", "parte", "pasar", "paseo", "pasión", "paso", "pasta", "pata", "patio", "patria", "pausa", "pauta", "pavo", "payaso", "peatón", "pecado", "pecera", "pecho", "pedal", "pedir", "pegar", "peine", "pelar", "peldaño", "pelea", "peligro", "pellejo", "pelo", "peluca", "pena", "pensar", "peñón", "peón", "peor", "pepino", "pequeño", "pera", "percha", "perder", "pereza", "perfil", "perico", "perla", "permiso", "perro", "persona", "pesa", "pesca", "pésimo", "pestaña", "pétalo", "petróleo", "pez", "pezuña", "picar", "pichón", "pie", "piedra", "pierna", "pieza", "pijama", "pilar", "piloto", "pimienta", "pino", "pintor", "pinza", "piña", "piojo", "pipa", "pirata", "pisar", "piscina", "piso", "pista", "pitón", "pizca", "placa", "plan", "plata", "playa", "plaza", "pleito", "pleno", "plomo", "pluma", "plural", "pobre", "poco", "poder", "podio", "poema", "poesía", "poeta", "polen", "policía", "pollo", "polvo", "pomada", "pomelo", "pomo", "pompa", "poner", "porción", "portal", "posada", "poseer", "posible", "poste", "potencia", "potro", "pozo", "prado", "precoz", "pregunta", "premio", "prensa", "preso", "previo", "primo", "príncipe", "prisión", "privar", "proa", "probar", "proceso", "producto", "proeza", "profesor", "programa", "prole", "promesa", "pronto", "propio", "próximo", "prueba", "público", "puchero", "pudor", "pueblo", "puerta", "puesto", "pulga", "pulir", "pulmón", "pulpo", "pulso", "puma", "punto", "puñal", "puño", "pupa", "pupila", "puré", "quedar", "queja", "quemar", "querer", "queso", "quieto", "química", "quince", "quitar", "rábano", "rabia", "rabo", "ración", "radical", "raíz", "rama", "rampa", "rancho", "rango", "rapaz", "rápido", "rapto", "rasgo", "raspa", "rato", "rayo", "raza", "razón", "reacción", "realidad", "rebaño", "rebote", "recaer", "receta", "rechazo", "recoger", "recreo", "recto", "recurso", "red", "redondo", "reducir", "reflejo", "reforma", "refrán", "refugio", "regalo", "regir", "regla", "regreso", "rehén", "reino", "reír", "reja", "relato", "relevo", "relieve", "relleno", "reloj", "remar", "remedio", "remo", "rencor", "rendir", "renta", "reparto", "repetir", "reposo", "reptil", "res", "rescate", "resina", "respeto", "resto", "resumen", "retiro", "retorno", "retrato", "reunir", "revés", "revista", "rey", "rezar", "rico", "riego", "rienda", "riesgo", "rifa", "rígido", "rigor", "rincón", "riñón", "río", "riqueza", "risa", "ritmo", "rito", "rizo", "roble", "roce", "rociar", "rodar", "rodeo", "rodilla", "roer", "rojizo", "rojo", "romero", "romper", "ron", "ronco", "ronda", "ropa", "ropero", "rosa", "rosca", "rostro", "rotar", "rubí", "rubor", "rudo", "rueda", "rugir", "ruido", "ruina", "ruleta", "rulo", "rumbo", "rumor", "ruptura", "ruta", "rutina", "sábado", "saber", "sabio", "sable", "sacar", "sagaz", "sagrado", "sala", "saldo", "salero", "salir", "salmón", "salón", "salsa", "salto", "salud", "salvar", "samba", "sanción", "sandía", "sanear", "sangre", "sanidad", "sano", "santo", "sapo", "saque", "sardina", "sartén", "sastre", "satán", "sauna", "saxofón", "sección", "seco", "secreto", "secta", "sed", "seguir", "seis", "sello", "selva", "semana", "semilla", "senda", "sensor", "señal", "señor", "separar", "sepia", "sequía", "ser", "serie", "sermón", "servir", "sesenta", "sesión", "seta", "setenta", "severo", "sexo", "sexto", "sidra", "siesta", "siete", "siglo", "signo", "sílaba", "silbar", "silencio", "silla", "símbolo", "simio", "sirena", "sistema", "sitio", "situar", "sobre", "socio", "sodio", "sol", "solapa", "soldado", "soledad", "sólido", "soltar", "solución", "sombra", "sondeo", "sonido", "sonoro", "sonrisa", "sopa", "soplar", "soporte", "sordo", "sorpresa", "sorteo", "sostén", "sótano", "suave", "subir", "suceso", "sudor", "suegra", "suelo", "sueño", "suerte", "sufrir", "sujeto", "sultán", "sumar", "superar", "suplir", "suponer", "supremo", "sur", "surco", "sureño", "surgir", "susto", "sutil", "tabaco", "tabique", "tabla", "tabú", "taco", "tacto", "tajo", "talar", "talco", "talento", "talla", "talón", "tamaño", "tambor", "tango", "tanque", "tapa", "tapete", "tapia", "tapón", "taquilla", "tarde", "tarea", "tarifa", "tarjeta", "tarot", "tarro", "tarta", "tatuaje", "tauro", "taza", "tazón", "teatro", "techo", "tecla", "técnica", "tejado", "tejer", "tejido", "tela", "teléfono", "tema", "temor", "templo", "tenaz", "tender", "tener", "tenis", "tenso", "teoría", "terapia", "terco", "término", "ternura", "terror", "tesis", "tesoro", "testigo", "tetera", "texto", "tez", "tibio", "tiburón", "tiempo", "tienda", "tierra", "tieso", "tigre", "tijera", "tilde", "timbre", "tímido", "timo", "tinta", "tío", "típico", "tipo", "tira", "tirón", "titán", "títere", "título", "tiza", "toalla", "tobillo", "tocar", "tocino", "todo", "toga", "toldo", "tomar", "tono", "tonto", "topar", "tope", "toque", "tórax", "torero", "tormenta", "torneo", "toro", "torpedo", "torre", "torso", "tortuga", "tos", "tosco", "toser", "tóxico", "trabajo", "tractor", "traer", "tráfico", "trago", "traje", "tramo", "trance", "trato", "trauma", "trazar", "trébol", "tregua", "treinta", "tren", "trepar", "tres", "tribu", "trigo", "tripa", "triste", "triunfo", "trofeo", "trompa", "tronco", "tropa", "trote", "trozo", "truco", "trueno", "trufa", "tubería", "tubo", "tuerto", "tumba", "tumor", "túnel", "túnica", "turbina", "turismo", "turno", "tutor", "ubicar", "úlcera", "umbral", "unidad", "unir", "universo", "uno", "untar", "uña", "urbano", "urbe", "urgente", "urna", "usar", "usuario", "útil", "utopía", "uva", "vaca", "vacío", "vacuna", "vagar", "vago", "vaina", "vajilla", "vale", "válido", "valle", "valor", "válvula", "vampiro", "vara", "variar", "varón", "vaso", "vecino", "vector", "vehículo", "veinte", "vejez", "vela", "velero", "veloz", "vena", "vencer", "venda", "veneno", "vengar", "venir", "venta", "venus", "ver", "verano", "verbo", "verde", "vereda", "verja", "verso", "verter", "vía", "viaje", "vibrar", "vicio", "víctima", "vida", "vídeo", "vidrio", "viejo", "viernes", "vigor", "vil", "villa", "vinagre", "vino", "viñedo", "violín", "viral", "virgo", "virtud", "visor", "víspera", "vista", "vitamina", "viudo", "vivaz", "vivero", "vivir", "vivo", "volcán", "volumen", "volver", "voraz", "votar", "voto", "voz", "vuelo", "vulgar", "yacer", "yate", "yegua", "yema", "yerno", "yeso", "yodo", "yoga", "yogur", "zafiro", "zanja", "zapato", "zarza", "zona", "zorro", "zumo", "zurdo"] +const japaneseWords*: seq[string] = @["あいこくしん", "あいさつ", "あいだ", "あおぞら", "あかちゃん", "あきる", "あけがた", "あける", "あこがれる", "あさい", "あさひ", "あしあと", "あじわう", "あずかる", "あずき", "あそぶ", "あたえる", "あたためる", "あたりまえ", "あたる", "あつい", "あつかう", "あっしゅく", "あつまり", "あつめる", "あてな", "あてはまる", "あひる", "あぶら", "あぶる", "あふれる", "あまい", "あまど", "あまやかす", "あまり", "あみもの", "あめりか", "あやまる", "あゆむ", "あらいぐま", "あらし", "あらすじ", "あらためる", "あらゆる", "あらわす", "ありがとう", "あわせる", "あわてる", "あんい", "あんがい", "あんこ", "あんぜん", "あんてい", "あんない", "あんまり", "いいだす", "いおん", "いがい", "いがく", "いきおい", "いきなり", "いきもの", "いきる", "いくじ", "いくぶん", "いけばな", "いけん", "いこう", "いこく", "いこつ", "いさましい", "いさん", "いしき", "いじゅう", "いじょう", "いじわる", "いずみ", "いずれ", "いせい", "いせえび", "いせかい", "いせき", "いぜん", "いそうろう", "いそがしい", "いだい", "いだく", "いたずら", "いたみ", "いたりあ", "いちおう", "いちじ", "いちど", "いちば", "いちぶ", "いちりゅう", "いつか", "いっしゅん", "いっせい", "いっそう", "いったん", "いっち", "いってい", "いっぽう", "いてざ", "いてん", "いどう", "いとこ", "いない", "いなか", "いねむり", "いのち", "いのる", "いはつ", "いばる", "いはん", "いびき", "いひん", "いふく", "いへん", "いほう", "いみん", "いもうと", "いもたれ", "いもり", "いやがる", "いやす", "いよかん", "いよく", "いらい", "いらすと", "いりぐち", "いりょう", "いれい", "いれもの", "いれる", "いろえんぴつ", "いわい", "いわう", "いわかん", "いわば", "いわゆる", "いんげんまめ", "いんさつ", "いんしょう", "いんよう", "うえき", "うえる", "うおざ", "うがい", "うかぶ", "うかべる", "うきわ", "うくらいな", "うくれれ", "うけたまわる", "うけつけ", "うけとる", "うけもつ", "うける", "うごかす", "うごく", "うこん", "うさぎ", "うしなう", "うしろがみ", "うすい", "うすぎ", "うすぐらい", "うすめる", "うせつ", "うちあわせ", "うちがわ", "うちき", "うちゅう", "うっかり", "うつくしい", "うったえる", "うつる", "うどん", "うなぎ", "うなじ", "うなずく", "うなる", "うねる", "うのう", "うぶげ", "うぶごえ", "うまれる", "うめる", "うもう", "うやまう", "うよく", "うらがえす", "うらぐち", "うらない", "うりあげ", "うりきれ", "うるさい", "うれしい", "うれゆき", "うれる", "うろこ", "うわき", "うわさ", "うんこう", "うんちん", "うんてん", "うんどう", "えいえん", "えいが", "えいきょう", "えいご", "えいせい", "えいぶん", "えいよう", "えいわ", "えおり", "えがお", "えがく", "えきたい", "えくせる", "えしゃく", "えすて", "えつらん", "えのぐ", "えほうまき", "えほん", "えまき", "えもじ", "えもの", "えらい", "えらぶ", "えりあ", "えんえん", "えんかい", "えんぎ", "えんげき", "えんしゅう", "えんぜつ", "えんそく", "えんちょう", "えんとつ", "おいかける", "おいこす", "おいしい", "おいつく", "おうえん", "おうさま", "おうじ", "おうせつ", "おうたい", "おうふく", "おうべい", "おうよう", "おえる", "おおい", "おおう", "おおどおり", "おおや", "おおよそ", "おかえり", "おかず", "おがむ", "おかわり", "おぎなう", "おきる", "おくさま", "おくじょう", "おくりがな", "おくる", "おくれる", "おこす", "おこなう", "おこる", "おさえる", "おさない", "おさめる", "おしいれ", "おしえる", "おじぎ", "おじさん", "おしゃれ", "おそらく", "おそわる", "おたがい", "おたく", "おだやか", "おちつく", "おっと", "おつり", "おでかけ", "おとしもの", "おとなしい", "おどり", "おどろかす", "おばさん", "おまいり", "おめでとう", "おもいで", "おもう", "おもたい", "おもちゃ", "おやつ", "おやゆび", "およぼす", "おらんだ", "おろす", "おんがく", "おんけい", "おんしゃ", "おんせん", "おんだん", "おんちゅう", "おんどけい", "かあつ", "かいが", "がいき", "がいけん", "がいこう", "かいさつ", "かいしゃ", "かいすいよく", "かいぜん", "かいぞうど", "かいつう", "かいてん", "かいとう", "かいふく", "がいへき", "かいほう", "かいよう", "がいらい", "かいわ", "かえる", "かおり", "かかえる", "かがく", "かがし", "かがみ", "かくご", "かくとく", "かざる", "がぞう", "かたい", "かたち", "がちょう", "がっきゅう", "がっこう", "がっさん", "がっしょう", "かなざわし", "かのう", "がはく", "かぶか", "かほう", "かほご", "かまう", "かまぼこ", "かめれおん", "かゆい", "かようび", "からい", "かるい", "かろう", "かわく", "かわら", "がんか", "かんけい", "かんこう", "かんしゃ", "かんそう", "かんたん", "かんち", "がんばる", "きあい", "きあつ", "きいろ", "ぎいん", "きうい", "きうん", "きえる", "きおう", "きおく", "きおち", "きおん", "きかい", "きかく", "きかんしゃ", "ききて", "きくばり", "きくらげ", "きけんせい", "きこう", "きこえる", "きこく", "きさい", "きさく", "きさま", "きさらぎ", "ぎじかがく", "ぎしき", "ぎじたいけん", "ぎじにってい", "ぎじゅつしゃ", "きすう", "きせい", "きせき", "きせつ", "きそう", "きぞく", "きぞん", "きたえる", "きちょう", "きつえん", "ぎっちり", "きつつき", "きつね", "きてい", "きどう", "きどく", "きない", "きなが", "きなこ", "きぬごし", "きねん", "きのう", "きのした", "きはく", "きびしい", "きひん", "きふく", "きぶん", "きぼう", "きほん", "きまる", "きみつ", "きむずかしい", "きめる", "きもだめし", "きもち", "きもの", "きゃく", "きやく", "ぎゅうにく", "きよう", "きょうりゅう", "きらい", "きらく", "きりん", "きれい", "きれつ", "きろく", "ぎろん", "きわめる", "ぎんいろ", "きんかくじ", "きんじょ", "きんようび", "ぐあい", "くいず", "くうかん", "くうき", "くうぐん", "くうこう", "ぐうせい", "くうそう", "ぐうたら", "くうふく", "くうぼ", "くかん", "くきょう", "くげん", "ぐこう", "くさい", "くさき", "くさばな", "くさる", "くしゃみ", "くしょう", "くすのき", "くすりゆび", "くせげ", "くせん", "ぐたいてき", "くださる", "くたびれる", "くちこみ", "くちさき", "くつした", "ぐっすり", "くつろぐ", "くとうてん", "くどく", "くなん", "くねくね", "くのう", "くふう", "くみあわせ", "くみたてる", "くめる", "くやくしょ", "くらす", "くらべる", "くるま", "くれる", "くろう", "くわしい", "ぐんかん", "ぐんしょく", "ぐんたい", "ぐんて", "けあな", "けいかく", "けいけん", "けいこ", "けいさつ", "げいじゅつ", "けいたい", "げいのうじん", "けいれき", "けいろ", "けおとす", "けおりもの", "げきか", "げきげん", "げきだん", "げきちん", "げきとつ", "げきは", "げきやく", "げこう", "げこくじょう", "げざい", "けさき", "げざん", "けしき", "けしごむ", "けしょう", "げすと", "けたば", "けちゃっぷ", "けちらす", "けつあつ", "けつい", "けつえき", "けっこん", "けつじょ", "けっせき", "けってい", "けつまつ", "げつようび", "げつれい", "けつろん", "げどく", "けとばす", "けとる", "けなげ", "けなす", "けなみ", "けぬき", "げねつ", "けねん", "けはい", "げひん", "けぶかい", "げぼく", "けまり", "けみかる", "けむし", "けむり", "けもの", "けらい", "けろけろ", "けわしい", "けんい", "けんえつ", "けんお", "けんか", "げんき", "けんげん", "けんこう", "けんさく", "けんしゅう", "けんすう", "げんそう", "けんちく", "けんてい", "けんとう", "けんない", "けんにん", "げんぶつ", "けんま", "けんみん", "けんめい", "けんらん", "けんり", "こあくま", "こいぬ", "こいびと", "ごうい", "こうえん", "こうおん", "こうかん", "ごうきゅう", "ごうけい", "こうこう", "こうさい", "こうじ", "こうすい", "ごうせい", "こうそく", "こうたい", "こうちゃ", "こうつう", "こうてい", "こうどう", "こうない", "こうはい", "ごうほう", "ごうまん", "こうもく", "こうりつ", "こえる", "こおり", "ごかい", "ごがつ", "ごかん", "こくご", "こくさい", "こくとう", "こくない", "こくはく", "こぐま", "こけい", "こける", "ここのか", "こころ", "こさめ", "こしつ", "こすう", "こせい", "こせき", "こぜん", "こそだて", "こたい", "こたえる", "こたつ", "こちょう", "こっか", "こつこつ", "こつばん", "こつぶ", "こてい", "こてん", "ことがら", "ことし", "ことば", "ことり", "こなごな", "こねこね", "このまま", "このみ", "このよ", "ごはん", "こひつじ", "こふう", "こふん", "こぼれる", "ごまあぶら", "こまかい", "ごますり", "こまつな", "こまる", "こむぎこ", "こもじ", "こもち", "こもの", "こもん", "こやく", "こやま", "こゆう", "こゆび", "こよい", "こよう", "こりる", "これくしょん", "ころっけ", "こわもて", "こわれる", "こんいん", "こんかい", "こんき", "こんしゅう", "こんすい", "こんだて", "こんとん", "こんなん", "こんびに", "こんぽん", "こんまけ", "こんや", "こんれい", "こんわく", "ざいえき", "さいかい", "さいきん", "ざいげん", "ざいこ", "さいしょ", "さいせい", "ざいたく", "ざいちゅう", "さいてき", "ざいりょう", "さうな", "さかいし", "さがす", "さかな", "さかみち", "さがる", "さぎょう", "さくし", "さくひん", "さくら", "さこく", "さこつ", "さずかる", "ざせき", "さたん", "さつえい", "ざつおん", "ざっか", "ざつがく", "さっきょく", "ざっし", "さつじん", "ざっそう", "さつたば", "さつまいも", "さてい", "さといも", "さとう", "さとおや", "さとし", "さとる", "さのう", "さばく", "さびしい", "さべつ", "さほう", "さほど", "さます", "さみしい", "さみだれ", "さむけ", "さめる", "さやえんどう", "さゆう", "さよう", "さよく", "さらだ", "ざるそば", "さわやか", "さわる", "さんいん", "さんか", "さんきゃく", "さんこう", "さんさい", "ざんしょ", "さんすう", "さんせい", "さんそ", "さんち", "さんま", "さんみ", "さんらん", "しあい", "しあげ", "しあさって", "しあわせ", "しいく", "しいん", "しうち", "しえい", "しおけ", "しかい", "しかく", "じかん", "しごと", "しすう", "じだい", "したうけ", "したぎ", "したて", "したみ", "しちょう", "しちりん", "しっかり", "しつじ", "しつもん", "してい", "してき", "してつ", "じてん", "じどう", "しなぎれ", "しなもの", "しなん", "しねま", "しねん", "しのぐ", "しのぶ", "しはい", "しばかり", "しはつ", "しはらい", "しはん", "しひょう", "しふく", "じぶん", "しへい", "しほう", "しほん", "しまう", "しまる", "しみん", "しむける", "じむしょ", "しめい", "しめる", "しもん", "しゃいん", "しゃうん", "しゃおん", "じゃがいも", "しやくしょ", "しゃくほう", "しゃけん", "しゃこ", "しゃざい", "しゃしん", "しゃせん", "しゃそう", "しゃたい", "しゃちょう", "しゃっきん", "じゃま", "しゃりん", "しゃれい", "じゆう", "じゅうしょ", "しゅくはく", "じゅしん", "しゅっせき", "しゅみ", "しゅらば", "じゅんばん", "しょうかい", "しょくたく", "しょっけん", "しょどう", "しょもつ", "しらせる", "しらべる", "しんか", "しんこう", "じんじゃ", "しんせいじ", "しんちく", "しんりん", "すあげ", "すあし", "すあな", "ずあん", "すいえい", "すいか", "すいとう", "ずいぶん", "すいようび", "すうがく", "すうじつ", "すうせん", "すおどり", "すきま", "すくう", "すくない", "すける", "すごい", "すこし", "ずさん", "すずしい", "すすむ", "すすめる", "すっかり", "ずっしり", "ずっと", "すてき", "すてる", "すねる", "すのこ", "すはだ", "すばらしい", "ずひょう", "ずぶぬれ", "すぶり", "すふれ", "すべて", "すべる", "ずほう", "すぼん", "すまい", "すめし", "すもう", "すやき", "すらすら", "するめ", "すれちがう", "すろっと", "すわる", "すんぜん", "すんぽう", "せあぶら", "せいかつ", "せいげん", "せいじ", "せいよう", "せおう", "せかいかん", "せきにん", "せきむ", "せきゆ", "せきらんうん", "せけん", "せこう", "せすじ", "せたい", "せたけ", "せっかく", "せっきゃく", "ぜっく", "せっけん", "せっこつ", "せっさたくま", "せつぞく", "せつだん", "せつでん", "せっぱん", "せつび", "せつぶん", "せつめい", "せつりつ", "せなか", "せのび", "せはば", "せびろ", "せぼね", "せまい", "せまる", "せめる", "せもたれ", "せりふ", "ぜんあく", "せんい", "せんえい", "せんか", "せんきょ", "せんく", "せんげん", "ぜんご", "せんさい", "せんしゅ", "せんすい", "せんせい", "せんぞ", "せんたく", "せんちょう", "せんてい", "せんとう", "せんぬき", "せんねん", "せんぱい", "ぜんぶ", "ぜんぽう", "せんむ", "せんめんじょ", "せんもん", "せんやく", "せんゆう", "せんよう", "ぜんら", "ぜんりゃく", "せんれい", "せんろ", "そあく", "そいとげる", "そいね", "そうがんきょう", "そうき", "そうご", "そうしん", "そうだん", "そうなん", "そうび", "そうめん", "そうり", "そえもの", "そえん", "そがい", "そげき", "そこう", "そこそこ", "そざい", "そしな", "そせい", "そせん", "そそぐ", "そだてる", "そつう", "そつえん", "そっかん", "そつぎょう", "そっけつ", "そっこう", "そっせん", "そっと", "そとがわ", "そとづら", "そなえる", "そなた", "そふぼ", "そぼく", "そぼろ", "そまつ", "そまる", "そむく", "そむりえ", "そめる", "そもそも", "そよかぜ", "そらまめ", "そろう", "そんかい", "そんけい", "そんざい", "そんしつ", "そんぞく", "そんちょう", "ぞんび", "ぞんぶん", "そんみん", "たあい", "たいいん", "たいうん", "たいえき", "たいおう", "だいがく", "たいき", "たいぐう", "たいけん", "たいこ", "たいざい", "だいじょうぶ", "だいすき", "たいせつ", "たいそう", "だいたい", "たいちょう", "たいてい", "だいどころ", "たいない", "たいねつ", "たいのう", "たいはん", "だいひょう", "たいふう", "たいへん", "たいほ", "たいまつばな", "たいみんぐ", "たいむ", "たいめん", "たいやき", "たいよう", "たいら", "たいりょく", "たいる", "たいわん", "たうえ", "たえる", "たおす", "たおる", "たおれる", "たかい", "たかね", "たきび", "たくさん", "たこく", "たこやき", "たさい", "たしざん", "だじゃれ", "たすける", "たずさわる", "たそがれ", "たたかう", "たたく", "ただしい", "たたみ", "たちばな", "だっかい", "だっきゃく", "だっこ", "だっしゅつ", "だったい", "たてる", "たとえる", "たなばた", "たにん", "たぬき", "たのしみ", "たはつ", "たぶん", "たべる", "たぼう", "たまご", "たまる", "だむる", "ためいき", "ためす", "ためる", "たもつ", "たやすい", "たよる", "たらす", "たりきほんがん", "たりょう", "たりる", "たると", "たれる", "たれんと", "たろっと", "たわむれる", "だんあつ", "たんい", "たんおん", "たんか", "たんき", "たんけん", "たんご", "たんさん", "たんじょうび", "だんせい", "たんそく", "たんたい", "だんち", "たんてい", "たんとう", "だんな", "たんにん", "だんねつ", "たんのう", "たんぴん", "だんぼう", "たんまつ", "たんめい", "だんれつ", "だんろ", "だんわ", "ちあい", "ちあん", "ちいき", "ちいさい", "ちえん", "ちかい", "ちから", "ちきゅう", "ちきん", "ちけいず", "ちけん", "ちこく", "ちさい", "ちしき", "ちしりょう", "ちせい", "ちそう", "ちたい", "ちたん", "ちちおや", "ちつじょ", "ちてき", "ちてん", "ちぬき", "ちぬり", "ちのう", "ちひょう", "ちへいせん", "ちほう", "ちまた", "ちみつ", "ちみどろ", "ちめいど", "ちゃんこなべ", "ちゅうい", "ちゆりょく", "ちょうし", "ちょさくけん", "ちらし", "ちらみ", "ちりがみ", "ちりょう", "ちるど", "ちわわ", "ちんたい", "ちんもく", "ついか", "ついたち", "つうか", "つうじょう", "つうはん", "つうわ", "つかう", "つかれる", "つくね", "つくる", "つけね", "つける", "つごう", "つたえる", "つづく", "つつじ", "つつむ", "つとめる", "つながる", "つなみ", "つねづね", "つのる", "つぶす", "つまらない", "つまる", "つみき", "つめたい", "つもり", "つもる", "つよい", "つるぼ", "つるみく", "つわもの", "つわり", "てあし", "てあて", "てあみ", "ていおん", "ていか", "ていき", "ていけい", "ていこく", "ていさつ", "ていし", "ていせい", "ていたい", "ていど", "ていねい", "ていひょう", "ていへん", "ていぼう", "てうち", "ておくれ", "てきとう", "てくび", "でこぼこ", "てさぎょう", "てさげ", "てすり", "てそう", "てちがい", "てちょう", "てつがく", "てつづき", "でっぱ", "てつぼう", "てつや", "でぬかえ", "てぬき", "てぬぐい", "てのひら", "てはい", "てぶくろ", "てふだ", "てほどき", "てほん", "てまえ", "てまきずし", "てみじか", "てみやげ", "てらす", "てれび", "てわけ", "てわたし", "でんあつ", "てんいん", "てんかい", "てんき", "てんぐ", "てんけん", "てんごく", "てんさい", "てんし", "てんすう", "でんち", "てんてき", "てんとう", "てんない", "てんぷら", "てんぼうだい", "てんめつ", "てんらんかい", "でんりょく", "でんわ", "どあい", "といれ", "どうかん", "とうきゅう", "どうぐ", "とうし", "とうむぎ", "とおい", "とおか", "とおく", "とおす", "とおる", "とかい", "とかす", "ときおり", "ときどき", "とくい", "とくしゅう", "とくてん", "とくに", "とくべつ", "とけい", "とける", "とこや", "とさか", "としょかん", "とそう", "とたん", "とちゅう", "とっきゅう", "とっくん", "とつぜん", "とつにゅう", "とどける", "ととのえる", "とない", "となえる", "となり", "とのさま", "とばす", "どぶがわ", "とほう", "とまる", "とめる", "ともだち", "ともる", "どようび", "とらえる", "とんかつ", "どんぶり", "ないかく", "ないこう", "ないしょ", "ないす", "ないせん", "ないそう", "なおす", "ながい", "なくす", "なげる", "なこうど", "なさけ", "なたでここ", "なっとう", "なつやすみ", "ななおし", "なにごと", "なにもの", "なにわ", "なのか", "なふだ", "なまいき", "なまえ", "なまみ", "なみだ", "なめらか", "なめる", "なやむ", "ならう", "ならび", "ならぶ", "なれる", "なわとび", "なわばり", "にあう", "にいがた", "にうけ", "におい", "にかい", "にがて", "にきび", "にくしみ", "にくまん", "にげる", "にさんかたんそ", "にしき", "にせもの", "にちじょう", "にちようび", "にっか", "にっき", "にっけい", "にっこう", "にっさん", "にっしょく", "にっすう", "にっせき", "にってい", "になう", "にほん", "にまめ", "にもつ", "にやり", "にゅういん", "にりんしゃ", "にわとり", "にんい", "にんか", "にんき", "にんげん", "にんしき", "にんずう", "にんそう", "にんたい", "にんち", "にんてい", "にんにく", "にんぷ", "にんまり", "にんむ", "にんめい", "にんよう", "ぬいくぎ", "ぬかす", "ぬぐいとる", "ぬぐう", "ぬくもり", "ぬすむ", "ぬまえび", "ぬめり", "ぬらす", "ぬんちゃく", "ねあげ", "ねいき", "ねいる", "ねいろ", "ねぐせ", "ねくたい", "ねくら", "ねこぜ", "ねこむ", "ねさげ", "ねすごす", "ねそべる", "ねだん", "ねつい", "ねっしん", "ねつぞう", "ねったいぎょ", "ねぶそく", "ねふだ", "ねぼう", "ねほりはほり", "ねまき", "ねまわし", "ねみみ", "ねむい", "ねむたい", "ねもと", "ねらう", "ねわざ", "ねんいり", "ねんおし", "ねんかん", "ねんきん", "ねんぐ", "ねんざ", "ねんし", "ねんちゃく", "ねんど", "ねんぴ", "ねんぶつ", "ねんまつ", "ねんりょう", "ねんれい", "のいず", "のおづま", "のがす", "のきなみ", "のこぎり", "のこす", "のこる", "のせる", "のぞく", "のぞむ", "のたまう", "のちほど", "のっく", "のばす", "のはら", "のべる", "のぼる", "のみもの", "のやま", "のらいぬ", "のらねこ", "のりもの", "のりゆき", "のれん", "のんき", "ばあい", "はあく", "ばあさん", "ばいか", "ばいく", "はいけん", "はいご", "はいしん", "はいすい", "はいせん", "はいそう", "はいち", "ばいばい", "はいれつ", "はえる", "はおる", "はかい", "ばかり", "はかる", "はくしゅ", "はけん", "はこぶ", "はさみ", "はさん", "はしご", "ばしょ", "はしる", "はせる", "ぱそこん", "はそん", "はたん", "はちみつ", "はつおん", "はっかく", "はづき", "はっきり", "はっくつ", "はっけん", "はっこう", "はっさん", "はっしん", "はったつ", "はっちゅう", "はってん", "はっぴょう", "はっぽう", "はなす", "はなび", "はにかむ", "はぶらし", "はみがき", "はむかう", "はめつ", "はやい", "はやし", "はらう", "はろうぃん", "はわい", "はんい", "はんえい", "はんおん", "はんかく", "はんきょう", "ばんぐみ", "はんこ", "はんしゃ", "はんすう", "はんだん", "ぱんち", "ぱんつ", "はんてい", "はんとし", "はんのう", "はんぱ", "はんぶん", "はんぺん", "はんぼうき", "はんめい", "はんらん", "はんろん", "ひいき", "ひうん", "ひえる", "ひかく", "ひかり", "ひかる", "ひかん", "ひくい", "ひけつ", "ひこうき", "ひこく", "ひさい", "ひさしぶり", "ひさん", "びじゅつかん", "ひしょ", "ひそか", "ひそむ", "ひたむき", "ひだり", "ひたる", "ひつぎ", "ひっこし", "ひっし", "ひつじゅひん", "ひっす", "ひつぜん", "ぴったり", "ぴっちり", "ひつよう", "ひてい", "ひとごみ", "ひなまつり", "ひなん", "ひねる", "ひはん", "ひびく", "ひひょう", "ひほう", "ひまわり", "ひまん", "ひみつ", "ひめい", "ひめじし", "ひやけ", "ひやす", "ひよう", "びょうき", "ひらがな", "ひらく", "ひりつ", "ひりょう", "ひるま", "ひるやすみ", "ひれい", "ひろい", "ひろう", "ひろき", "ひろゆき", "ひんかく", "ひんけつ", "ひんこん", "ひんしゅ", "ひんそう", "ぴんち", "ひんぱん", "びんぼう", "ふあん", "ふいうち", "ふうけい", "ふうせん", "ぷうたろう", "ふうとう", "ふうふ", "ふえる", "ふおん", "ふかい", "ふきん", "ふくざつ", "ふくぶくろ", "ふこう", "ふさい", "ふしぎ", "ふじみ", "ふすま", "ふせい", "ふせぐ", "ふそく", "ぶたにく", "ふたん", "ふちょう", "ふつう", "ふつか", "ふっかつ", "ふっき", "ふっこく", "ぶどう", "ふとる", "ふとん", "ふのう", "ふはい", "ふひょう", "ふへん", "ふまん", "ふみん", "ふめつ", "ふめん", "ふよう", "ふりこ", "ふりる", "ふるい", "ふんいき", "ぶんがく", "ぶんぐ", "ふんしつ", "ぶんせき", "ふんそう", "ぶんぽう", "へいあん", "へいおん", "へいがい", "へいき", "へいげん", "へいこう", "へいさ", "へいしゃ", "へいせつ", "へいそ", "へいたく", "へいてん", "へいねつ", "へいわ", "へきが", "へこむ", "べにいろ", "べにしょうが", "へらす", "へんかん", "べんきょう", "べんごし", "へんさい", "へんたい", "べんり", "ほあん", "ほいく", "ぼうぎょ", "ほうこく", "ほうそう", "ほうほう", "ほうもん", "ほうりつ", "ほえる", "ほおん", "ほかん", "ほきょう", "ぼきん", "ほくろ", "ほけつ", "ほけん", "ほこう", "ほこる", "ほしい", "ほしつ", "ほしゅ", "ほしょう", "ほせい", "ほそい", "ほそく", "ほたて", "ほたる", "ぽちぶくろ", "ほっきょく", "ほっさ", "ほったん", "ほとんど", "ほめる", "ほんい", "ほんき", "ほんけ", "ほんしつ", "ほんやく", "まいにち", "まかい", "まかせる", "まがる", "まける", "まこと", "まさつ", "まじめ", "ますく", "まぜる", "まつり", "まとめ", "まなぶ", "まぬけ", "まねく", "まほう", "まもる", "まゆげ", "まよう", "まろやか", "まわす", "まわり", "まわる", "まんが", "まんきつ", "まんぞく", "まんなか", "みいら", "みうち", "みえる", "みがく", "みかた", "みかん", "みけん", "みこん", "みじかい", "みすい", "みすえる", "みせる", "みっか", "みつかる", "みつける", "みてい", "みとめる", "みなと", "みなみかさい", "みねらる", "みのう", "みのがす", "みほん", "みもと", "みやげ", "みらい", "みりょく", "みわく", "みんか", "みんぞく", "むいか", "むえき", "むえん", "むかい", "むかう", "むかえ", "むかし", "むぎちゃ", "むける", "むげん", "むさぼる", "むしあつい", "むしば", "むじゅん", "むしろ", "むすう", "むすこ", "むすぶ", "むすめ", "むせる", "むせん", "むちゅう", "むなしい", "むのう", "むやみ", "むよう", "むらさき", "むりょう", "むろん", "めいあん", "めいうん", "めいえん", "めいかく", "めいきょく", "めいさい", "めいし", "めいそう", "めいぶつ", "めいれい", "めいわく", "めぐまれる", "めざす", "めした", "めずらしい", "めだつ", "めまい", "めやす", "めんきょ", "めんせき", "めんどう", "もうしあげる", "もうどうけん", "もえる", "もくし", "もくてき", "もくようび", "もちろん", "もどる", "もらう", "もんく", "もんだい", "やおや", "やける", "やさい", "やさしい", "やすい", "やすたろう", "やすみ", "やせる", "やそう", "やたい", "やちん", "やっと", "やっぱり", "やぶる", "やめる", "ややこしい", "やよい", "やわらかい", "ゆうき", "ゆうびんきょく", "ゆうべ", "ゆうめい", "ゆけつ", "ゆしゅつ", "ゆせん", "ゆそう", "ゆたか", "ゆちゃく", "ゆでる", "ゆにゅう", "ゆびわ", "ゆらい", "ゆれる", "ようい", "ようか", "ようきゅう", "ようじ", "ようす", "ようちえん", "よかぜ", "よかん", "よきん", "よくせい", "よくぼう", "よけい", "よごれる", "よさん", "よしゅう", "よそう", "よそく", "よっか", "よてい", "よどがわく", "よねつ", "よやく", "よゆう", "よろこぶ", "よろしい", "らいう", "らくがき", "らくご", "らくさつ", "らくだ", "らしんばん", "らせん", "らぞく", "らたい", "らっか", "られつ", "りえき", "りかい", "りきさく", "りきせつ", "りくぐん", "りくつ", "りけん", "りこう", "りせい", "りそう", "りそく", "りてん", "りねん", "りゆう", "りゅうがく", "りよう", "りょうり", "りょかん", "りょくちゃ", "りょこう", "りりく", "りれき", "りろん", "りんご", "るいけい", "るいさい", "るいじ", "るいせき", "るすばん", "るりがわら", "れいかん", "れいぎ", "れいせい", "れいぞうこ", "れいとう", "れいぼう", "れきし", "れきだい", "れんあい", "れんけい", "れんこん", "れんさい", "れんしゅう", "れんぞく", "れんらく", "ろうか", "ろうご", "ろうじん", "ろうそく", "ろくが", "ろこつ", "ろじうら", "ろしゅつ", "ろせん", "ろてん", "ろめん", "ろれつ", "ろんぎ", "ろんぱ", "ろんぶん", "ろんり", "わかす", "わかめ", "わかやま", "わかれる", "わしつ", "わじまし", "わすれもの", "わらう", "われる"] +const italianWords*: seq[string] = @["abaco", "abbaglio", "abbinato", "abete", "abisso", "abolire", "abrasivo", "abrogato", "accadere", "accenno", "accusato", "acetone", "achille", "acido", "acqua", "acre", "acrilico", "acrobata", "acuto", "adagio", "addebito", "addome", "adeguato", "aderire", "adipe", "adottare", "adulare", "affabile", "affetto", "affisso", "affranto", "aforisma", "afoso", "africano", "agave", "agente", "agevole", "aggancio", "agire", "agitare", "agonismo", "agricolo", "agrumeto", "aguzzo", "alabarda", "alato", "albatro", "alberato", "albo", "albume", "alce", "alcolico", "alettone", "alfa", "algebra", "aliante", "alibi", "alimento", "allagato", "allegro", "allievo", "allodola", "allusivo", "almeno", "alogeno", "alpaca", "alpestre", "altalena", "alterno", "alticcio", "altrove", "alunno", "alveolo", "alzare", "amalgama", "amanita", "amarena", "ambito", "ambrato", "ameba", "america", "ametista", "amico", "ammasso", "ammenda", "ammirare", "ammonito", "amore", "ampio", "ampliare", "amuleto", "anacardo", "anagrafe", "analista", "anarchia", "anatra", "anca", "ancella", "ancora", "andare", "andrea", "anello", "angelo", "angolare", "angusto", "anima", "annegare", "annidato", "anno", "annuncio", "anonimo", "anticipo", "anzi", "apatico", "apertura", "apode", "apparire", "appetito", "appoggio", "approdo", "appunto", "aprile", "arabica", "arachide", "aragosta", "araldica", "arancio", "aratura", "arazzo", "arbitro", "archivio", "ardito", "arenile", "argento", "argine", "arguto", "aria", "armonia", "arnese", "arredato", "arringa", "arrosto", "arsenico", "arso", "artefice", "arzillo", "asciutto", "ascolto", "asepsi", "asettico", "asfalto", "asino", "asola", "aspirato", "aspro", "assaggio", "asse", "assoluto", "assurdo", "asta", "astenuto", "astice", "astratto", "atavico", "ateismo", "atomico", "atono", "attesa", "attivare", "attorno", "attrito", "attuale", "ausilio", "austria", "autista", "autonomo", "autunno", "avanzato", "avere", "avvenire", "avviso", "avvolgere", "azione", "azoto", "azzimo", "azzurro", "babele", "baccano", "bacino", "baco", "badessa", "badilata", "bagnato", "baita", "balcone", "baldo", "balena", "ballata", "balzano", "bambino", "bandire", "baraonda", "barbaro", "barca", "baritono", "barlume", "barocco", "basilico", "basso", "batosta", "battuto", "baule", "bava", "bavosa", "becco", "beffa", "belgio", "belva", "benda", "benevole", "benigno", "benzina", "bere", "berlina", "beta", "bibita", "bici", "bidone", "bifido", "biga", "bilancia", "bimbo", "binocolo", "biologo", "bipede", "bipolare", "birbante", "birra", "biscotto", "bisesto", "bisnonno", "bisonte", "bisturi", "bizzarro", "blando", "blatta", "bollito", "bonifico", "bordo", "bosco", "botanico", "bottino", "bozzolo", "braccio", "bradipo", "brama", "branca", "bravura", "bretella", "brevetto", "brezza", "briglia", "brillante", "brindare", "broccolo", "brodo", "bronzina", "brullo", "bruno", "bubbone", "buca", "budino", "buffone", "buio", "bulbo", "buono", "burlone", "burrasca", "bussola", "busta", "cadetto", "caduco", "calamaro", "calcolo", "calesse", "calibro", "calmo", "caloria", "cambusa", "camerata", "camicia", "cammino", "camola", "campale", "canapa", "candela", "cane", "canino", "canotto", "cantina", "capace", "capello", "capitolo", "capogiro", "cappero", "capra", "capsula", "carapace", "carcassa", "cardo", "carisma", "carovana", "carretto", "cartolina", "casaccio", "cascata", "caserma", "caso", "cassone", "castello", "casuale", "catasta", "catena", "catrame", "cauto", "cavillo", "cedibile", "cedrata", "cefalo", "celebre", "cellulare", "cena", "cenone", "centesimo", "ceramica", "cercare", "certo", "cerume", "cervello", "cesoia", "cespo", "ceto", "chela", "chiaro", "chicca", "chiedere", "chimera", "china", "chirurgo", "chitarra", "ciao", "ciclismo", "cifrare", "cigno", "cilindro", "ciottolo", "circa", "cirrosi", "citrico", "cittadino", "ciuffo", "civetta", "civile", "classico", "clinica", "cloro", "cocco", "codardo", "codice", "coerente", "cognome", "collare", "colmato", "colore", "colposo", "coltivato", "colza", "coma", "cometa", "commando", "comodo", "computer", "comune", "conciso", "condurre", "conferma", "congelare", "coniuge", "connesso", "conoscere", "consumo", "continuo", "convegno", "coperto", "copione", "coppia", "copricapo", "corazza", "cordata", "coricato", "cornice", "corolla", "corpo", "corredo", "corsia", "cortese", "cosmico", "costante", "cottura", "covato", "cratere", "cravatta", "creato", "credere", "cremoso", "crescita", "creta", "criceto", "crinale", "crisi", "critico", "croce", "cronaca", "crostata", "cruciale", "crusca", "cucire", "cuculo", "cugino", "cullato", "cupola", "curatore", "cursore", "curvo", "cuscino", "custode", "dado", "daino", "dalmata", "damerino", "daniela", "dannoso", "danzare", "datato", "davanti", "davvero", "debutto", "decennio", "deciso", "declino", "decollo", "decreto", "dedicato", "definito", "deforme", "degno", "delegare", "delfino", "delirio", "delta", "demenza", "denotato", "dentro", "deposito", "derapata", "derivare", "deroga", "descritto", "deserto", "desiderio", "desumere", "detersivo", "devoto", "diametro", "dicembre", "diedro", "difeso", "diffuso", "digerire", "digitale", "diluvio", "dinamico", "dinnanzi", "dipinto", "diploma", "dipolo", "diradare", "dire", "dirotto", "dirupo", "disagio", "discreto", "disfare", "disgelo", "disposto", "distanza", "disumano", "dito", "divano", "divelto", "dividere", "divorato", "doblone", "docente", "doganale", "dogma", "dolce", "domato", "domenica", "dominare", "dondolo", "dono", "dormire", "dote", "dottore", "dovuto", "dozzina", "drago", "druido", "dubbio", "dubitare", "ducale", "duna", "duomo", "duplice", "duraturo", "ebano", "eccesso", "ecco", "eclissi", "economia", "edera", "edicola", "edile", "editoria", "educare", "egemonia", "egli", "egoismo", "egregio", "elaborato", "elargire", "elegante", "elencato", "eletto", "elevare", "elfico", "elica", "elmo", "elsa", "eluso", "emanato", "emblema", "emesso", "emiro", "emotivo", "emozione", "empirico", "emulo", "endemico", "enduro", "energia", "enfasi", "enoteca", "entrare", "enzima", "epatite", "epilogo", "episodio", "epocale", "eppure", "equatore", "erario", "erba", "erboso", "erede", "eremita", "erigere", "ermetico", "eroe", "erosivo", "errante", "esagono", "esame", "esanime", "esaudire", "esca", "esempio", "esercito", "esibito", "esigente", "esistere", "esito", "esofago", "esortato", "esoso", "espanso", "espresso", "essenza", "esso", "esteso", "estimare", "estonia", "estroso", "esultare", "etilico", "etnico", "etrusco", "etto", "euclideo", "europa", "evaso", "evidenza", "evitato", "evoluto", "evviva", "fabbrica", "faccenda", "fachiro", "falco", "famiglia", "fanale", "fanfara", "fango", "fantasma", "fare", "farfalla", "farinoso", "farmaco", "fascia", "fastoso", "fasullo", "faticare", "fato", "favoloso", "febbre", "fecola", "fede", "fegato", "felpa", "feltro", "femmina", "fendere", "fenomeno", "fermento", "ferro", "fertile", "fessura", "festivo", "fetta", "feudo", "fiaba", "fiducia", "fifa", "figurato", "filo", "finanza", "finestra", "finire", "fiore", "fiscale", "fisico", "fiume", "flacone", "flamenco", "flebo", "flemma", "florido", "fluente", "fluoro", "fobico", "focaccia", "focoso", "foderato", "foglio", "folata", "folclore", "folgore", "fondente", "fonetico", "fonia", "fontana", "forbito", "forchetta", "foresta", "formica", "fornaio", "foro", "fortezza", "forzare", "fosfato", "fosso", "fracasso", "frana", "frassino", "fratello", "freccetta", "frenata", "fresco", "frigo", "frollino", "fronde", "frugale", "frutta", "fucilata", "fucsia", "fuggente", "fulmine", "fulvo", "fumante", "fumetto", "fumoso", "fune", "funzione", "fuoco", "furbo", "furgone", "furore", "fuso", "futile", "gabbiano", "gaffe", "galateo", "gallina", "galoppo", "gambero", "gamma", "garanzia", "garbo", "garofano", "garzone", "gasdotto", "gasolio", "gastrico", "gatto", "gaudio", "gazebo", "gazzella", "geco", "gelatina", "gelso", "gemello", "gemmato", "gene", "genitore", "gennaio", "genotipo", "gergo", "ghepardo", "ghiaccio", "ghisa", "giallo", "gilda", "ginepro", "giocare", "gioiello", "giorno", "giove", "girato", "girone", "gittata", "giudizio", "giurato", "giusto", "globulo", "glutine", "gnomo", "gobba", "golf", "gomito", "gommone", "gonfio", "gonna", "governo", "gracile", "grado", "grafico", "grammo", "grande", "grattare", "gravoso", "grazia", "greca", "gregge", "grifone", "grigio", "grinza", "grotta", "gruppo", "guadagno", "guaio", "guanto", "guardare", "gufo", "guidare", "ibernato", "icona", "identico", "idillio", "idolo", "idra", "idrico", "idrogeno", "igiene", "ignaro", "ignorato", "ilare", "illeso", "illogico", "illudere", "imballo", "imbevuto", "imbocco", "imbuto", "immane", "immerso", "immolato", "impacco", "impeto", "impiego", "importo", "impronta", "inalare", "inarcare", "inattivo", "incanto", "incendio", "inchino", "incisivo", "incluso", "incontro", "incrocio", "incubo", "indagine", "india", "indole", "inedito", "infatti", "infilare", "inflitto", "ingaggio", "ingegno", "inglese", "ingordo", "ingrosso", "innesco", "inodore", "inoltrare", "inondato", "insano", "insetto", "insieme", "insonnia", "insulina", "intasato", "intero", "intonaco", "intuito", "inumidire", "invalido", "invece", "invito", "iperbole", "ipnotico", "ipotesi", "ippica", "iride", "irlanda", "ironico", "irrigato", "irrorare", "isolato", "isotopo", "isterico", "istituto", "istrice", "italia", "iterare", "labbro", "labirinto", "lacca", "lacerato", "lacrima", "lacuna", "laddove", "lago", "lampo", "lancetta", "lanterna", "lardoso", "larga", "laringe", "lastra", "latenza", "latino", "lattuga", "lavagna", "lavoro", "legale", "leggero", "lembo", "lentezza", "lenza", "leone", "lepre", "lesivo", "lessato", "lesto", "letterale", "leva", "levigato", "libero", "lido", "lievito", "lilla", "limatura", "limitare", "limpido", "lineare", "lingua", "liquido", "lira", "lirica", "lisca", "lite", "litigio", "livrea", "locanda", "lode", "logica", "lombare", "londra", "longevo", "loquace", "lorenzo", "loto", "lotteria", "luce", "lucidato", "lumaca", "luminoso", "lungo", "lupo", "luppolo", "lusinga", "lusso", "lutto", "macabro", "macchina", "macero", "macinato", "madama", "magico", "maglia", "magnete", "magro", "maiolica", "malafede", "malgrado", "malinteso", "malsano", "malto", "malumore", "mana", "mancia", "mandorla", "mangiare", "manifesto", "mannaro", "manovra", "mansarda", "mantide", "manubrio", "mappa", "maratona", "marcire", "maretta", "marmo", "marsupio", "maschera", "massaia", "mastino", "materasso", "matricola", "mattone", "maturo", "mazurca", "meandro", "meccanico", "mecenate", "medesimo", "meditare", "mega", "melassa", "melis", "melodia", "meninge", "meno", "mensola", "mercurio", "merenda", "merlo", "meschino", "mese", "messere", "mestolo", "metallo", "metodo", "mettere", "miagolare", "mica", "micelio", "michele", "microbo", "midollo", "miele", "migliore", "milano", "milite", "mimosa", "minerale", "mini", "minore", "mirino", "mirtillo", "miscela", "missiva", "misto", "misurare", "mitezza", "mitigare", "mitra", "mittente", "mnemonico", "modello", "modifica", "modulo", "mogano", "mogio", "mole", "molosso", "monastero", "monco", "mondina", "monetario", "monile", "monotono", "monsone", "montato", "monviso", "mora", "mordere", "morsicato", "mostro", "motivato", "motosega", "motto", "movenza", "movimento", "mozzo", "mucca", "mucosa", "muffa", "mughetto", "mugnaio", "mulatto", "mulinello", "multiplo", "mummia", "munto", "muovere", "murale", "musa", "muscolo", "musica", "mutevole", "muto", "nababbo", "nafta", "nanometro", "narciso", "narice", "narrato", "nascere", "nastrare", "naturale", "nautica", "naviglio", "nebulosa", "necrosi", "negativo", "negozio", "nemmeno", "neofita", "neretto", "nervo", "nessuno", "nettuno", "neutrale", "neve", "nevrotico", "nicchia", "ninfa", "nitido", "nobile", "nocivo", "nodo", "nome", "nomina", "nordico", "normale", "norvegese", "nostrano", "notare", "notizia", "notturno", "novella", "nucleo", "nulla", "numero", "nuovo", "nutrire", "nuvola", "nuziale", "oasi", "obbedire", "obbligo", "obelisco", "oblio", "obolo", "obsoleto", "occasione", "occhio", "occidente", "occorrere", "occultare", "ocra", "oculato", "odierno", "odorare", "offerta", "offrire", "offuscato", "oggetto", "oggi", "ognuno", "olandese", "olfatto", "oliato", "oliva", "ologramma", "oltre", "omaggio", "ombelico", "ombra", "omega", "omissione", "ondoso", "onere", "onice", "onnivoro", "onorevole", "onta", "operato", "opinione", "opposto", "oracolo", "orafo", "ordine", "orecchino", "orefice", "orfano", "organico", "origine", "orizzonte", "orma", "ormeggio", "ornativo", "orologio", "orrendo", "orribile", "ortensia", "ortica", "orzata", "orzo", "osare", "oscurare", "osmosi", "ospedale", "ospite", "ossa", "ossidare", "ostacolo", "oste", "otite", "otre", "ottagono", "ottimo", "ottobre", "ovale", "ovest", "ovino", "oviparo", "ovocito", "ovunque", "ovviare", "ozio", "pacchetto", "pace", "pacifico", "padella", "padrone", "paese", "paga", "pagina", "palazzina", "palesare", "pallido", "palo", "palude", "pandoro", "pannello", "paolo", "paonazzo", "paprica", "parabola", "parcella", "parere", "pargolo", "pari", "parlato", "parola", "partire", "parvenza", "parziale", "passivo", "pasticca", "patacca", "patologia", "pattume", "pavone", "peccato", "pedalare", "pedonale", "peggio", "peloso", "penare", "pendice", "penisola", "pennuto", "penombra", "pensare", "pentola", "pepe", "pepita", "perbene", "percorso", "perdonato", "perforare", "pergamena", "periodo", "permesso", "perno", "perplesso", "persuaso", "pertugio", "pervaso", "pesatore", "pesista", "peso", "pestifero", "petalo", "pettine", "petulante", "pezzo", "piacere", "pianta", "piattino", "piccino", "picozza", "piega", "pietra", "piffero", "pigiama", "pigolio", "pigro", "pila", "pilifero", "pillola", "pilota", "pimpante", "pineta", "pinna", "pinolo", "pioggia", "piombo", "piramide", "piretico", "pirite", "pirolisi", "pitone", "pizzico", "placebo", "planare", "plasma", "platano", "plenario", "pochezza", "poderoso", "podismo", "poesia", "poggiare", "polenta", "poligono", "pollice", "polmonite", "polpetta", "polso", "poltrona", "polvere", "pomice", "pomodoro", "ponte", "popoloso", "porfido", "poroso", "porpora", "porre", "portata", "posa", "positivo", "possesso", "postulato", "potassio", "potere", "pranzo", "prassi", "pratica", "precluso", "predica", "prefisso", "pregiato", "prelievo", "premere", "prenotare", "preparato", "presenza", "pretesto", "prevalso", "prima", "principe", "privato", "problema", "procura", "produrre", "profumo", "progetto", "prolunga", "promessa", "pronome", "proposta", "proroga", "proteso", "prova", "prudente", "prugna", "prurito", "psiche", "pubblico", "pudica", "pugilato", "pugno", "pulce", "pulito", "pulsante", "puntare", "pupazzo", "pupilla", "puro", "quadro", "qualcosa", "quasi", "querela", "quota", "raccolto", "raddoppio", "radicale", "radunato", "raffica", "ragazzo", "ragione", "ragno", "ramarro", "ramingo", "ramo", "randagio", "rantolare", "rapato", "rapina", "rappreso", "rasatura", "raschiato", "rasente", "rassegna", "rastrello", "rata", "ravveduto", "reale", "recepire", "recinto", "recluta", "recondito", "recupero", "reddito", "redimere", "regalato", "registro", "regola", "regresso", "relazione", "remare", "remoto", "renna", "replica", "reprimere", "reputare", "resa", "residente", "responso", "restauro", "rete", "retina", "retorica", "rettifica", "revocato", "riassunto", "ribadire", "ribelle", "ribrezzo", "ricarica", "ricco", "ricevere", "riciclato", "ricordo", "ricreduto", "ridicolo", "ridurre", "rifasare", "riflesso", "riforma", "rifugio", "rigare", "rigettato", "righello", "rilassato", "rilevato", "rimanere", "rimbalzo", "rimedio", "rimorchio", "rinascita", "rincaro", "rinforzo", "rinnovo", "rinomato", "rinsavito", "rintocco", "rinuncia", "rinvenire", "riparato", "ripetuto", "ripieno", "riportare", "ripresa", "ripulire", "risata", "rischio", "riserva", "risibile", "riso", "rispetto", "ristoro", "risultato", "risvolto", "ritardo", "ritegno", "ritmico", "ritrovo", "riunione", "riva", "riverso", "rivincita", "rivolto", "rizoma", "roba", "robotico", "robusto", "roccia", "roco", "rodaggio", "rodere", "roditore", "rogito", "rollio", "romantico", "rompere", "ronzio", "rosolare", "rospo", "rotante", "rotondo", "rotula", "rovescio", "rubizzo", "rubrica", "ruga", "rullino", "rumine", "rumoroso", "ruolo", "rupe", "russare", "rustico", "sabato", "sabbiare", "sabotato", "sagoma", "salasso", "saldatura", "salgemma", "salivare", "salmone", "salone", "saltare", "saluto", "salvo", "sapere", "sapido", "saporito", "saraceno", "sarcasmo", "sarto", "sassoso", "satellite", "satira", "satollo", "saturno", "savana", "savio", "saziato", "sbadiglio", "sbalzo", "sbancato", "sbarra", "sbattere", "sbavare", "sbendare", "sbirciare", "sbloccato", "sbocciato", "sbrinare", "sbruffone", "sbuffare", "scabroso", "scadenza", "scala", "scambiare", "scandalo", "scapola", "scarso", "scatenare", "scavato", "scelto", "scenico", "scettro", "scheda", "schiena", "sciarpa", "scienza", "scindere", "scippo", "sciroppo", "scivolo", "sclerare", "scodella", "scolpito", "scomparto", "sconforto", "scoprire", "scorta", "scossone", "scozzese", "scriba", "scrollare", "scrutinio", "scuderia", "scultore", "scuola", "scuro", "scusare", "sdebitare", "sdoganare", "seccatura", "secondo", "sedano", "seggiola", "segnalato", "segregato", "seguito", "selciato", "selettivo", "sella", "selvaggio", "semaforo", "sembrare", "seme", "seminato", "sempre", "senso", "sentire", "sepolto", "sequenza", "serata", "serbato", "sereno", "serio", "serpente", "serraglio", "servire", "sestina", "setola", "settimana", "sfacelo", "sfaldare", "sfamato", "sfarzoso", "sfaticato", "sfera", "sfida", "sfilato", "sfinge", "sfocato", "sfoderare", "sfogo", "sfoltire", "sforzato", "sfratto", "sfruttato", "sfuggito", "sfumare", "sfuso", "sgabello", "sgarbato", "sgonfiare", "sgorbio", "sgrassato", "sguardo", "sibilo", "siccome", "sierra", "sigla", "signore", "silenzio", "sillaba", "simbolo", "simpatico", "simulato", "sinfonia", "singolo", "sinistro", "sino", "sintesi", "sinusoide", "sipario", "sisma", "sistole", "situato", "slitta", "slogatura", "sloveno", "smarrito", "smemorato", "smentito", "smeraldo", "smilzo", "smontare", "smottato", "smussato", "snellire", "snervato", "snodo", "sobbalzo", "sobrio", "soccorso", "sociale", "sodale", "soffitto", "sogno", "soldato", "solenne", "solido", "sollazzo", "solo", "solubile", "solvente", "somatico", "somma", "sonda", "sonetto", "sonnifero", "sopire", "soppeso", "sopra", "sorgere", "sorpasso", "sorriso", "sorso", "sorteggio", "sorvolato", "sospiro", "sosta", "sottile", "spada", "spalla", "spargere", "spatola", "spavento", "spazzola", "specie", "spedire", "spegnere", "spelatura", "speranza", "spessore", "spettrale", "spezzato", "spia", "spigoloso", "spillato", "spinoso", "spirale", "splendido", "sportivo", "sposo", "spranga", "sprecare", "spronato", "spruzzo", "spuntino", "squillo", "sradicare", "srotolato", "stabile", "stacco", "staffa", "stagnare", "stampato", "stantio", "starnuto", "stasera", "statuto", "stelo", "steppa", "sterzo", "stiletto", "stima", "stirpe", "stivale", "stizzoso", "stonato", "storico", "strappo", "stregato", "stridulo", "strozzare", "strutto", "stuccare", "stufo", "stupendo", "subentro", "succoso", "sudore", "suggerito", "sugo", "sultano", "suonare", "superbo", "supporto", "surgelato", "surrogato", "sussurro", "sutura", "svagare", "svedese", "sveglio", "svelare", "svenuto", "svezia", "sviluppo", "svista", "svizzera", "svolta", "svuotare", "tabacco", "tabulato", "tacciare", "taciturno", "tale", "talismano", "tampone", "tannino", "tara", "tardivo", "targato", "tariffa", "tarpare", "tartaruga", "tasto", "tattico", "taverna", "tavolata", "tazza", "teca", "tecnico", "telefono", "temerario", "tempo", "temuto", "tendone", "tenero", "tensione", "tentacolo", "teorema", "terme", "terrazzo", "terzetto", "tesi", "tesserato", "testato", "tetro", "tettoia", "tifare", "tigella", "timbro", "tinto", "tipico", "tipografo", "tiraggio", "tiro", "titanio", "titolo", "titubante", "tizio", "tizzone", "toccare", "tollerare", "tolto", "tombola", "tomo", "tonfo", "tonsilla", "topazio", "topologia", "toppa", "torba", "tornare", "torrone", "tortora", "toscano", "tossire", "tostatura", "totano", "trabocco", "trachea", "trafila", "tragedia", "tralcio", "tramonto", "transito", "trapano", "trarre", "trasloco", "trattato", "trave", "treccia", "tremolio", "trespolo", "tributo", "tricheco", "trifoglio", "trillo", "trincea", "trio", "tristezza", "triturato", "trivella", "tromba", "trono", "troppo", "trottola", "trovare", "truccato", "tubatura", "tuffato", "tulipano", "tumulto", "tunisia", "turbare", "turchino", "tuta", "tutela", "ubicato", "uccello", "uccisore", "udire", "uditivo", "uffa", "ufficio", "uguale", "ulisse", "ultimato", "umano", "umile", "umorismo", "uncinetto", "ungere", "ungherese", "unicorno", "unificato", "unisono", "unitario", "unte", "uovo", "upupa", "uragano", "urgenza", "urlo", "usanza", "usato", "uscito", "usignolo", "usuraio", "utensile", "utilizzo", "utopia", "vacante", "vaccinato", "vagabondo", "vagliato", "valanga", "valgo", "valico", "valletta", "valoroso", "valutare", "valvola", "vampata", "vangare", "vanitoso", "vano", "vantaggio", "vanvera", "vapore", "varano", "varcato", "variante", "vasca", "vedetta", "vedova", "veduto", "vegetale", "veicolo", "velcro", "velina", "velluto", "veloce", "venato", "vendemmia", "vento", "verace", "verbale", "vergogna", "verifica", "vero", "verruca", "verticale", "vescica", "vessillo", "vestale", "veterano", "vetrina", "vetusto", "viandante", "vibrante", "vicenda", "vichingo", "vicinanza", "vidimare", "vigilia", "vigneto", "vigore", "vile", "villano", "vimini", "vincitore", "viola", "vipera", "virgola", "virologo", "virulento", "viscoso", "visione", "vispo", "vissuto", "visura", "vita", "vitello", "vittima", "vivanda", "vivido", "viziare", "voce", "voga", "volatile", "volere", "volpe", "voragine", "vulcano", "zampogna", "zanna", "zappato", "zattera", "zavorra", "zefiro", "zelante", "zelo", "zenzero", "zerbino", "zibetto", "zinco", "zircone", "zitto", "zolla", "zotico", "zucchero", "zufolo", "zulu", "zuppa"] +const russianWords*: seq[string] = @["абажур", "абзац", "абонент", "абрикос", "абсурд", "авангард", "август", "авиация", "авоська", "автор", "агат", "агент", "агнец", "агония", "агрегат", "адвокат", "адмирал", "адрес", "ажиотаж", "азарт", "азбука", "азот", "аист", "айсберг", "академия", "аквариум", "аккорд", "акробат", "аксиома", "актер", "актриса", "акула", "акцент", "акция", "алгоритм", "аллея", "алмаз", "алтарь", "алфавит", "алхимик", "алый", "альбом", "алюминий", "амбар", "америка", "аметист", "амнезия", "ампула", "амфора", "анализ", "ангел", "англия", "анекдот", "анимация", "анкета", "аномалия", "ансамбль", "антенна", "апатия", "апельсин", "апофеоз", "аппарат", "аппетит", "апрель", "аптека", "арбуз", "аргумент", "арест", "ария", "арка", "армия", "аромат", "арсенал", "артист", "архив", "аршин", "асбест", "аспект", "ассорти", "астроном", "асфальт", "атака", "ателье", "атлас", "атом", "атрибут", "аудитор", "аукцион", "аура", "афера", "афиша", "африка", "ахинея", "ацетон", "аэропорт", "баба", "бабка", "бабочка", "бабушка", "багаж", "бадья", "база", "баклажан", "балкон", "бампер", "бандит", "банк", "баня", "барак", "барин", "барон", "барьер", "бассейн", "батарея", "бахрома", "башмак", "башня", "баян", "бегство", "беда", "бедный", "бежать", "бездна", "бекон", "белок", "белый", "белье", "бензин", "берег", "берлин", "беседа", "биатлон", "бивень", "бигуди", "бидон", "бизнес", "бикини", "билет", "бинокль", "биология", "биржа", "бисер", "битва", "бицепс", "благо", "блеск", "близкий", "блин", "блокнот", "блюдо", "бляха", "бобер", "бодрый", "боевик", "боец", "бокал", "болезнь", "болото", "больница", "бомба", "борода", "борт", "борьба", "босой", "ботинок", "боцман", "бочка", "боярин", "брак", "брат", "бревно", "бред", "бригада", "бровь", "бродить", "бросать", "брызги", "брюки", "брюхо", "бублик", "бугор", "будка", "будущее", "буква", "букет", "бульвар", "бумага", "бунт", "буря", "бусы", "бутылка", "буфет", "бухта", "бушлат", "бывать", "быль", "быть", "бюджет", "бюро", "бюст", "вагон", "важный", "ваза", "вакцина", "валенок", "валить", "валюта", "вампир", "ванна", "вариант", "вассал", "вата", "вафля", "вахта", "вдали", "вдвоем", "вдова", "вдоль", "вдруг", "ведать", "ведро", "ведущий", "ведьма", "веер", "везде", "везти", "веко", "велеть", "великий", "вена", "веранда", "веревка", "вернуть", "версия", "вершина", "веселый", "весло", "весна", "весть", "ветвь", "ветер", "ветка", "вечер", "вещество", "вещь", "веяние", "взбучка", "взвод", "взгляд", "вздох", "взмах", "взнос", "взор", "взрыв", "взятка", "вибрация", "видение", "видный", "виза", "визг", "визит", "вилка", "вина", "вино", "вирус", "висеть", "виски", "висок", "витрина", "вихрь", "вкус", "владелец", "власть", "влево", "влияние", "вместе", "вниз", "внимание", "внук", "внутрь", "вода", "водитель", "водка", "вождь", "воздух", "возить", "возраст", "воин", "вокзал", "вокруг", "волк", "волна", "волос", "воля", "вопль", "вопрос", "ворота", "восемь", "восток", "враг", "врать", "врач", "время", "всадник", "всегда", "вспышка", "встреча", "всюду", "вторник", "вулкан", "вход", "вчера", "выбор", "выбрать", "вывод", "вызов", "выигрыш", "выпить", "выпуск", "высота", "выстрел", "высший", "выход", "вычет", "вышка", "вязать", "вялый", "гавань", "гадать", "газета", "гаишник", "галстук", "гамма", "гараж", "гвардия", "гвоздь", "гектар", "гель", "генерал", "гений", "геолог", "герой", "гибель", "гигант", "гильза", "гимн", "гипотеза", "гитара", "глава", "глаз", "глина", "глоток", "глубина", "глухой", "глыба", "гнев", "гнездо", "гном", "голова", "голый", "гонка", "гонять", "гора", "горе", "горло", "горный", "город", "горшок", "горячий", "госпожа", "гость", "готовый", "градус", "грамм", "грань", "графа", "грек", "грех", "гриб", "гроза", "гром", "грохот", "грубый", "грудь", "груз", "группа", "груша", "грызть", "грязь", "губа", "гудеть", "гулять", "густой", "гусь", "гуща", "давать", "давление", "давний", "даже", "далекий", "даль", "дама", "данные", "дарить", "дата", "дать", "дача", "дверь", "движение", "двое", "дворец", "дебют", "девочка", "девушка", "девять", "действие", "декабрь", "делить", "дело", "демократ", "день", "депутат", "дерево", "десяток", "деталь", "детство", "дефицит", "деятель", "джаз", "джинсы", "джунгли", "диагноз", "диалог", "диван", "дивизия", "диета", "дизайн", "дикий", "динамика", "диплом", "директор", "диск", "дитя", "дичь", "длина", "дневник", "добавить", "добро", "добыча", "доверие", "договор", "дождь", "доза", "дойти", "доказать", "доклад", "доктор", "документ", "долг", "доллар", "доля", "допрос", "дорога", "досада", "доска", "доход", "доцент", "дочь", "дошлый", "драка", "дрова", "дрожь", "друг", "дружба", "дуга", "дудка", "дукат", "дуло", "дума", "дупло", "дурак", "дуть", "душа", "дуэт", "дыня", "дыра", "дыхание", "дышать", "дюжина", "дюйм", "дюна", "дядя", "дятел", "европа", "егерь", "едва", "единица", "едкий", "ежевика", "езда", "елка", "емкость", "ерунда", "ехать", "жадный", "жажда", "жалкий", "жалоба", "жанр", "жара", "жгучий", "ждать", "желание", "железо", "желтый", "желудок", "жемчуг", "жена", "жених", "женщина", "жест", "живот", "жидкость", "жизнь", "жилой", "жилье", "житель", "жить", "жрать", "журнал", "жуткий", "жюри", "забор", "забрать", "забыть", "зависть", "завод", "завтрак", "загадка", "заговор", "задача", "задний", "закат", "закон", "залить", "заметка", "замок", "замысел", "занятие", "запад", "запись", "заросль", "зарплата", "заря", "заслуга", "зато", "затрата", "затылок", "захват", "зацепка", "зачет", "защита", "заявка", "заяц", "звать", "звезда", "зверь", "звонок", "звук", "звучать", "здание", "здесь", "здоровье", "зебра", "земля", "зенит", "зеркало", "зерно", "зефир", "зигзаг", "зима", "зиять", "злак", "злоба", "змея", "знак", "знамя", "знание", "знать", "значение", "зной", "зодчий", "золото", "зона", "зоопарк", "зоркий", "зрачок", "зрелище", "зрение", "зритель", "зыбкий", "зять", "игла", "игра", "игрок", "игрушка", "идеал", "идея", "идиот", "идол", "идти", "иерархия", "изба", "известие", "издание", "изделие", "износ", "изнутри", "изоляция", "изредка", "изучение", "икра", "иллюзия", "имбирь", "иметь", "имидж", "империя", "иначе", "инвестор", "индивид", "инерция", "инженер", "иногда", "иной", "институт", "интерес", "ипподром", "ирис", "ирония", "искать", "искра", "испуг", "истина", "история", "исход", "итак", "италия", "итог", "июль", "июнь", "кабина", "каблук", "кавалер", "кавказ", "кадр", "каждый", "казак", "казнь", "кайф", "кактус", "калитка", "камень", "канал", "кандидат", "капитал", "капля", "капуста", "карман", "карта", "карьера", "касса", "катер", "катить", "кафе", "качать", "качество", "каша", "квартал", "квинтет", "квота", "кедр", "кекс", "кенгуру", "кепка", "керосин", "кетчуп", "кибитка", "кивать", "кидать", "километр", "кино", "кинуть", "киоск", "кипеть", "кирпич", "кислота", "класс", "клетка", "клиент", "кличка", "клоун", "клуб", "клык", "ключ", "клятва", "книга", "книжка", "кнопка", "кнут", "князь", "кобура", "ковер", "когда", "кодекс", "кожа", "козел", "койка", "коктейль", "колбаса", "колдун", "колесо", "коллега", "колодец", "колхоз", "кольцо", "комар", "комбат", "комитет", "комната", "компания", "конверт", "конец", "конкурс", "контакт", "конфета", "концерт", "кончик", "конь", "копейка", "копия", "копыто", "копье", "корабль", "корень", "корзина", "коридор", "кормить", "король", "корпус", "космос", "косой", "костюм", "котел", "кофе", "кошка", "край", "кран", "краска", "кредит", "кремль", "крепко", "кресло", "кривой", "кризис", "крик", "критика", "кричать", "кровать", "кролик", "кроме", "круг", "крупный", "крутить", "крыло", "крыша", "крючок", "кстати", "кубок", "кувшин", "куда", "кузнец", "кузов", "кукла", "кулак", "культура", "кумир", "купец", "купол", "курица", "курс", "куртка", "кусок", "куст", "кухня", "куча", "кушать", "кювет", "лабиринт", "лавка", "лагерь", "ладонь", "лазерный", "лайнер", "лакей", "лампа", "ландшафт", "лапа", "ларек", "лауреат", "лачуга", "лаять", "лебедь", "левый", "легенда", "легкий", "лежать", "лезть", "лекция", "лениво", "лента", "лепесток", "лестница", "лететь", "лето", "летчик", "лечение", "леший", "либерал", "либо", "ливень", "лига", "лидер", "лимон", "линия", "липа", "лирика", "лист", "литр", "лифт", "лихой", "лицо", "личность", "ловить", "ловко", "логика", "лодка", "ложь", "лозунг", "локоть", "ломать", "лондон", "лоно", "лопата", "лорд", "лось", "лоток", "лошадь", "лужа", "луна", "лучший", "лыжа", "лысый", "льгота", "льдина", "любить", "любовь", "люстра", "лютый", "лягушка", "магазин", "магия", "мадам", "мазать", "майор", "максимум", "малыш", "мальчик", "мама", "манера", "марка", "март", "маршал", "масло", "масса", "мастер", "масштаб", "материя", "матрос", "матушка", "матч", "мать", "мафия", "махать", "махнуть", "машина", "маэстро", "маяк", "мгла", "мебель", "медаль", "медведь", "между", "мелкий", "мелодия", "мемуары", "менять", "мера", "место", "месяц", "металл", "метод", "метр", "механизм", "мечта", "мешать", "мешок", "миграция", "мизинец", "микрофон", "милиция", "миллион", "милость", "милый", "мимо", "министр", "минута", "мирный", "мировой", "миссия", "митинг", "мишень", "мнение", "мнимый", "много", "мода", "модель", "можно", "мозг", "мокрый", "молния", "молоко", "молчать", "момент", "монах", "монета", "мораль", "морда", "море", "мороз", "моряк", "москвич", "мост", "мотив", "мотор", "мочь", "мощность", "мрак", "мстить", "мудрый", "мужик", "мужчина", "музей", "музыка", "мука", "мундир", "муравей", "мусор", "мутный", "муфта", "муха", "мучить", "мушкетер", "мыло", "мысль", "мыть", "мычать", "мышь", "мэтр", "мюзикл", "мягкий", "мякиш", "мясо", "мятый", "набор", "наверх", "навык", "награда", "надежда", "надпись", "нажать", "назад", "название", "найти", "налево", "наличие", "налог", "намек", "напиток", "народ", "наружу", "нары", "наряд", "натура", "наука", "нация", "начало", "небо", "невеста", "негодяй", "неделя", "некий", "некого", "нельзя", "немой", "нерв", "нести", "неудача", "нефть", "нива", "нигде", "нижний", "низкий", "никак", "никель", "никто", "нирвана", "нитка", "нить", "ничья", "ниша", "нищий", "новичок", "новость", "новый", "нога", "ноготь", "ножницы", "ноздря", "ноль", "номер", "норма", "носок", "нота", "ночной", "ночь", "ноша", "ноябрь", "нрав", "нужда", "нужный", "нутро", "нынче", "ныть", "нюанс", "нюхать", "няня", "оазис", "обаяние", "обед", "обещать", "обзор", "обида", "облако", "облик", "обломок", "обман", "обморок", "обойти", "оборот", "обочина", "образ", "обрести", "обрыв", "обувь", "обучение", "общаться", "общение", "общий", "объект", "объятие", "обычай", "овес", "овощ", "овраг", "овца", "овчарка", "огонь", "огород", "ограда", "огурец", "одежда", "одесса", "одеяло", "один", "однажды", "ожидание", "ожог", "озеро", "океан", "оклад", "окно", "около", "окоп", "окраина", "округ", "октябрь", "окурок", "олень", "операция", "описание", "оплата", "опора", "оппонент", "опрос", "опыт", "опять", "орать", "орбита", "орган", "орден", "орел", "оригинал", "оркестр", "орнамент", "оружие", "осадок", "осел", "осень", "осина", "осколок", "осмотр", "основа", "особняк", "остаток", "остров", "отбор", "ответ", "отдать", "отдел", "отдых", "отель", "отец", "отзыв", "отказ", "открытие", "откуда", "отличие", "отнюдь", "отнять", "отойти", "отпуск", "отрасль", "отряд", "отсюда", "оттенок", "оттого", "оттуда", "отчет", "отъезд", "офис", "офицер", "охапка", "охота", "охрана", "оценка", "очаг", "очередь", "очки", "ошейник", "ошибка", "ощутить", "ощущение", "павильон", "падаль", "падение", "паек", "пазуха", "пакет", "палата", "палец", "палуба", "пальто", "память", "панель", "паника", "парень", "париж", "парк", "пароход", "партия", "паспорт", "пастух", "патрон", "пауза", "пафос", "пацан", "пациент", "пашня", "певец", "педагог", "пейзаж", "пельмень", "пена", "пенсия", "пепел", "первый", "перец", "период", "перо", "песня", "песок", "петля", "петух", "петь", "пехота", "печать", "печень", "пешеход", "пешком", "пещера", "пианист", "пиво", "пиджак", "пила", "пилить", "пилот", "пионер", "писатель", "пистолет", "письмо", "питание", "пить", "пицца", "пища", "плавать", "плакат", "план", "платье", "пленка", "плечо", "плита", "плод", "плоть", "плохо", "площадь", "плыть", "плюс", "пляж", "победа", "повар", "повесть", "повод", "повязка", "погода", "подарок", "подвиг", "подпись", "подруга", "подушка", "подход", "подъем", "поезд", "поза", "позвать", "позиция", "позор", "поиск", "пока", "покой", "покрыть", "покупка", "полдень", "поле", "политик", "полк", "полный", "полоса", "полтора", "польза", "поляна", "помимо", "помнить", "помощь", "пони", "понятие", "попасть", "поперек", "попытка", "пора", "порог", "порт", "порция", "порыв", "порядок", "посадка", "поселок", "послать", "постель", "посуда", "потеря", "поток", "походка", "поцелуй", "почва", "почему", "пощечина", "поэзия", "поэт", "пояс", "правда", "праздник", "практик", "предел", "премия", "препарат", "пресса", "прибор", "привет", "прием", "призыв", "приказ", "пример", "принц", "природа", "приступ", "причина", "приятель", "пробка", "провал", "прогноз", "продукт", "проект", "проза", "прокурор", "пропуск", "простор", "против", "процент", "прочесть", "прошлое", "пруд", "прыгать", "прыжок", "психика", "птица", "публика", "пугать", "пудра", "пузырь", "пульт", "пуля", "пункт", "пурга", "пустяк", "путем", "путь", "пухлый", "пучок", "пчела", "пшеница", "пыль", "пышный", "пьеса", "пьяный", "пятеро", "пятно", "пяток", "пять", "работа", "радио", "радость", "разбить", "разговор", "размер", "разница", "разряд", "разум", "район", "ракета", "рама", "рамка", "рана", "ранее", "рапорт", "рассказ", "раствор", "расход", "расчет", "раунд", "рация", "реакция", "ребенок", "ребро", "ребята", "реветь", "регион", "реестр", "режим", "резать", "рейтинг", "река", "реклама", "рельс", "ремень", "ремонт", "рента", "реплика", "ресница", "ресторан", "ресурс", "реформа", "рецепт", "речь", "решать", "решение", "решить", "ржавый", "риск", "рисунок", "ритм", "рифма", "робкий", "родитель", "родной", "рожа", "рождение", "роза", "роль", "роман", "ронять", "роса", "рослый", "россия", "рост", "рота", "роща", "рояль", "рубашка", "рубеж", "рубль", "ругать", "руда", "ружье", "руины", "рука", "руль", "ручей", "ручка", "рыба", "рыдать", "рыжий", "рынок", "рысь", "рыхлый", "рыцарь", "рычаг", "рюкзак", "рюмка", "рябой", "сабля", "салон", "самолет", "сани", "сапог", "сарай", "сатира", "сауна", "сахар", "сбоку", "сбор", "сбыт", "свадьба", "сведение", "свежий", "сверху", "свет", "свеча", "свидание", "свинья", "свист", "свобода", "свойство", "связь", "сделка", "сеанс", "север", "сегмент", "сегодня", "седло", "седой", "сезон", "сейф", "сейчас", "секрет", "сектор", "секунда", "село", "семья", "семя", "сено", "сентябрь", "сердце", "середина", "сержант", "серия", "серый", "сестра", "сеть", "сечение", "сжечь", "сзади", "сибирь", "сигарета", "сигнал", "сиденье", "сизый", "сила", "сильный", "символ", "симптом", "синий", "сирота", "система", "ситуация", "сиять", "сказка", "скала", "скамья", "скважина", "сквозь", "скелет", "скидка", "склад", "скорость", "скотина", "скрипка", "скрыть", "скука", "слабый", "слава", "слегка", "след", "слеза", "слияние", "слово", "слой", "слон", "слуга", "служба", "слух", "случай", "слышать", "слюна", "смело", "смена", "смесь", "смех", "смеяться", "смола", "смысл", "смятение", "снаряд", "снег", "снижение", "снизу", "снимок", "снова", "собака", "собор", "собрание", "событие", "совет", "совсем", "создать", "сознание", "сойти", "сокол", "солдат", "солнце", "соль", "сомнение", "соперник", "сопка", "сорок", "сорт", "сосед", "сосна", "состав", "сосуд", "сотня", "соус", "социолог", "союз", "спальня", "спасать", "спать", "спешить", "спина", "список", "спичка", "спор", "способ", "спутник", "сразу", "среда", "срок", "ссылка", "ставка", "стадо", "стакан", "сталкер", "станция", "старик", "статус", "стая", "ствол", "стекло", "стена", "степь", "стиль", "стихи", "стойка", "столица", "стон", "сторона", "стоянка", "страна", "стрела", "строка", "струя", "студия", "стук", "стул", "ступень", "стыд", "суббота", "субъект", "сувенир", "сугроб", "судно", "судья", "суета", "суждение", "сумерки", "сумка", "сумма", "супруг", "сустав", "сутки", "суть", "сухой", "суша", "существо", "сущность", "сфера", "схема", "сцена", "счастье", "счет", "съезд", "съемка", "сырой", "сырье", "сытый", "сыщик", "сюжет", "сюрприз", "табак", "таблица", "тайна", "также", "такси", "талант", "таможня", "танец", "танк", "танцор", "тарелка", "тахта", "таять", "тварь", "твой", "творить", "театр", "тезис", "текст", "телега", "тело", "тема", "темнота", "темп", "тень", "теория", "теперь", "тепло", "терапия", "термин", "терять", "тесный", "тетрадь", "тетя", "техника", "течение", "течь", "теща", "тигр", "тираж", "титул", "тишина", "ткань", "ткнуть", "товар", "тогда", "тоже", "толк", "толпа", "толчок", "тонкий", "тонна", "топливо", "топор", "торговля", "точка", "точно", "тощий", "трава", "традиция", "трактор", "трамвай", "траншея", "трасса", "тревога", "треск", "третий", "трещина", "трибуна", "триста", "трогать", "тройка", "тронуть", "тропа", "тротуар", "труба", "труд", "трюк", "тряпка", "трясти", "туалет", "туго", "туда", "туловище", "туман", "тундра", "тупик", "турист", "турнир", "туфля", "туча", "тыкать", "тысяча", "тьма", "тюльпан", "тяга", "тяжесть", "тянуть", "убедить", "убыток", "уважение", "угол", "угроза", "удав", "удар", "удача", "удивить", "уезд", "ужас", "ужин", "узел", "узкий", "узнать", "узор", "уйма", "указ", "уклон", "укол", "украина", "уксус", "улица", "улыбка", "ум", "умение", "уметь", "умный", "умысел", "уныние", "упасть", "уплата", "упор", "уран", "урна", "уровень", "урок", "усадьба", "усердие", "усилие", "условие", "услуга", "усмешка", "успех", "устав", "утка", "утро", "утюг", "уход", "участок", "учеба", "ученик", "учет", "училище", "учитель", "ушко", "ущерб", "уютный", "фабрика", "фаворит", "фаза", "файл", "факт", "фамилия", "фантазия", "фара", "фасад", "фашист", "февраль", "феномен", "ферма", "фигура", "физика", "философ", "фильм", "финал", "фирма", "фишка", "флаг", "флейта", "флот", "фокус", "фонарь", "фонд", "фонтан", "форма", "фото", "фраза", "франция", "фреска", "фронт", "фрукт", "функция", "фуражка", "футбол", "халат", "хаос", "характер", "хата", "хвост", "хижина", "химия", "хирург", "хищник", "хлам", "хлеб", "хмурый", "хоббит", "ходить", "хозяин", "хоккей", "холм", "холод", "хотеть", "хохот", "храм", "хранить", "хребет", "хрен", "хроника", "художник", "хулиган", "хутор", "царство", "царь", "цветок", "целиком", "целое", "целый", "цель", "цемент", "цена", "ценность", "центр", "цепь", "цикл", "цилиндр", "цирк", "цистерна", "цитата", "цифра", "цыпленок", "чадо", "чайник", "часть", "часы", "чашка", "человек", "челюсть", "чемодан", "чепуха", "череп", "черный", "черта", "честь", "четкий", "четыре", "чехол", "чиновник", "число", "чистота", "читатель", "член", "чтение", "чувство", "чудо", "чужой", "чулок", "чуткий", "чучело", "чушь", "шаблон", "шагать", "шакал", "шалаш", "шампунь", "шанс", "шапка", "шасси", "шатер", "шахта", "шашлык", "шедевр", "шепот", "шерсть", "шестой", "шинель", "шипеть", "широкий", "шить", "шкаф", "школа", "шкура", "шланг", "шлем", "шлюпка", "шляпа", "шнур", "шоколад", "шорох", "шоссе", "шофер", "шпага", "шпион", "шприц", "шрам", "шрифт", "штаб", "штаны", "штат", "штора", "штраф", "штука", "штурман", "штык", "шуба", "шумный", "шутить", "шутка", "щадить", "щедрый", "щека", "щель", "щенок", "щепка", "щетка", "щука", "эволюция", "эгоизм", "экзамен", "экипаж", "экономия", "экран", "эксперт", "элемент", "элита", "эмблема", "эмоция", "энергия", "эпизод", "эпоха", "эскиз", "эссе", "эстония", "эстрада", "этаж", "этап", "этика", "этюд", "эфир", "эффект", "эшелон", "юбилей", "юбка", "южный", "юмор", "юность", "юноша", "юный", "юрист", "яблоко", "явление", "явно", "явный", "ягода", "ядро", "язва", "язык", "яйцо", "якобы", "якорь", "январь", "япония", "яркий", "ярко", "ярмарка", "ярость", "ярус", "ясно", "ясный", "яхта", "ячейка", "ящик"] \ No newline at end of file diff --git a/src/app_service/service/accounts/service.nim b/src/app_service/service/accounts/service.nim index 10cb29ef6d..4033db348a 100644 --- a/src/app_service/service/accounts/service.nim +++ b/src/app_service/service/accounts/service.nim @@ -1,9 +1,10 @@ -import os, json, sequtils, strutils, uuids +import os, json, sequtils, strutils, uuids, times import json_serialization, chronicles import ../../../app/global/global_singleton import ./dto/accounts as dto_accounts import ./dto/generated_accounts as dto_generated_accounts +from ../keycard/service import KeycardEvent, KeyDetails import ../../../backend/general as status_general import ../../../backend/core as status_core @@ -143,6 +144,25 @@ proc saveAccountAndLogin(self: Service, hashedPassword: string, account, except Exception as e: error "error: ", procName="saveAccountAndLogin", errName = e.name, errDesription = e.msg +proc saveKeycardAccountAndLogin(self: Service, chatKey, password: string, account, subaccounts, settings, + config: JsonNode): AccountDto = + try: + let response = status_account.saveAccountAndLoginWithKeycard(chatKey, password, account, subaccounts, settings, config) + + var error = "response doesn't contain \"error\"" + if(response.result.contains("error")): + error = response.result["error"].getStr + if error == "": + debug "Account saved succesfully" + result = toAccountDto(account) + return + + let err = "Error saving account and logging in via keycard : " & error + error "error: ", procName="saveKeycardAccountAndLogin", errDesription = err + + except Exception as e: + error "error: ", procName="saveKeycardAccountAndLogin", errName = e.name, errDesription = e.msg + proc prepareAccountJsonObject(self: Service, account: GeneratedAccountDto, displayName: string): JsonNode = result = %* { "name": if displayName == "": account.alias else: displayName, @@ -259,15 +279,32 @@ proc setLocalAccountSettingsFile(self: Service) = if(defined(macosx) and self.getLoggedInAccount.isValid()): singletonInstance.localAccountSettings.setFileName(self.getLoggedInAccount.name) -proc setupAccount*(self: Service, accountId, password, displayName: string): string = +proc addKeycardDetails(self: Service, settingsJson: var JsonNode, accountData: var JsonNode) = + let keycardPairingJsonString = readFile(main_constants.KEYCARDPAIRINGDATAFILE) + let keycardPairingJsonObj = keycardPairingJsonString.parseJSON + let now = now().toTime().toUnix() + for instanceUid, kcDataObj in keycardPairingJsonObj: + if not settingsJson.isNil: + settingsJson["keycard-instance-uid"] = %* instanceUid + settingsJson["keycard-paired-on"] = %* now + settingsJson["keycard-pairing"] = kcDataObj{"key"} + if not accountData.isNil: + accountData["keycard-pairing"] = kcDataObj{"key"} + +proc setupAccount*(self: Service, accountId, password, displayName: string, keycardUsage: bool): string = try: let installationId = $genUUID() - let accountDataJson = self.getAccountDataForAccountId(accountId, displayName) + var accountDataJson = self.getAccountDataForAccountId(accountId, displayName) + + var usedPassword = password + if password.len == 0: + # this means we're setting up an account using keycard + usedPassword = accountDataJson{"key-uid"}.getStr self.setKeyStoreDir(accountDataJson{"key-uid"}.getStr) let subaccountDataJson = self.getSubaccountDataForAccountId(accountId, displayName) - let settingsJson = self.getAccountSettings(accountId, installationId, displayName) + var settingsJson = self.getAccountSettings(accountId, installationId, displayName) let nodeConfigJson = self.getDefaultNodeConfig(installationId) if(accountDataJson.isNil or subaccountDataJson.isNil or settingsJson.isNil or @@ -276,10 +313,13 @@ proc setupAccount*(self: Service, accountId, password, displayName: string): str error "error: ", procName="setupAccount", errDesription = description return description - let hashedPassword = hashString(password) + let hashedPassword = hashString(usedPassword) discard self.storeAccount(accountId, hashedPassword) discard self.storeDerivedAccounts(accountId, hashedPassword, PATHS) + if keycardUsage: + self.addKeycardDetails(settingsJson, accountDataJson) + self.loggedInAccount = self.saveAccountAndLogin(hashedPassword, accountDataJson, subaccountDataJson, settingsJson, nodeConfigJson) self.setLocalAccountSettingsFile() @@ -292,7 +332,83 @@ proc setupAccount*(self: Service, accountId, password, displayName: string): str error "error: ", procName="setupAccount", errName = e.name, errDesription = e.msg return e.msg +proc setupAccountKeycard*(self: Service, keycardData: KeycardEvent) = + try: + let installationId = $genUUID() + + let alias = generateAliasFromPk(keycardData.whisperKey.publicKey) + var accountDataJson = %* { + "name": alias, + "address": keycardData.masterKey.address, + "key-uid": keycardData.keyUid + } + + self.setKeyStoreDir(keycardData.keyUid) + let nodeConfigJson = self.getDefaultNodeConfig(installationId) + let subaccountDataJson = %* [ + { + "public-key": keycardData.walletKey.publicKey, + "address": keycardData.walletKey.address, + "color": "#4360df", + "wallet": true, + "path": PATH_DEFAULT_WALLET, + "name": "Status account", + "derived-from": keycardData.masterKey.address, + }, + { + "public-key": keycardData.whisperKey.publicKey, + "address": keycardData.whisperKey.address, + "name": alias, + "path": PATH_WHISPER, + "chat": true, + "derived-from": "" + } + ] + + var settingsJson = %* { + "key-uid": keycardData.keyUid, + "public-key": keycardData.whisperKey.publicKey, + "name": alias, + "display-name": "", + "address": keycardData.whisperKey.address, + "eip1581-address": keycardData.eip1581Key.address, + "dapps-address": keycardData.walletKey.address, + "wallet-root-address": keycardData.walletRootKey.address, + "preview-privacy?": true, + "signing-phrase": generateSigningPhrase(3), + "log-level": $LogLevel.INFO, + "latest-derived-path": 0, + "currency": "usd", + "networks/networks": @[], + "networks/current-network": "", + "wallet/visible-tokens": {}, + "waku-enabled": true, + "appearance": 0, + "installation-id": installationId + } + + self.addKeycardDetails(settingsJson, accountDataJson) + + if(accountDataJson.isNil or subaccountDataJson.isNil or settingsJson.isNil or + nodeConfigJson.isNil): + let description = "at least one json object is not prepared well" + error "error: ", procName="setupAccountKeycard", errDesription = description + return + + let hashedPassword = hashString(keycardData.keyUid) # using hashed keyUid as password + + self.loggedInAccount = self.saveKeycardAccountAndLogin(keycardData.whisperKey.privateKey, + hashedPassword, + accountDataJson, + subaccountDataJson, + settingsJson, + nodeConfigJson) + except Exception as e: + error "error: ", procName="setupAccount", errName = e.name, errDesription = e.msg + proc importMnemonic*(self: Service, mnemonic: string): string = + if mnemonic.len == 0: + return "empty mnemonic" try: let response = status_account.multiAccountImportMnemonic(mnemonic) self.importedAccount = toGeneratedAccountDto(response.result) @@ -376,6 +492,35 @@ proc login*(self: Service, account: AccountDto, password: string): string = error "error: ", procName="login", errName = e.name, errDesription = e.msg return e.msg +proc loginAccountKeycard*(self: Service, keycardData: KeycardEvent): string = + try: + self.setKeyStoreDir(keycardData.keyUid) + + let alias = generateAliasFromPk(keycardData.whisperKey.publicKey) + var accountDataJson = %* { + "name": alias, + "address": keycardData.masterKey.address, + "key-uid": keycardData.keyUid + } + var settingsJson: JsonNode + self.addKeycardDetails(settingsJson, accountDataJson) + + let hashedPassword = hashString(keycardData.keyUid) # using hashed keyUid as password + + let response = status_account.loginWithKeycard(keycardData.whisperKey.privateKey, + hashedPassword, + accountDataJson) + + var error = "response doesn't contain \"error\"" + if(response.result.contains("error")): + error = response.result["error"].getStr + if error == "": + debug "Account logged in succesfully" + return + except Exception as e: + error "error: ", procName="loginAccountKeycard", errName = e.name, errDesription = e.msg + return e.msg + proc verifyAccountPassword*(self: Service, account: string, password: string): bool = try: let response = status_account.verifyAccountPassword(account, password, self.keyStoreDir) diff --git a/src/app_service/service/eth/utils.nim b/src/app_service/service/eth/utils.nim index 301aaff8c4..0f67f6419b 100644 --- a/src/app_service/service/eth/utils.nim +++ b/src/app_service/service/eth/utils.nim @@ -81,16 +81,6 @@ proc keys*(obj: JsonNode): seq[string] = for k, _ in obj: result.add k -proc generateSigningPhrase*(count: int): string = - let now = getTime() - var rng = initRand(now.toUnix * 1000000000 + now.nanosecond) - var phrases: seq[string] = @[] - - for i in 1..count: - phrases.add(rng.sample(signing_phrases.phrases)) - - result = phrases.join(" ") - proc handleRPCErrors*(response: string) = let parsedReponse = parseJson(response) if (parsedReponse.hasKey("error")): diff --git a/src/app_service/service/keycard/async_tasks.nim b/src/app_service/service/keycard/async_tasks.nim new file mode 100644 index 0000000000..556a7b19d2 --- /dev/null +++ b/src/app_service/service/keycard/async_tasks.nim @@ -0,0 +1,12 @@ +################################################# +# Async timer +################################################# + +type + TimerTaskArg = ref object of QObjectTaskArg + timeoutInMilliseconds: int + +const timerTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} = + let arg = decode[TimerTaskArg](argEncoded) + sleep(arg.timeoutInMilliseconds) + arg.finish("") \ No newline at end of file diff --git a/src/app_service/service/keycard/constants.nim b/src/app_service/service/keycard/constants.nim new file mode 100644 index 0000000000..667634087d --- /dev/null +++ b/src/app_service/service/keycard/constants.nim @@ -0,0 +1,66 @@ +const ResponseKeyType* = "type" +const ResponseKeyEvent* = "event" + +const ResponseTypeValueKeycardFlowResult* = "keycard.flow-result" +const ResponseTypeValueInsertCard* = "keycard.action.insert-card" +const ResponseTypeValueCardInserted* = "keycard.action.card-inserted" +const ResponseTypeValueSwapCard* = "keycard.action.swap-card" +const ResponseTypeValueEnterPairing* = "keycard.action.enter-pairing" +const ResponseTypeValueEnterPIN* = "keycard.action.enter-pin" +const ResponseTypeValueEnterPUK* = "keycard.action.enter-puk" +const ResponseTypeValueEnterNewPair* = "keycard.action.enter-new-pairing" +const ResponseTypeValueEnterNewPIN* = "keycard.action.enter-new-pin" +const ResponseTypeValueEnterNewPUK* = "keycard.action.enter-new-puk" +const ResponseTypeValueEnterTXHash* = "keycard.action.enter-tx-hash" +const ResponseTypeValueEnterPath* = "keycard.action.enter-bip44-path" +const ResponseTypeValueEnterMnemonic* = "keycard.action.enter-mnemonic" + +const ErrorKey* = "error" +const ErrorOK* = "ok" +const ErrorCancel* = "cancel" +const ErrorConnection* = "connection-error" +const ErrorUnknownFlow* = "unknown-flow" +const ErrorNotAKeycard* = "not-a-keycard" +const ErrorNoKeys* = "no-keys" +const ErrorHasKeys* = "has-keys" +const ErrorRequireInit* = "require-init" +const ErrorPairing* = "pairing" +const ErrorUnblocking* = "unblocking" +const ErrorSigning* = "signing" +const ErrorExporting* = "exporting" +const ErrorChanging* = "changing-credentials" +const ErrorLoadingKeys* = "loading-keys" + +const RequestParamAppInfo* = "application-info" +const RequestParamInstanceUID* = "instance-uid" +const RequestParamFactoryReset* = "factory reset" +const RequestParamKeyUID* = "key-uid" +const RequestParamFreeSlots* = "free-pairing-slots" +const RequestParamPINRetries* = "pin-retries" +const RequestParamPUKRetries* = "puk-retries" +const RequestParamPairingPass* = "pairing-pass" +const RequestParamPaired* = "paired" +const RequestParamNewPairing* = "new-pairing-pass" +const RequestParamDefPairing* = "KeycardDefaultPairing" +const RequestParamPIN* = "pin" +const RequestParamNewPIN* = "new-pin" +const RequestParamPUK* = "puk" +const RequestParamNewPUK* = "new-puk" +const RequestParamMasterKey* = "master-key" +const RequestParamWalleRootKey* = "wallet-root-key" +const RequestParamWalletKey* = "wallet-key" +const RequestParamEIP1581Key* = "eip1581-key" +const RequestParamWhisperKey* = "whisper-key" +const RequestParamEncKey* = "encryption-key" +const RequestParamExportedKey* = "exported-key" +const RequestParamMnemonic* = "mnemonic" +const RequestParamMnemonicLen* = "mnemonic-length" +const RequestParamMnemonicIdxs* = "mnemonic-indexes" +const RequestParamTXHash* = "tx-hash" +const RequestParamBIP44Path* = "bip44-path" +const RequestParamTXSignature* = "tx-signature" +const RequestParamOverwrite* = "overwrite" + +const RequestParamAddress* = "address" +const RequestParamPublicKey* = "publicKey" +const RequestParamPrivateKey* = "privateKey" \ No newline at end of file diff --git a/src/app_service/service/keycard/internal.nim b/src/app_service/service/keycard/internal.nim new file mode 100644 index 0000000000..5a68971c49 --- /dev/null +++ b/src/app_service/service/keycard/internal.nim @@ -0,0 +1,57 @@ +type + KeyDetails* = object + address*: string + publicKey*: string + privateKey*: string + + KeycardEvent* = object + error*: string + seedPhraseIndexes*: seq[int] + freePairingSlots*: int + keyUid*: string + pinRetries*: int + pukRetries*: int + eip1581Key*: KeyDetails + encryptionKey*: KeyDetails + masterKey*: KeyDetails + walletKey*: KeyDetails + walletRootKey*: KeyDetails + whisperKey*: KeyDetails + +proc toKeyDetails(jsonObj: JsonNode): KeyDetails = + discard jsonObj.getProp(RequestParamAddress, result.address) + discard jsonObj.getProp(RequestParamPrivateKey, result.privateKey) + if jsonObj.getProp(RequestParamPublicKey, result.publicKey): + result.publicKey = "0x" & result.publicKey + +proc toKeycardEvent(jsonObj: JsonNode): KeycardEvent = + discard jsonObj.getProp(ErrorKey, result.error) + discard jsonObj.getProp(RequestParamFreeSlots, result.freePairingSlots) + discard jsonObj.getProp(RequestParamPINRetries, result.pinRetries) + discard jsonObj.getProp(RequestParamPUKRetries, result.pukRetries) + if jsonObj.getProp(RequestParamKeyUID, result.keyUid): + result.keyUid = "0x" & result.keyUid + + var obj: JsonNode + if(jsonObj.getProp(RequestParamEIP1581Key, obj)): + result.eip1581Key = toKeyDetails(obj) + + if(jsonObj.getProp(RequestParamEncKey, obj)): + result.encryptionKey = toKeyDetails(obj) + + if(jsonObj.getProp(RequestParamMasterKey, obj)): + result.masterKey = toKeyDetails(obj) + + if(jsonObj.getProp(RequestParamWalletKey, obj)): + result.walletKey = toKeyDetails(obj) + + if(jsonObj.getProp(RequestParamWalleRootKey, obj)): + result.walletRootKey = toKeyDetails(obj) + + if(jsonObj.getProp(RequestParamWhisperKey, obj)): + result.whisperKey = toKeyDetails(obj) + + var indexesArr: JsonNode + if jsonObj.getProp(RequestParamMnemonicIdxs, indexesArr) and indexesArr.kind == JArray: + for ind in indexesArr: + result.seedPhraseIndexes.add(ind.getInt) \ No newline at end of file diff --git a/src/app_service/service/keycard/service.nim b/src/app_service/service/keycard/service.nim new file mode 100644 index 0000000000..e7a69480d4 --- /dev/null +++ b/src/app_service/service/keycard/service.nim @@ -0,0 +1,240 @@ +import NimQml, json, os, chronicles, random +import keycard_go +import ../../../app/core/eventemitter +import ../../../app/core/tasks/[qt, threadpool] +import ../../../constants as status_const + +import constants + +type FlowType {.pure.} = enum + NoFlow = -1 # this type is added only for the desktop app purpose + GetAppInfo = 0 # enumeration of these flows should follow enumeration in the `status-keycard-go` + RecoverAccount + LoadAccount + Login + ExportPublic + Sign + ChangePIN + ChangePUK + ChangePairing + UnpairThis + UnpairOthers + DeleteAccountAndUnpair + +const PINLengthForStatusApp* = 6 +const PUKLengthForStatusApp* = 12 + +const SupportedMnemonicLength12* = 12 +const SupportedMnemonicLength18* = 18 +const SupportedMnemonicLength24* = 24 + +const MnemonicLengthForStatusApp = SupportedMnemonicLength12 +const TimerIntervalInMilliseconds = 3 * 1000 # 3 seconds + +const SignalKeycardResponse* = "keycardResponse" + +logScope: + topics = "keycard-service" + +include ../../common/json_utils +include ../../common/mnemonics +include internal +include async_tasks + +type + KeycardArgs* = ref object of Args + flowType*: string + flowEvent*: KeycardEvent + +QtObject: + type Service* = ref object of QObject + events: EventEmitter + threadpool: ThreadPool + closingApp: bool + currentFlow: FlowType + + ################################################# + # Forward declaration section + proc runTimer(self: Service) + proc factoryReset*(self: Service) + + ################################################# + + proc setup(self: Service) = + self.QObject.setup + + proc delete*(self: Service) = + self.closingApp = true + self.QObject.delete + + proc newService*(events: EventEmitter, threadpool: ThreadPool): Service = + new(result) + result.setup() + result.events = events + result.threadpool = threadpool + result.closingApp = false + result.currentFlow = FlowType.NoFlow + + proc init*(self: Service) = + debug "init keycard using ", pairingsJson=status_const.KEYCARDPAIRINGDATAFILE + let initResp = keycard_go.keycardInitFlow(status_const.KEYCARDPAIRINGDATAFILE) + debug "initialization response: ", initResp + + proc processSignal(self: Service, signal: string) = + var jsonSignal: JsonNode + try: + jsonSignal = signal.parseJson + except: + error "Invalid signal received", data = signal + return + + debug "keycard_signal", response=signal + + var typeObj, eventObj: JsonNode + if(not jsonSignal.getProp(ResponseKeyType, typeObj) or + not jsonSignal.getProp(ResponseKeyEvent, eventObj)): + return + + let flowType = typeObj.getStr + let flowEvent = toKeycardEvent(eventObj) + self.events.emit(SignalKeycardResponse, KeycardArgs(flowType: flowType, flowEvent: flowEvent)) + + proc receiveKeycardSignal(self: Service, signal: string) {.slot.} = + self.processSignal(signal) + + proc buildSeedPhrasesFromIndexes*(self: Service, seedPhraseIndexes: seq[int]): seq[string] = + var seedPhrase: seq[string] + for ind in seedPhraseIndexes: + seedPhrase.add(englishWords[ind]) + return seedPhrase + + proc startFlow(self: Service, payload: JsonNode) = + let response = keycard_go.keycardStartFlow(self.currentFlow.int, $payload) + debug "keycardStartFlow", flowType=self.currentFlow.int, payload=payload, response=response + + proc resumeFlow(self: Service, payload: JsonNode) = + let response = keycard_go.keycardResumeFlow($payload) + debug "keycardResumeFlow", flowType=self.currentFlow.int, payload=payload, response=response + + proc cancelCurrentFlow*(self: Service) = + let response = keycard_go.keycardCancelFlow() + self.currentFlow = FlowType.NoFlow + debug "keycardCancelFlow", flowType=self.currentFlow.int, response=response + + proc generateRandomPUK*(self: Service): string = + for i in 0 ..< PUKLengthForStatusApp: + result = result & $rand(0 .. 9) + + proc onTimeout(self: Service, response: string) {.slot.} = + if(self.closingApp or self.currentFlow == FlowType.NoFlow): + return + debug "onTimeout, about to start flow: ", flowType=self.currentFlow + self.startFlow(%* { }) + + proc runTimer(self: Service) = + if(self.closingApp or self.currentFlow == FlowType.NoFlow): + return + + let arg = TimerTaskArg( + tptr: cast[ByteAddress](timerTask), + vptr: cast[ByteAddress](self.vptr), + slot: "onTimeout", + timeoutInMilliseconds: TimerIntervalInMilliseconds + ) + self.threadpool.start(arg) + + proc startLoadAccountFlow*(self: Service, factoryReset: bool) = + var payload = %* { } + if factoryReset: + payload[RequestParamFactoryReset] = %* factoryReset + self.currentFlow = FlowType.LoadAccount + self.startFlow(payload) + + proc startLoadAccountFlowWithSeedPhrase*(self: Service, seedPhraseLength: int, seedPhrase: string, factoryReset: bool) = + if seedPhrase.len == 0: + info "empty seed phrase provided" + return + var payload = %* { + RequestParamOverwrite: true, + RequestParamMnemonicLen: seedPhraseLength, + RequestParamNewPUK: self.generateRandomPUK(), + RequestParamMnemonic: seedPhrase + } + if factoryReset: + payload[RequestParamFactoryReset] = %* factoryReset + self.currentFlow = FlowType.LoadAccount + self.startFlow(payload) + + proc startLoginFlow*(self: Service) = + let payload = %* { } + self.currentFlow = FlowType.Login + self.startFlow(payload) + + proc startLoginFlowAutomatically*(self: Service, pin: string) = + let payload = %* { + RequestParamPIN: pin + } + self.currentFlow = FlowType.Login + self.startFlow(payload) + + proc startRecoverAccountFlow*(self: Service) = + let payload = %* { } + self.currentFlow = FlowType.RecoverAccount + self.startFlow(payload) + + proc storePin*(self: Service, pin: string, puk: string) = + if pin.len == 0: + info "empty pin provided" + return + var payload = %* { + RequestParamOverwrite: true, + RequestParamMnemonicLen: MnemonicLengthForStatusApp, + RequestParamPIN: pin, + RequestParamNewPIN: pin + } + if puk.len > 0: + payload[RequestParamNewPUK] = %* puk + self.resumeFlow(payload) + + proc enterPin*(self: Service, pin: string) = + if pin.len == 0: + info "empty pin provided" + return + var payload = %* { + RequestParamPIN: pin + } + self.resumeFlow(payload) + + proc enterPuk*(self: Service, puk: string) = + if puk.len == 0: + info "empty puk provided" + return + var payload = %* { + RequestParamPUK: puk + } + self.resumeFlow(payload) + + proc storeSeedPhrase*(self: Service, seedPhraseLength: int, seedPhrase: string) = + if seedPhrase.len == 0: + info "empty seed phrase provided" + return + var payload = %* { + RequestParamOverwrite: true, + RequestParamMnemonicLen: seedPhraseLength, + RequestParamNewPUK: self.generateRandomPUK(), + RequestParamMnemonic: seedPhrase + } + self.resumeFlow(payload) + + proc resumeCurrentFlow*(self: Service) = + var payload = %* { } + self.resumeFlow(payload) + + proc resumeCurrentFlowLater*(self: Service) = + self.runTimer() + + proc factoryReset*(self: Service) = + var payload = %* { + RequestParamFactoryReset: true + } + self.resumeFlow(payload) \ No newline at end of file diff --git a/src/app_service/service/keychain/service.nim b/src/app_service/service/keychain/service.nim index c7cb9eb413..5924da0170 100644 --- a/src/app_service/service/keychain/service.nim +++ b/src/app_service/service/keychain/service.nim @@ -42,21 +42,21 @@ QtObject: signalConnect(self.keychainManager, "error(QString, int, QString)", self, "onKeychainManagerError(QString, int, QString)", 2) - proc storePassword*(self: Service, username: string, password: string) = - self.keychainManager.storeDataAsync(username, password) - - proc tryToObtainPassword*(self: Service, username: string) = - self.keychainManager.readDataAsync(username) + proc storeData*(self: Service, key: string, data: string) = + self.keychainManager.storeDataAsync(key, data) + + proc tryToObtainData*(self: Service, key: string) = + self.keychainManager.readDataAsync(key) proc onKeychainManagerError*(self: Service, errorType: string, errorCode: int, errorDescription: string) {.slot.} = ## This slot is called in case an error occured while we're dealing with ## KeychainManager. So far we're just logging the error. - info "KeychainManager stopped: ", msg = errorCode, errorDescription + info "KeychainManager stopped: ", errCode=errorCode, errType=errorType, errDesc=errorDescription let arg = KeyChainServiceArg(errCode: errorCode, errType: errorType, errDescription: errorDescription) - self.events.emit("", arg) + self.events.emit(SIGNAL_KEYCHAIN_SERVICE_ERROR, arg) proc onKeychainManagerSuccess*(self: Service, data: string) {.slot.} = ## This slot is called in case a password is successfully retrieved from the diff --git a/src/backend/accounts.nim b/src/backend/accounts.nim index 478d209940..cd29f99866 100644 --- a/src/backend/accounts.nim +++ b/src/backend/accounts.nim @@ -218,6 +218,26 @@ proc saveAccountAndLogin*(hashedPassword: string, account, subaccounts, settings error "error doing rpc request", methodName = "saveAccountAndLogin", exception=e.msg raise newException(RpcException, e.msg) +proc saveAccountAndLoginWithKeycard*(chatKey, password: string, account, subaccounts, settings, config: JsonNode): + RpcResponse[JsonNode] {.raises: [Exception].} = + try: + let response = status_go.saveAccountAndLoginWithKeycard($account, password, $settings, $config, $subaccounts, chatKey) + result.result = Json.decode(response, JsonNode) + + except RpcException as e: + error "error doing rpc request", methodName = "saveAccountAndLogin", exception=e.msg + raise newException(RpcException, e.msg) + +proc convertToKeycardAccount*(keyStoreDir: string, account: JsonNode, settings: JsonNode, password: string, newPassword: string): + RpcResponse[JsonNode] {.raises: [Exception].} = + try: + let response = status_go.convertToKeycardAccount(keyStoreDir, $account, $settings, password, newPassword) + result.result = Json.decode(response, JsonNode) + + except RpcException as e: + error "error doing rpc request", methodName = "convertToKeycardAccount", exception=e.msg + raise newException(RpcException, e.msg) + proc login*(name, keyUid, hashedPassword, thumbnail, large: string, nodeCfgObj: string): RpcResponse[JsonNode] {.raises: [Exception].} = @@ -238,6 +258,14 @@ proc login*(name, keyUid, hashedPassword, thumbnail, large: string, nodeCfgObj: error "error doing rpc request", methodName = "login", exception=e.msg raise newException(RpcException, e.msg) +proc loginWithKeycard*(chatKey, password: string, account: JsonNode): RpcResponse[JsonNode] {.raises: [Exception].} = + try: + let response = status_go.loginWithKeycard($account, password, chatKey) + result.result = Json.decode(response, JsonNode) + except RpcException as e: + error "error doing rpc request", methodName = "loginWithKeycard", exception=e.msg + raise newException(RpcException, e.msg) + proc verifyAccountPassword*(address: string, password: string, keystoreDir: string): RpcResponse[JsonNode] {.raises: [Exception].} = try: diff --git a/src/constants.nim b/src/constants.nim index 889c528f89..ac2fc78eb4 100644 --- a/src/constants.nim +++ b/src/constants.nim @@ -59,6 +59,7 @@ let ROOTKEYSTOREDIR* = joinPath(baseDir, "data", "keystore") TMPDIR* = joinPath(baseDir, "tmp") & sep LOGDIR* = joinPath(baseDir, "logs") & sep + KEYCARDPAIRINGDATAFILE* = joinPath(baseDir, "data", "keycard/pairings.json") proc ensureDirectories*(dataDir, tmpDir, logDir: string) = createDir(dataDir) diff --git a/src/nim_status_client.nim b/src/nim_status_client.nim index b4a934be86..63cb8ca3c3 100644 --- a/src/nim_status_client.nim +++ b/src/nim_status_client.nim @@ -1,6 +1,7 @@ import NimQml, chronicles, os, strformat, strutils, times, md5, json import status_go +import keycard_go import app/core/main import constants @@ -11,6 +12,7 @@ logScope: topics = "status-app" var signalsManagerQObjPointer: pointer +var keycardServiceQObjPointer: pointer proc isExperimental(): string = result = if getEnv("EXPERIMENTAL") == "1": "1" else: "0" # value explicity passed to avoid trusting input @@ -66,11 +68,15 @@ proc setupRemoteSignalsHandling() = # Please note that this must use the `cdecl` calling convention because # it will be passed as a regular C function to statusgo_backend. This means that # we cannot capture any local variables here (we must rely on globals) - var callback: SignalCallback = proc(p0: cstring) {.cdecl.} = + var callbackStatusGo: status_go.SignalCallback = proc(p0: cstring) {.cdecl.} = if signalsManagerQObjPointer != nil: signal_handler(signalsManagerQObjPointer, p0, "receiveSignal") + status_go.setSignalEventCallback(callbackStatusGo) - status_go.setSignalEventCallback(callback) + var callbackKeycardGo: keycard_go.KeycardSignalCallback = proc(p0: cstring) {.cdecl.} = + if keycardServiceQObjPointer != nil: + signal_handler(keycardServiceQObjPointer, p0, "receiveKeycardSignal") + keycard_go.setSignalEventCallback(callbackKeycardGo) proc mainProc() = if defined(macosx) and defined(production): @@ -133,6 +139,7 @@ proc mainProc() = defer: info "shutting down..." signalsManagerQObjPointer = nil + keycardServiceQObjPointer = nil isProductionQVariant.delete() isExperimentalQVariant.delete() signalsManagerQVariant.delete() @@ -149,6 +156,12 @@ proc mainProc() = info "Terminating the app as the second instance" quit() + # We need these global variables in order to be able to access the application + # from the non-closure callback passed to `statusgo_backend.setSignalEventCallback` + signalsManagerQObjPointer = cast[pointer](statusFoundation.signalsManager.vptr) + keycardServiceQObjPointer = cast[pointer](appController.keycardService.vptr) + setupRemoteSignalsHandling() + info fmt("Version: {DESKTOP_VERSION}") info fmt("Commit: {GIT_COMMIT}") info "Current date:", currentDateTime=now() @@ -156,11 +169,6 @@ proc mainProc() = info "starting application controller..." appController.start() - # We need this global variable in order to be able to access the application - # from the non-closure callback passed to `statusgo_backend.setSignalEventCallback` - signalsManagerQObjPointer = cast[pointer](statusFoundation.signalsManager.vptr) - setupRemoteSignalsHandling() - info "starting application..." app.exec() diff --git a/ui/app/AppLayouts/Onboarding/OnboardingLayout.qml b/ui/app/AppLayouts/Onboarding/OnboardingLayout.qml index c387921f70..883b5b5dd1 100644 --- a/ui/app/AppLayouts/Onboarding/OnboardingLayout.qml +++ b/ui/app/AppLayouts/Onboarding/OnboardingLayout.qml @@ -31,41 +31,86 @@ OnboardingBasePage { { return allowNotificationsViewComponent } - else if (root.startupStore.currentStartupState.stateType === Constants.startupState.welcome) + if (root.startupStore.currentStartupState.stateType === Constants.startupState.welcome) { return welcomeViewComponent } - else if (root.startupStore.currentStartupState.stateType === Constants.startupState.welcomeNewStatusUser || - root.startupStore.currentStartupState.stateType === Constants.startupState.welcomeOldStatusUser || - root.startupStore.currentStartupState.stateType === Constants.startupState.userProfileImportSeedPhrase) + if (root.startupStore.currentStartupState.stateType === Constants.startupState.welcomeNewStatusUser || + root.startupStore.currentStartupState.stateType === Constants.startupState.welcomeOldStatusUser || + root.startupStore.currentStartupState.stateType === Constants.startupState.userProfileImportSeedPhrase) { return keysMainViewComponent } - else if (root.startupStore.currentStartupState.stateType === Constants.startupState.userProfileCreate || - root.startupStore.currentStartupState.stateType === Constants.startupState.userProfileChatKey) + if (root.startupStore.currentStartupState.stateType === Constants.startupState.userProfileCreate || + root.startupStore.currentStartupState.stateType === Constants.startupState.userProfileChatKey) { return insertDetailsViewComponent } - else if (root.startupStore.currentStartupState.stateType === Constants.startupState.userProfileCreatePassword) + if (root.startupStore.currentStartupState.stateType === Constants.startupState.userProfileCreatePassword) { return createPasswordViewComponent } - else if (root.startupStore.currentStartupState.stateType === Constants.startupState.userProfileConfirmPassword) + if (root.startupStore.currentStartupState.stateType === Constants.startupState.userProfileConfirmPassword) { return confirmPasswordViewComponent } - else if (root.startupStore.currentStartupState.stateType === Constants.startupState.biometrics) + if (root.startupStore.currentStartupState.stateType === Constants.startupState.biometrics) { return touchIdAuthViewComponent } - else if (root.startupStore.currentStartupState.stateType === Constants.startupState.userProfileEnterSeedPhrase) + if (root.startupStore.currentStartupState.stateType === Constants.startupState.userProfileEnterSeedPhrase) { return seedPhraseInputViewComponent } - else if (root.startupStore.currentStartupState.stateType === Constants.startupState.login) + if (root.startupStore.currentStartupState.stateType === Constants.startupState.login || + root.startupStore.currentStartupState.stateType === Constants.startupState.loginKeycardInsertKeycard || + root.startupStore.currentStartupState.stateType === Constants.startupState.loginKeycardReadingKeycard || + root.startupStore.currentStartupState.stateType === Constants.startupState.loginKeycardEnterPin || + root.startupStore.currentStartupState.stateType === Constants.startupState.loginKeycardWrongKeycard || + root.startupStore.currentStartupState.stateType === Constants.startupState.loginKeycardWrongPin || + root.startupStore.currentStartupState.stateType === Constants.startupState.loginKeycardMaxPinRetriesReached || + root.startupStore.currentStartupState.stateType === Constants.startupState.loginKeycardMaxPukRetriesReached || + root.startupStore.currentStartupState.stateType === Constants.startupState.loginKeycardEmpty) { return loginViewComponent } + if (root.startupStore.currentStartupState.stateType === Constants.startupState.keycardPluginReader || + root.startupStore.currentStartupState.stateType === Constants.startupState.keycardInsertKeycard || + root.startupStore.currentStartupState.stateType === Constants.startupState.keycardReadingKeycard) + { + return keycardInitViewComponent + } + if (root.startupStore.currentStartupState.stateType === Constants.startupState.keycardCreatePin || + root.startupStore.currentStartupState.stateType === Constants.startupState.keycardRepeatPin || + root.startupStore.currentStartupState.stateType === Constants.startupState.keycardPinSet || + root.startupStore.currentStartupState.stateType === Constants.startupState.keycardEnterPin || + root.startupStore.currentStartupState.stateType === Constants.startupState.keycardWrongPin) + { + return keycardPinViewComponent + } + if (root.startupStore.currentStartupState.stateType === Constants.startupState.keycardDisplaySeedPhrase) + { + return seedphraseViewComponent + } + if (root.startupStore.currentStartupState.stateType === Constants.startupState.keycardEnterSeedPhraseWords) + { + return seedphraseWordsInputViewComponent + } + if (root.startupStore.currentStartupState.stateType === Constants.startupState.keycardNotEmpty || + root.startupStore.currentStartupState.stateType === Constants.startupState.keycardEmpty || + root.startupStore.currentStartupState.stateType === Constants.startupState.keycardLocked || + root.startupStore.currentStartupState.stateType === Constants.startupState.keycardRecover || + root.startupStore.currentStartupState.stateType === Constants.startupState.keycardMaxPairingSlotsReached || + root.startupStore.currentStartupState.stateType === Constants.startupState.keycardMaxPinRetriesReached || + root.startupStore.currentStartupState.stateType === Constants.startupState.keycardMaxPukRetriesReached) + { + return keycardStateViewComponent + } + if (root.startupStore.currentStartupState.stateType === Constants.startupState.keycardEnterPuk || + root.startupStore.currentStartupState.stateType === Constants.startupState.keycardWrongPuk) + { + return keycardPukViewComponent + } return undefined } @@ -87,7 +132,9 @@ OnboardingBasePage { onAccountImportError: { if (error === Constants.existingAccountError) { msgDialog.title = qsTr("Keys for this account already exist") - msgDialog.text = qsTr("Keys for this account already exist and can't be added again. If you've lost your password, passcode or Keycard, uninstall the app, reinstall and access your keys by entering your seed phrase") + msgDialog.text = qsTr("Keys for this account already exist and can't be added again. If you've lost \ +your password, passcode or Keycard, uninstall the app, reinstall and access your keys by entering your seed phrase. In \ +case of Keycard try recovering using PUK or reinstall the app and try login with the Keycard option.") } else { msgDialog.title = qsTr("Error importing seed") msgDialog.text = error @@ -169,4 +216,46 @@ OnboardingBasePage { startupStore: root.startupStore } } + + Component { + id: keycardInitViewComponent + KeycardInitView { + startupStore: root.startupStore + } + } + + Component { + id: keycardPinViewComponent + KeycardPinView { + startupStore: root.startupStore + } + } + + Component { + id: keycardPukViewComponent + KeycardPukView { + startupStore: root.startupStore + } + } + + Component { + id: seedphraseViewComponent + SeedPhraseView { + startupStore: root.startupStore + } + } + + Component { + id: seedphraseWordsInputViewComponent + SeedPhraseWordsInputView { + startupStore: root.startupStore + } + } + + Component { + id: keycardStateViewComponent + KeycardStateView { + startupStore: root.startupStore + } + } } diff --git a/ui/app/AppLayouts/Onboarding/panels/AccountMenuItemPanel.qml b/ui/app/AppLayouts/Onboarding/panels/AccountMenuItemPanel.qml index 907a7e493f..689f13df04 100644 --- a/ui/app/AppLayouts/Onboarding/panels/AccountMenuItemPanel.qml +++ b/ui/app/AppLayouts/Onboarding/panels/AccountMenuItemPanel.qml @@ -17,6 +17,7 @@ MenuItem { property string colorId: "" property var colorHash property url image: "" + property bool keycardCreatedAccount: false property StatusIconSettings iconSettings: StatusIconSettings { name: "add" } @@ -65,8 +66,28 @@ MenuItem { font.pixelSize: 15 anchors.verticalCenter: parent.verticalCenter anchors.left: userImageOrIcon.right - anchors.leftMargin: 16 + anchors.right: root.keycardCreatedAccount? keycardIcon.left : parent.right + anchors.leftMargin: Style.current.padding color: !!root.colorId ? Theme.palette.directColor1 : Theme.palette.primaryColor1 + elide: Text.ElideRight + } + + Loader { + id: keycardIcon + active: root.keycardCreatedAccount + sourceComponent: keycardIconComponent + anchors.rightMargin: Style.current.padding + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + } + + Component { + id: keycardIconComponent + StatusIcon { + icon: "keycard" + height: Style.current.padding + color: Theme.palette.baseColor1 + } } } diff --git a/ui/app/AppLayouts/Onboarding/popups/KeycardCreatePINModal.qml b/ui/app/AppLayouts/Onboarding/popups/KeycardCreatePINModal.qml index 75aaeb37e0..6f6d7a4eeb 100644 --- a/ui/app/AppLayouts/Onboarding/popups/KeycardCreatePINModal.qml +++ b/ui/app/AppLayouts/Onboarding/popups/KeycardCreatePINModal.qml @@ -11,7 +11,6 @@ import utils 1.0 as Imports import shared 1.0 import shared.controls 1.0 -import shared.keycard 1.0 StatusModal { property bool firstPINFieldValid: false diff --git a/ui/app/AppLayouts/Onboarding/stores/StartupStore.qml b/ui/app/AppLayouts/Onboarding/stores/StartupStore.qml index 3b1a994130..33051e30d6 100644 --- a/ui/app/AppLayouts/Onboarding/stores/StartupStore.qml +++ b/ui/app/AppLayouts/Onboarding/stores/StartupStore.qml @@ -35,6 +35,10 @@ QtObject { return root.startupModuleInst.generateImage(source, aX, aY, bX, bY) } + function getCroppedProfileImage() { + return root.startupModuleInst.getCroppedProfileImage() + } + function setDisplayName(value) { root.startupModuleInst.setDisplayName(value) } @@ -51,6 +55,18 @@ QtObject { return root.startupModuleInst.getPassword() } + function setPin(value) { + root.startupModuleInst.setPin(value) + } + + function getPin() { + return root.startupModuleInst.getPin() + } + + function setPuk(value) { + root.startupModuleInst.setPuk(value) + } + function getPasswordStrengthScore(password) { let userName = root.startupModuleInst.importedAccountAlias return root.startupModuleInst.getPasswordStrengthScore(password, userName) @@ -63,4 +79,12 @@ QtObject { function setSelectedLoginAccountByIndex(index) { root.startupModuleInst.setSelectedLoginAccountByIndex(index) } + + function checkRepeatedKeycardPinWhileTyping(pin) { + return root.startupModuleInst.checkRepeatedKeycardPinWhileTyping(pin) + } + + function getSeedPhrase() { + return root.startupModuleInst.getSeedPhrase() + } } diff --git a/ui/app/AppLayouts/Onboarding/views/InsertDetailsView.qml b/ui/app/AppLayouts/Onboarding/views/InsertDetailsView.qml index a49cd948f8..bbbd04f65f 100644 --- a/ui/app/AppLayouts/Onboarding/views/InsertDetailsView.qml +++ b/ui/app/AppLayouts/Onboarding/views/InsertDetailsView.qml @@ -36,6 +36,7 @@ Item { } nameInput.text = root.startupStore.getDisplayName(); nameInput.input.edit.forceActiveFocus(); + userImage.image.source = root.startupStore.getCroppedProfileImage(); } Loader { diff --git a/ui/app/AppLayouts/Onboarding/views/KeycardFlowSelectionView.qml b/ui/app/AppLayouts/Onboarding/views/KeycardFlowSelectionView.qml deleted file mode 100644 index b3ff40a6d3..0000000000 --- a/ui/app/AppLayouts/Onboarding/views/KeycardFlowSelectionView.qml +++ /dev/null @@ -1,115 +0,0 @@ -import QtQuick 2.13 - -import shared.keycard 1.0 -import "../popups" -import "../stores" - -Item { - enum OnboardingFlow { - Recover, - Generate, - ImportMnemonic - } - - property var onClosed: function () {} - property bool connected: false - property int flow: KeycardFlowSelectionView.OnboardingFlow.Recover - - id: keycardView - Component.onCompleted: { - insertCard.open() - KeycardStore.startConnection() - } - - KeycardCreatePINModal { - id: createPinModal - onSubmitBtnClicked: KeycardStore.init(pin) - onClosed: function () { - if (!createPinModal.submitted) { - keycardView.onClosed() - } - } - } - - PairingModal { - id: pairingModal - onClosed: function () { - if (!pairingModal.submitted) { - keycardView.onClosed() - } - } - } - - PINModal { - id: pinModal - onClosed: function () { - if (!pinModal.submitted) { - keycardView.onClosed() - } - } - } - - InsertCard { - id: insertCard - onCancel: function() { - keycardView.onClosed() - } - } - - // Not Refactored Yet -// Connections { -// id: connection -// target: OnboardingStore.keycardModelInst -// ignoreUnknownSignals: true - -// onCardUnpaired: { -// pairingModal.open() -// } - -// onCardPaired: { -// pinModal.open() -// } - -// onCardAuthenticated: { -// switch (flow) { -// case OnboardingFlow.Recover: { -// KeycardStore.recoverAccount(); -// break; -// } -// case OnboardingFlow.Generate: { -// break; -// } -// case OnboardingFlow.ImportMnemonic: { -// break; -// } -// } -// } - -// //TODO: support the states below - -// onCardPreInit: { -// createPinModal.open() -// } - -// onCardFrozen: { -// keycardView.onClosed() - -// } - -// onCardBlocked: { -// keycardView.onClosed() -// } - -// // TODO: handle these by showing an error an prompting for another card -// // later add factory reset option for the NoFreeSlots case - -// onCardNoFreeSlots: { -// keycardView.onClosed() -// } - -// onCardNotKeycard: { -// keycardView.onClosed() -// } - -// } -} diff --git a/ui/app/AppLayouts/Onboarding/views/KeycardInitView.qml b/ui/app/AppLayouts/Onboarding/views/KeycardInitView.qml new file mode 100644 index 0000000000..c326fab638 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding/views/KeycardInitView.qml @@ -0,0 +1,97 @@ +import QtQuick 2.13 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.13 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 + +import utils 1.0 + +import "../stores" + +Item { + id: root + + property StartupStore startupStore + + Component.onCompleted: { + if(root.startupStore.currentStartupState.stateType === Constants.startupState.keycardPluginReader) { + root.startupStore.doPrimaryAction() + } + } + + QtObject { + id: d + + property int index: 0 + property variant images : [ + Style.svg("keycard/card0@2x"), + Style.svg("keycard/card1@2x"), + Style.svg("keycard/card2@2x"), + Style.svg("keycard/card3@2x") + ] + } + + Timer { + interval: 400 + running: true + repeat: true + onTriggered: { + d.index++ + } + } + + ColumnLayout { + anchors.centerIn: parent + spacing: Style.current.padding + + Image { + Layout.alignment: Qt.AlignHCenter + Layout.preferredHeight: sourceSize.height + Layout.preferredWidth: sourceSize.width + fillMode: Image.PreserveAspectFit + antialiasing: true + source: d.images[d.index % d.images.length] + mipmap: true + } + + StatusBaseText { + id: title + Layout.alignment: Qt.AlignHCenter + font.weight: Font.Bold + } + } + + states: [ + State { + name: Constants.startupState.keycardPluginReader + when: root.startupStore.currentStartupState.stateType === Constants.startupState.keycardPluginReader + PropertyChanges { + target: title + text: qsTrId("Plug in Keycard reader...") + font.pixelSize: Constants.keycard.general.fontSize1 + color: Theme.palette.directColor1 + } + }, + State { + name: Constants.startupState.keycardInsertKeycard + when: root.startupStore.currentStartupState.stateType === Constants.startupState.keycardInsertKeycard + PropertyChanges { + target: title + text: qsTrId("Insert your Keycard...") + font.pixelSize: Constants.keycard.general.fontSize1 + color: Theme.palette.directColor1 + } + }, + State { + name: Constants.startupState.keycardReadingKeycard + when: root.startupStore.currentStartupState.stateType === Constants.startupState.keycardReadingKeycard + PropertyChanges { + target: title + text: qsTr("Reading Keycard...") + font.pixelSize: Constants.keycard.general.fontSize2 + color: Theme.palette.baseColor1 + } + } + ] +} diff --git a/ui/app/AppLayouts/Onboarding/views/KeycardPinView.qml b/ui/app/AppLayouts/Onboarding/views/KeycardPinView.qml new file mode 100644 index 0000000000..a2894f2eb2 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding/views/KeycardPinView.qml @@ -0,0 +1,200 @@ +import QtQuick 2.13 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.13 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Controls.Validators 0.1 + +import utils 1.0 + +import "../stores" + +Item { + id: root + + property StartupStore startupStore + + property int remainingAttempts: parseInt(root.startupStore.startupModuleInst.keycardData, 10) + + onRemainingAttemptsChanged: { + if (root.startupStore.currentStartupState.stateType === Constants.startupState.keycardWrongPin) { + pinInputField.statesInitialization() + pinInputField.forceFocus() + } + } + + onStateChanged: { + if(state === Constants.startupState.keycardPinSet) { + pinInputField.setPin("123456") // we are free to set fake pin in this case + } else { + pinInputField.statesInitialization() + pinInputField.forceFocus() + } + } + + Timer { + id: timer + interval: 1000 + running: root.startupStore.currentStartupState.stateType === Constants.startupState.keycardPinSet + onTriggered: { + root.startupStore.doPrimaryAction() + } + } + + ColumnLayout { + anchors.centerIn: parent + spacing: Style.current.padding + + Image { + Layout.alignment: Qt.AlignHCenter + Layout.preferredHeight: sourceSize.height + Layout.preferredWidth: sourceSize.width + fillMode: Image.PreserveAspectFit + antialiasing: true + source: root.startupStore.currentStartupState.stateType === Constants.startupState.keycardPinSet? + Style.svg("keycard/card-success3@2x") : + Style.svg("keycard/card3@2x") + mipmap: true + } + + StatusBaseText { + id: title + Layout.alignment: Qt.AlignHCenter + font.weight: Font.Bold + font.pixelSize: Constants.keycard.general.fontSize1 + color: Theme.palette.directColor1 + } + + StatusPinInput { + id: pinInputField + Layout.alignment: Qt.AlignHCenter + validator: StatusIntValidator{bottom: 0; top: 999999;} + pinLen: Constants.keycard.general.keycardPinLength + enabled: root.startupStore.currentStartupState.stateType !== Constants.startupState.keycardPinSet + + onPinInputChanged: { + if(pinInput.length == 0) + return + if(root.state === Constants.startupState.keycardCreatePin || + root.state === Constants.startupState.keycardEnterPin || + root.state === Constants.startupState.keycardWrongPin) { + root.startupStore.setPin(pinInput) + root.startupStore.doPrimaryAction() + } + else if(root.state === Constants.startupState.keycardRepeatPin) { + let pinsMatch = root.startupStore.checkRepeatedKeycardPinWhileTyping(pinInput) + if (pinsMatch) { + info.text = qsTr("It is very important that you do not loose this PIN") + root.startupStore.doPrimaryAction() + } else { + info.text = qsTr("PINs don't match") + } + } + } + } + + StatusBaseText { + id: info + Layout.alignment: Qt.AlignHCenter + font.pixelSize: Constants.keycard.general.fontSize3 + wrapMode: Text.WordWrap + } + + StatusBaseText { + id: message + Layout.alignment: Qt.AlignHCenter + font.pixelSize: Constants.keycard.general.fontSize3 + wrapMode: Text.WordWrap + } + } + + states: [ + State { + name: Constants.startupState.keycardCreatePin + when: root.startupStore.currentStartupState.stateType === Constants.startupState.keycardCreatePin + PropertyChanges { + target: title + text: qsTr("Create new Keycard PIN") + } + PropertyChanges { + target: info + text: qsTr("It is very important that you do not loose this PIN") + } + PropertyChanges { + target: message + text: "" + } + }, + State { + name: Constants.startupState.keycardRepeatPin + when: root.startupStore.currentStartupState.stateType === Constants.startupState.keycardRepeatPin + PropertyChanges { + target: title + text: qsTr("Repeat Keycard PIN") + } + PropertyChanges { + target: info + text: qsTr("It is very important that you do not loose this PIN") + color: Theme.palette.dangerColor1 + } + PropertyChanges { + target: message + text: "" + } + }, + State { + name: Constants.startupState.keycardPinSet + when: root.startupStore.currentStartupState.stateType === Constants.startupState.keycardPinSet + PropertyChanges { + target: title + text: qsTr("Keycard PIN set") + } + PropertyChanges { + target: info + text: "" + } + PropertyChanges { + target: message + text: "" + } + }, + State { + name: Constants.startupState.keycardEnterPin + when: root.startupStore.currentStartupState.stateType === Constants.startupState.keycardEnterPin + PropertyChanges { + target: title + text: qsTr("Enter Keycard PIN") + } + PropertyChanges { + target: info + text: "" + } + PropertyChanges { + target: message + text: "" + } + }, + State { + name: Constants.startupState.keycardWrongPin + when: root.startupStore.currentStartupState.stateType === Constants.startupState.keycardWrongPin + PropertyChanges { + target: title + text: qsTr("Enter Keycard PIN") + } + PropertyChanges { + target: info + text: qsTr("PIN incorrect") + color: Theme.palette.dangerColor1 + } + PropertyChanges { + target: message + text: qsTr("%n attempt(s) remaining", "", d.remainingAttempts) + color: root.remainingAttempts === 1? + Theme.palette.dangerColor1 : + Theme.palette.baseColor1 + } + } + ] +} diff --git a/ui/app/AppLayouts/Onboarding/views/KeycardPukView.qml b/ui/app/AppLayouts/Onboarding/views/KeycardPukView.qml new file mode 100644 index 0000000000..b4efc4d956 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding/views/KeycardPukView.qml @@ -0,0 +1,195 @@ +import QtQuick 2.13 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.13 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Controls.Validators 0.1 + +import utils 1.0 + +import "../stores" + +Item { + id: root + + property StartupStore startupStore + + property int remainingAttempts: parseInt(root.startupStore.startupModuleInst.keycardData, 10) + + Component.onCompleted: { + d.allEntriesValid = false + d.pukArray = Array(d.pukLength) + d.pukArray.fill("") + } + + QtObject { + id: d + + readonly property int pukLength: 12 + property var pukArray: [] + property bool allEntriesValid: false + readonly property int rowSpacing: Style.current.padding + + function updateValidity() { + for(let i = 0; i < pukLength; ++i) { + if(pukArray[i].length !== 1) { + allEntriesValid = false + return + } + } + allEntriesValid = true + } + + function submitPuk() { + let puk = d.pukArray.join("") + root.startupStore.setPuk(puk) + root.startupStore.doPrimaryAction() + } + } + + Item { + anchors.top: parent.top + anchors.bottom: footerWrapper.top + anchors.left: parent.left + anchors.right: parent.right + + ColumnLayout { + anchors.centerIn: parent + spacing: Style.current.padding + + StatusBaseText { + id: title + Layout.alignment: Qt.AlignHCenter + font.pixelSize: Constants.keycard.general.fontSize1 + font.weight: Font.Bold + color: Theme.palette.directColor1 + text: qsTr("Enter PUK code to recover Keycard") + } + + RowLayout { + id: rowLayout + Layout.alignment: Qt.AlignHCenter + spacing: d.rowSpacing + + Component.onCompleted: { + for (var i = 0; i < children.length - 1; ++i) { + if(children[i] && children[i].input && children[i+1] && children[i+1].input){ + children[i].input.tabNavItem = children[i+1].input.edit + } + } + if(children.length > 0){ + children[0].input.edit.forceActiveFocus() + } + } + + Repeater { + model: d.pukLength + delegate: StatusInput { + Layout.preferredWidth: Constants.keycard.general.pukCellWidth + Layout.preferredHeight: Constants.keycard.general.pukCellHeight + input.acceptReturn: true + validators: [ + StatusRegularExpressionValidator { + regularExpression: /[0-9]/ + errorMessage: "" + }, + StatusMinLengthValidator { + minLength: 1 + errorMessage: "" + } + ] + + onTextChanged: { + text = text.trim() + if(text.length >= 1) { + text = text.charAt(0); + } + if(Utils.isDigit(text)) { + let nextInd = index+1 + if(nextInd <= rowLayout.children.length - 1 && + rowLayout.children[nextInd] && + rowLayout.children[nextInd].input){ + rowLayout.children[nextInd].input.edit.forceActiveFocus() + } + } + else { + text = "" + } + d.pukArray[index] = text + d.updateValidity() + } + + onKeyPressed: { + if(input.edit.keyEvent === Qt.Key_Backspace){ + if (text == ""){ + let prevInd = index-1 + if(prevInd >= 0){ + rowLayout.children[prevInd].input.edit.forceActiveFocus() + } + } + } + else if (input.edit.keyEvent === Qt.Key_Return || + input.edit.keyEvent === Qt.Key_Enter) { + if(d.allEntriesValid) { + event.accepted = true + d.submitPuk() + } + } + } + } + } + } + + StatusBaseText { + id: info + Layout.alignment: Qt.AlignHCenter + font.pixelSize: Constants.keycard.general.fontSize3 + color: Theme.palette.dangerColor1 + horizontalAlignment: Qt.AlignHCenter + } + } + } + + Item { + id: footerWrapper + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + height: Constants.keycard.general.footerWrapperHeight + + StatusButton { + anchors.top: parent.top + anchors.topMargin: Style.current.padding + anchors.horizontalCenter: parent.horizontalCenter + enabled: d.allEntriesValid + text: qsTr("Recover Keycard") + onClicked: { + d.submitPuk() + } + } + } + + states: [ + State { + name: Constants.startupState.keycardEnterPuk + when: root.startupStore.currentStartupState.stateType === Constants.startupState.keycardEnterPuk + PropertyChanges { + target: info + text: "" + } + }, + State { + name: Constants.startupState.keycardWrongPuk + when: root.startupStore.currentStartupState.stateType === Constants.startupState.keycardWrongPuk + PropertyChanges { + target: info + text: qsTr("Invalid PUK code, %n attempt(s) remaining", "", root.remainingAttempts) + } + StateChangeScript { + script: d.allEntriesValid = false + } + } + ] +} diff --git a/ui/app/AppLayouts/Onboarding/views/KeycardStateView.qml b/ui/app/AppLayouts/Onboarding/views/KeycardStateView.qml new file mode 100644 index 0000000000..6858a4e89f --- /dev/null +++ b/ui/app/AppLayouts/Onboarding/views/KeycardStateView.qml @@ -0,0 +1,288 @@ +import QtQuick 2.13 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.13 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Controls 0.1 + +import utils 1.0 + +import "../stores" + +Item { + id: root + + property StartupStore startupStore + + Item { + anchors.top: parent.top + anchors.bottom: footerWrapper.top + anchors.left: parent.left + anchors.right: parent.right + + ColumnLayout { + anchors.centerIn: parent + spacing: Style.current.padding + + Image { + id: image + Layout.alignment: Qt.AlignHCenter + Layout.preferredHeight: sourceSize.height + Layout.preferredWidth: sourceSize.width + fillMode: Image.PreserveAspectFit + antialiasing: true + mipmap: true + } + + StatusBaseText { + id: title + Layout.alignment: Qt.AlignHCenter + font.pixelSize: Constants.keycard.general.fontSize1 + font.weight: Font.Bold + } + + StatusBaseText { + id: info + Layout.alignment: Qt.AlignHCenter + font.pixelSize: Constants.keycard.general.fontSize3 + wrapMode: Text.WordWrap + } + } + } + + Item { + id: footerWrapper + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + height: Constants.keycard.general.footerWrapperHeight + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Style.current.bigPadding + + StatusButton { + id: button + visible: text.length > 0 + Layout.alignment: Qt.AlignHCenter + focus: true + onClicked: { + root.startupStore.doPrimaryAction() + } + } + + StatusBaseText { + id: link + visible: text.length > 0 + Layout.alignment: Qt.AlignHCenter + font.pixelSize: Constants.keycard.general.buttonFontSize + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: { + parent.font.underline = true + } + onExited: { + parent.font.underline = false + } + onClicked: { + root.startupStore.doSecondaryAction() + } + } + } + } + } + + states: [ + State { + name: Constants.startupState.keycardNotEmpty + when: root.startupStore.currentStartupState.stateType === Constants.startupState.keycardNotEmpty + PropertyChanges { + target: image + source: Style.svg("keycard/card3@2x") + } + PropertyChanges { + target: title + text: qsTr("This Keycard already stores keys") + } + PropertyChanges { + target: info + text: qsTr("To generate new keys, you will need to perform a factory reset first") + color: Theme.palette.directColor1 + } + PropertyChanges { + target: button + text: qsTr("Factory reset") + type: StatusBaseButton.Type.Normal + } + PropertyChanges { + target: link + text: qsTr("Insert another Keycard") + color: Theme.palette.primaryColor1 + } + }, + State { + name: Constants.startupState.keycardEmpty + when: root.startupStore.currentStartupState.stateType === Constants.startupState.keycardEmpty + PropertyChanges { + target: image + source: Style.svg("keycard/card-error3@2x") + } + PropertyChanges { + target: title + text: "" + } + PropertyChanges { + target: info + text: qsTr("The keycard is empty") + color: Theme.palette.dangerColor1 + } + PropertyChanges { + target: button + text: qsTr("Generate new keys for this Keycard") + type: StatusBaseButton.Type.Normal + } + PropertyChanges { + target: link + text: "" + } + }, + State { + name: Constants.startupState.keycardLocked + when: root.startupStore.currentStartupState.stateType === Constants.startupState.keycardLocked + PropertyChanges { + target: image + source: Style.svg("keycard/card-error3@2x") + } + PropertyChanges { + target: title + text: qsTr("Keycard locked and already stores keys") + } + PropertyChanges { + target: info + text: qsTr("The Keycard you have inserted is locked, you will need to factory reset it before proceeding") + color: Theme.palette.directColor1 + } + PropertyChanges { + target: button + text: qsTr("Factory reset") + type: StatusBaseButton.Type.Normal + } + PropertyChanges { + target: link + text: qsTr("Insert another Keycard") + color: Theme.palette.primaryColor1 + } + }, + State { + name: Constants.startupState.keycardMaxPairingSlotsReached + when: root.startupStore.currentStartupState.stateType === Constants.startupState.keycardMaxPairingSlotsReached + PropertyChanges { + target: image + source: Style.svg("keycard/card-error3@2x") + } + PropertyChanges { + target: title + text: qsTr("Keycard locked") + color: Theme.palette.dangerColor1 + } + PropertyChanges { + target: info + text: qsTr("Max pairing slots reached for this keycard") + color: Theme.palette.dangerColor1 + } + PropertyChanges { + target: button + text: qsTr("Factory reset") + type: StatusBaseButton.Type.Normal + } + PropertyChanges { + target: link + text: qsTr("Insert another Keycard") + color: Theme.palette.primaryColor1 + } + }, + State { + name: Constants.startupState.keycardMaxPukRetriesReached + when: root.startupStore.currentStartupState.stateType === Constants.startupState.keycardMaxPukRetriesReached + PropertyChanges { + target: image + source: Style.svg("keycard/card-error3@2x") + } + PropertyChanges { + target: title + text: qsTr("Keycard locked") + color: Theme.palette.dangerColor1 + } + PropertyChanges { + target: info + text: qsTr("Max PUK retries reached for this keycard") + color: Theme.palette.dangerColor1 + } + PropertyChanges { + target: button + text: qsTr("Factory reset") + type: StatusBaseButton.Type.Normal + } + PropertyChanges { + target: link + text: qsTr("Insert another Keycard") + color: Theme.palette.primaryColor1 + } + }, + State { + name: Constants.startupState.keycardMaxPinRetriesReached + when: root.startupStore.currentStartupState.stateType === Constants.startupState.keycardMaxPinRetriesReached + PropertyChanges { + target: image + source: Style.svg("keycard/card-error3@2x") + } + PropertyChanges { + target: title + text: "" + } + PropertyChanges { + target: info + text: qsTr("Keycard locked") + color: Theme.palette.dangerColor1 + } + PropertyChanges { + target: button + text: qsTr("Recover your Keycard") + type: StatusBaseButton.Type.Danger + } + PropertyChanges { + target: link + text: "" + } + }, + State { + name: Constants.startupState.keycardRecover + when: root.startupStore.currentStartupState.stateType === Constants.startupState.keycardRecover + PropertyChanges { + target: image + source: Style.svg("keycard/card-error3@2x") + } + PropertyChanges { + target: title + text: qsTr("Recover your Keycard") + } + PropertyChanges { + target: info + text: "" + } + PropertyChanges { + target: button + text: qsTr("Recover with seed phrase") + type: StatusBaseButton.Type.Danger + } + PropertyChanges { + target: link + text: qsTr("Recover with PUK") + color: Theme.palette.dangerColor1 + } + } + ] +} diff --git a/ui/app/AppLayouts/Onboarding/views/KeysMainView.qml b/ui/app/AppLayouts/Onboarding/views/KeysMainView.qml index c30426300b..7a0ca4a91d 100644 --- a/ui/app/AppLayouts/Onboarding/views/KeysMainView.qml +++ b/ui/app/AppLayouts/Onboarding/views/KeysMainView.qml @@ -86,6 +86,7 @@ Item { id: keycardLink Layout.alignment: Qt.AlignHCenter color: Theme.palette.primaryColor1 + font.pixelSize: 15 MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor @@ -148,10 +149,10 @@ Item { //TODO remove when sync code is implemented opacity: 0.0 } -// PropertyChanges { -// target: keycardLink -// text: qsTr("Login with Keycard") -// } + PropertyChanges { + target: keycardLink + text: qsTr("Login with Keycard") + } PropertyChanges { target: seedLink text: qsTr("Enter a seed phrase") @@ -179,10 +180,10 @@ Item { //TODO remove when sync code is implemented opacity: 1.0 } -// PropertyChanges { -// target: keycardLink -// text: qsTr("Generate keys for a new Keycard") -// } + PropertyChanges { + target: keycardLink + text: qsTr("Generate keys for a new Keycard") + } PropertyChanges { target: seedLink text: qsTr("Import a seed phrase") @@ -217,10 +218,10 @@ Item { //TODO remove when sync code is implemented opacity: 1.0 } -// PropertyChanges { -// target: keycardLink -// text: qsTr("Import a seed phrase into a new Keycard") -// } + PropertyChanges { + target: keycardLink + text: qsTr("Import a seed phrase into a new Keycard") + } PropertyChanges { target: seedLink text: "" diff --git a/ui/app/AppLayouts/Onboarding/views/LoginView.qml b/ui/app/AppLayouts/Onboarding/views/LoginView.qml index 7d5ee59dad..84325eac4e 100644 --- a/ui/app/AppLayouts/Onboarding/views/LoginView.qml +++ b/ui/app/AppLayouts/Onboarding/views/LoginView.qml @@ -7,7 +7,8 @@ import QtGraphicalEffects 1.13 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Components 0.1 -import StatusQ.Controls 0.1 as StatusQControls +import StatusQ.Controls 0.1 +import StatusQ.Controls.Validators 0.1 import StatusQ.Popups 0.1 import shared.panels 1.0 @@ -27,47 +28,120 @@ Item { property StartupStore startupStore - property bool loading: false - - function doLogin(password) { - if (loading || password.length === 0) - return - - loading = true - txtPassword.textField.clear() - root.startupStore.setPassword(password) - root.startupStore.doPrimaryAction() - } - - function resetLogin() { - if(localAccountSettings.storeToKeychainValue === Constants.storeToKeychainValueStore) - { - connection.enabled = true - } - else - { - txtPassword.visible = true - txtPassword.forceActiveFocus(Qt.MouseFocusReason) - } - } - Component.onCompleted: { - resetLogin() + d.resetLogin() + } + + onStateChanged: { + pinInputField.statesInitialization() + pinInputField.forceFocus() + } + + QtObject { + id: d + property bool loading: false + + readonly property string stateLoginRegularUser: "regularUserLogin" + readonly property string stateLoginKeycardUser: "keycardUserLogin" + + property int index: 0 + property variant images : [ + Style.svg("keycard/card0@2x"), + Style.svg("keycard/card1@2x"), + Style.svg("keycard/card2@2x"), + Style.svg("keycard/card3@2x") + ] + + property int remainingAttempts: parseInt(root.startupStore.startupModuleInst.keycardData, 10) + onRemainingAttemptsChanged: { + pinInputField.statesInitialization() + pinInputField.forceFocus() + } + + function doLogin(password) { + if (d.loading || password.length === 0) + return + + d.loading = true + txtPassword.textField.clear() + root.startupStore.setPassword(password) + root.startupStore.doPrimaryAction() + } + + function doKeycardLogin(pin) { + if (d.loading || pin.length === 0) + return + + d.loading = true + root.startupStore.setPin(pin) + root.startupStore.doPrimaryAction() + } + + function resetLogin() { + if(localAccountSettings.storeToKeychainValue !== Constants.keychain.storedValue.store) + { + if (!root.startupStore.selectedLoginAccount.keycardCreatedAccount){ + txtPassword.visible = true + txtPassword.forceActiveFocus(Qt.MouseFocusReason) + } + } + } + } + + Timer { + interval: 400 + running: root.state === d.stateLoginKeycardUser || + root.state === Constants.startupState.loginKeycardInsertKeycard || + root.state === Constants.startupState.loginKeycardReadingKeycard + repeat: true + onTriggered: { + d.index++ + image.source = d.images[d.index % d.images.length] + } } Connections{ - id: connection target: root.startupStore.startupModuleInst onObtainingPasswordError: { - enabled = false + if (root.startupStore.selectedLoginAccount.keycardCreatedAccount) { + root.startupStore.doPrimaryAction() // in this case, switch to enter pin state + } + + if (errorType === Constants.keychain.errorType.authentication) { + // We are notifying user only about keychain errors. + return + } + obtainingPasswordErrorNotification.confirmationText = errorDescription obtainingPasswordErrorNotification.open() } onObtainingPasswordSuccess: { - enabled = false - doLogin(password) + if(localAccountSettings.storeToKeychainValue !== Constants.keychain.storedValue.store) + return + + if (root.startupStore.selectedLoginAccount.keycardCreatedAccount) { + d.doKeycardLogin(password) + } + else { + d.doLogin(password) + } + } + + onAccountLoginError: { + if (error) { + if (!root.startupStore.selectedLoginAccount.keycardCreatedAccount) { + // SQLITE_NOTADB: "file is not a database" + if (error === "file is not a database") { + txtPassword.validationError = qsTr("Password incorrect") + } else { + txtPassword.validationError = qsTr("Login failed: %1").arg(error.toUpperCase()) + } + d.loading = false + txtPassword.textField.forceActiveFocus() + } + } } } @@ -85,229 +159,715 @@ Item { } } - Item { - id: element - width: 360 - height: childrenRect.height - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter + ColumnLayout { + anchors.centerIn: parent + spacing: Style.current.bigPadding Image { - id: statusIcon - width: 140 - height: 140 + id: image + Layout.alignment: Qt.AlignHCenter fillMode: Image.PreserveAspectFit - source: Style.png("status-logo") - anchors.horizontalCenter: parent.horizontalCenter + antialiasing: true + mipmap: true } StatusBaseText { - id: welcomeBackText - text: qsTr("Welcome back") + id: title + Layout.alignment: Qt.AlignHCenter font.weight: Font.Bold - font.pixelSize: 17 - anchors.top: statusIcon.bottom - anchors.topMargin: 10 - anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: Constants.keycard.general.fontSize1 color: Theme.palette.directColor1 } - ConfirmAddExistingKeyModal { - id: confirmAddExstingKeyModal - onOpenModalClicked: { - root.startupStore.doTertiaryAction() - } - } + Item { + id: userInfo + height: userImage.height + width: 318 + Layout.alignment: Qt.AlignHCenter - SelectAnotherAccountModal { - id: selectAnotherAccountModal - startupStore: root.startupStore - onAccountSelected: { - root.startupStore.setSelectedLoginAccountByIndex(index) - resetLogin() + UserImage { + id: userImage + image: root.startupStore.selectedLoginAccount.thumbnailImage + name: root.startupStore.selectedLoginAccount.username + colorId: root.startupStore.selectedLoginAccount.colorId + colorHash: root.startupStore.selectedLoginAccount.colorHash + anchors.left: parent.left } - onOpenModalClicked: { - root.startupStore.doTertiaryAction() + + StatusBaseText { + id: usernameText + text: root.startupStore.selectedLoginAccount.username + font.pixelSize: 17 + anchors.left: userImage.right + anchors.right: root.startupStore.selectedLoginAccount.keycardCreatedAccount? + keycardIcon.left : changeAccountBtn.left + anchors.leftMargin: Style.current.padding + anchors.rightMargin: Style.current.padding + anchors.verticalCenter: userImage.verticalCenter + color: Theme.palette.directColor1 + elide: Text.ElideRight + } + + StatusIcon { + id: keycardIcon + visible: root.startupStore.selectedLoginAccount.keycardCreatedAccount + anchors.right: changeAccountBtn.left + anchors.verticalCenter: userImage.verticalCenter + icon: "keycard" + height: Style.current.padding + color: Theme.palette.baseColor1 + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: changeAccountBtn.clicked(mouse) + } + + StatusFlatRoundButton { + id: changeAccountBtn + icon.name: "chevron-down" + type: StatusFlatRoundButton.Type.Tertiary + width: 24 + height: 24 + anchors.verticalCenter: usernameText.verticalCenter + anchors.right: parent.right + + onClicked: { + if (accountsPopup.opened) { + accountsPopup.close() + } else { + accountsPopup.popup(userInfo, 0, userInfo.height+4) + } + } + } + + StatusPopupMenu { + id: accountsPopup + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent + width: parent.width + dim: false + Repeater { + id: accounts + model: root.startupStore.startupModuleInst.loginAccountsModel + delegate: AccountMenuItemPanel { + label: model.username + image: model.thumbnailImage + colorId: model.colorId + colorHash: model.colorHash + keycardCreatedAccount: model.keycardCreatedAccount + onClicked: { + root.startupStore.setSelectedLoginAccountByIndex(index) + d.resetLogin() + accountsPopup.close() + } + } + } + + AccountMenuItemPanel { + label: qsTr("Add new user") + onClicked: { + accountsPopup.close() + root.startupStore.doSecondaryAction() + } + } + + AccountMenuItemPanel { + label: qsTr("Add existing Status user") + iconSettings.name: "wallet" + onClicked: { + accountsPopup.close() + root.startupStore.doTertiaryAction() + } + } } } Item { - id: userInfo - height: userImage.height - anchors.top: welcomeBackText.bottom - anchors.topMargin: 64 - width: 318 - anchors.horizontalCenter: parent.horizontalCenter + id: passwordSection + Layout.fillWidth: true + Layout.preferredHeight: txtPassword.height + Layout.alignment: Qt.AlignHCenter - UserImage { - id: userImage - image: root.startupStore.selectedLoginAccount.thumbnailImage - name: root.startupStore.selectedLoginAccount.username - colorId: root.startupStore.selectedLoginAccount.colorId - colorHash: root.startupStore.selectedLoginAccount.colorHash - anchors.left: parent.left - } - - StatusBaseText { - id: usernameText - text: root.startupStore.selectedLoginAccount.username - font.pixelSize: 17 - anchors.left: userImage.right - anchors.leftMargin: 16 - anchors.verticalCenter: userImage.verticalCenter - color: Theme.palette.directColor1 - } - - MouseArea { - property bool accountPopupWasOpened: false - - anchors.fill: parent - anchors.margins: -10 - cursorShape: Qt.PointingHandCursor - hoverEnabled: true - onEntered: { - // cache opened state, because it is always false in onClicked - // because of CloseOnPressOutsideParent policy of accountsPopup - accountPopupWasOpened = accountsPopup.opened - } - onClicked: { - if (!accountPopupWasOpened) { - changeAccountBtn.clicked(mouse) - } - accountPopupWasOpened = accountsPopup.opened - } - } - - StatusQControls.StatusFlatRoundButton { - icon.name: "chevron-down" - type: StatusQControls.StatusFlatRoundButton.Type.Tertiary - width: 24 - height: 24 - id: changeAccountBtn - objectName: "changeAccountBtn" - anchors.verticalCenter: usernameText.verticalCenter - anchors.right: parent.right - - - onClicked: { - if (accountsPopup.opened) { - accountsPopup.close() - } else { - accountsPopup.popup(width-userInfo.width-16, userInfo.height+4) - } - } - - StatusPopupMenu { - id: accountsPopup - closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent - width: 346 - dim: false - Repeater { - id: accounts - model: root.startupStore.startupModuleInst.loginAccountsModel - delegate: AccountMenuItemPanel { - label: model.username - image: model.thumbnailImage - colorId: model.colorId - colorHash: model.colorHash - onClicked: { - root.startupStore.setSelectedLoginAccountByIndex(index) - resetLogin() - accountsPopup.close() - } - } - } - - AccountMenuItemPanel { - label: qsTr("Add new user") - onClicked: { - accountsPopup.close() - root.startupStore.doSecondaryAction() - } - } - - AccountMenuItemPanel { - label: qsTr("Add existing Status user") - iconSettings.name: "wallet" - onClicked: { - accountsPopup.close() - root.startupStore.doTertiaryAction() - } - } - } - } - } - - Input { - id: txtPassword - width: 318 - height: 70 - anchors.top: userInfo.bottom - anchors.topMargin: Style.current.padding * 2 - anchors.left: undefined - anchors.right: undefined - anchors.horizontalCenter: parent.horizontalCenter - enabled: !loading - validationErrorAlignment: Text.AlignHCenter - validationErrorTopMargin: 10 - placeholderText: loading ? - qsTr("Connecting...") : - qsTr("Password") - textField.objectName: "loginPasswordInput" - textField.echoMode: TextInput.Password - Keys.onReturnPressed: { - doLogin(textField.text) + Input { + id: txtPassword + width: 318 + height: 70 + enabled: !d.loading + validationErrorAlignment: Text.AlignHCenter + validationErrorTopMargin: 10 + placeholderText: d.loading ? + qsTr("Connecting...") : + qsTr("Password") + textField.echoMode: TextInput.Password + Keys.onReturnPressed: { + d.doLogin(textField.text) + } + onTextEdited: { + validationError = ""; + d.loading = false + } } - onTextEdited: { - validationError = ""; - loading = false + + StatusRoundButton { + id: submitBtn + width: 40 + height: 40 + type: StatusRoundButton.Type.Secondary + icon.name: "arrow-right" + opacity: (d.loading || txtPassword.text.length > 0) ? 1 : 0 + anchors.left: txtPassword.right + anchors.leftMargin: (d.loading || txtPassword.text.length > 0) ? Style.current.padding : Style.current.smallPadding + state: d.loading ? "pending" : "default" + onClicked: { + d.doLogin(txtPassword.textField.text) + } + + // https://www.figma.com/file/BTS422M9AkvWjfRrXED3WC/%F0%9F%91%8B-Onboarding%E2%8E%9CDesktop?node-id=6%3A0 + Behavior on opacity { + OpacityAnimator { + from: 0.5 + duration: 200 + } + } + Behavior on anchors.leftMargin { + NumberAnimation { + duration: 200 + } + } } } - StatusQControls.StatusRoundButton { - id: submitBtn - width: 40 - height: 40 - type: StatusQControls.StatusRoundButton.Type.Secondary - icon.name: "arrow-right" - opacity: (loading || txtPassword.text.length > 0) ? 1 : 0 - anchors.top: userInfo.bottom - anchors.topMargin: 34 - anchors.left: txtPassword.right - anchors.leftMargin: (loading || txtPassword.text.length > 0) ? Style.current.padding : Style.current.smallPadding - state: loading ? "pending" : "default" + Column { + id: pinSection + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + spacing: Style.current.padding + + StatusBaseText { + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: Constants.keycard.general.fontSize2 + wrapMode: Text.WordWrap + text: qsTr("Enter Keycard PIN") + } + + StatusPinInput { + id: pinInputField + anchors.horizontalCenter: parent.horizontalCenter + validator: StatusIntValidator{bottom: 0; top: 999999;} + pinLen: Constants.keycard.general.keycardPinLength + enabled: !d.loading + + onPinInputChanged: { + if(pinInput.length == 0) + return + root.startupStore.setPin(pinInput) + root.startupStore.doPrimaryAction() + } + } + } + + StatusBaseText { + id: info + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Constants.keycard.general.fontSize3 + wrapMode: Text.WordWrap + } + + StatusBaseText { + id: message + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Constants.keycard.general.fontSize3 + wrapMode: Text.WordWrap + } + + StatusButton { + id: button + Layout.alignment: Qt.AlignHCenter + focus: true onClicked: { - doLogin(txtPassword.textField.text) - } - - // https://www.figma.com/file/BTS422M9AkvWjfRrXED3WC/%F0%9F%91%8B-Onboarding%E2%8E%9CDesktop?node-id=6%3A0 - Behavior on opacity { - OpacityAnimator { - from: 0.5 - duration: 200 - } - } - Behavior on anchors.leftMargin { - NumberAnimation { - duration: 200 - } + root.startupStore.doPrimaryAction() } } - Connections { - target: root.startupStore.startupModuleInst - onAccountLoginError: { - if (error) { - // SQLITE_NOTADB: "file is not a database" - if (error === "file is not a database") { - txtPassword.validationError = qsTr("Password incorrect") - } else { - txtPassword.validationError = qsTr("Login failed: %1").arg(error.toUpperCase()) - } - loading = false - txtPassword.textField.forceActiveFocus() + StatusBaseText { + id: link + Layout.alignment: Qt.AlignHCenter + color: Theme.palette.primaryColor1 + font.pixelSize: Constants.keycard.general.fontSize2 + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: { + parent.font.underline = true + } + onExited: { + parent.font.underline = false + } + onClicked: { + root.startupStore.doPrimaryAction() } } } } + + states: [ + State { + name: d.stateLoginRegularUser + when: !root.startupStore.selectedLoginAccount.keycardCreatedAccount + PropertyChanges { + target: image + source: Style.png("status-logo") + Layout.preferredHeight: 140 + Layout.preferredWidth: 140 + } + PropertyChanges { + target: title + text: qsTr("Welcome back") + } + PropertyChanges { + target: passwordSection + visible: true + } + PropertyChanges { + target: pinSection + visible: false + } + PropertyChanges { + target: info + text: "" + visible: false + } + PropertyChanges { + target: message + text: "" + visible: false + } + PropertyChanges { + target: button + text: "" + visible: false + } + PropertyChanges { + target: link + text: "" + visible: false + } + }, + State { + name: d.stateLoginKeycardUser + when: root.startupStore.selectedLoginAccount.keycardCreatedAccount && + root.startupStore.currentStartupState.stateType === Constants.startupState.login + PropertyChanges { + target: image + source: Style.svg("keycard/card3@2x") + Layout.preferredHeight: sourceSize.height + Layout.preferredWidth: sourceSize.width + } + PropertyChanges { + target: title + text: "" + visible: false + } + PropertyChanges { + target: passwordSection + visible: false + } + PropertyChanges { + target: pinSection + visible: false + } + PropertyChanges { + target: info + text: qsTr("Plug in Keycard reader...") + visible: true + font.pixelSize: Constants.keycard.general.fontSize2 + color: Theme.palette.baseColor1 + } + PropertyChanges { + target: message + text: "" + visible: false + } + PropertyChanges { + target: button + text: "" + visible: false + } + PropertyChanges { + target: link + text: "" + visible: false + } + }, + State { + name: Constants.startupState.loginKeycardInsertKeycard + when: root.startupStore.currentStartupState.stateType === Constants.startupState.loginKeycardInsertKeycard + PropertyChanges { + target: image + source: Style.svg("keycard/card3@2x") + Layout.preferredHeight: sourceSize.height + Layout.preferredWidth: sourceSize.width + } + PropertyChanges { + target: title + text: "" + visible: false + } + PropertyChanges { + target: passwordSection + visible: false + } + PropertyChanges { + target: pinSection + visible: false + } + PropertyChanges { + target: info + text: qsTr("Insert your Keycard...") + visible: true + font.pixelSize: Constants.keycard.general.fontSize2 + color: Theme.palette.baseColor1 + } + PropertyChanges { + target: message + text: "" + visible: false + } + PropertyChanges { + target: button + text: "" + visible: false + } + PropertyChanges { + target: link + text: "" + visible: false + } + }, + State { + name: Constants.startupState.loginKeycardReadingKeycard + when: root.startupStore.currentStartupState.stateType === Constants.startupState.loginKeycardReadingKeycard + PropertyChanges { + target: image + source: Style.svg("keycard/card3@2x") + Layout.preferredHeight: sourceSize.height + Layout.preferredWidth: sourceSize.width + } + PropertyChanges { + target: title + text: "" + visible: false + } + PropertyChanges { + target: passwordSection + visible: false + } + PropertyChanges { + target: pinSection + visible: false + } + PropertyChanges { + target: info + text: qsTr("Reading Keycard...") + visible: true + font.pixelSize: Constants.keycard.general.fontSize2 + color: Theme.palette.baseColor1 + } + PropertyChanges { + target: message + text: "" + visible: false + } + PropertyChanges { + target: button + text: "" + visible: false + } + PropertyChanges { + target: link + text: "" + visible: false + } + }, + State { + name: Constants.startupState.loginKeycardEnterPin + when: root.startupStore.currentStartupState.stateType === Constants.startupState.loginKeycardEnterPin + PropertyChanges { + target: image + source: Style.svg("keycard/card3@2x") + Layout.preferredHeight: sourceSize.height + Layout.preferredWidth: sourceSize.width + } + PropertyChanges { + target: title + text: "" + visible: false + } + PropertyChanges { + target: passwordSection + visible: false + } + PropertyChanges { + target: pinSection + visible: true + } + PropertyChanges { + target: info + text: "" + visible: false + font.pixelSize: Constants.keycard.general.fontSize2 + color: Theme.palette.baseColor1 + } + PropertyChanges { + target: message + text: "" + visible: false + } + PropertyChanges { + target: button + text: "" + visible: false + } + PropertyChanges { + target: link + text: "" + visible: false + } + }, + State { + name: Constants.startupState.loginKeycardWrongKeycard + when: root.startupStore.currentStartupState.stateType === Constants.startupState.loginKeycardWrongKeycard + PropertyChanges { + target: image + source: Style.svg("keycard/card-wrong3@2x") + Layout.preferredHeight: sourceSize.height + Layout.preferredWidth: sourceSize.width + } + PropertyChanges { + target: title + text: "" + visible: false + } + PropertyChanges { + target: passwordSection + visible: false + } + PropertyChanges { + target: pinSection + visible: false + } + PropertyChanges { + target: info + text: qsTr("Wrong Keycard!\nThe card inserted is not linked to your profile.") + visible: true + font.pixelSize: Constants.keycard.general.fontSize2 + color: Theme.palette.dangerColor1 + } + PropertyChanges { + target: message + text: qsTr("Insert another Keycard") + visible: true + font.pixelSize: Constants.keycard.general.fontSize2 + color: Theme.palette.baseColor1 + } + PropertyChanges { + target: button + text: "" + visible: false + } + PropertyChanges { + target: link + text: "" + visible: false + } + }, + State { + name: Constants.startupState.loginKeycardWrongPin + when: root.startupStore.currentStartupState.stateType === Constants.startupState.loginKeycardWrongPin + PropertyChanges { + target: image + source: Style.svg("keycard/card-wrong3@2x") + Layout.preferredHeight: sourceSize.height + Layout.preferredWidth: sourceSize.width + } + PropertyChanges { + target: title + text: "" + visible: false + } + PropertyChanges { + target: passwordSection + visible: false + } + PropertyChanges { + target: pinSection + visible: true + } + PropertyChanges { + target: info + text: qsTr("PIN incorrect") + visible: true + font.pixelSize: Constants.keycard.general.fontSize2 + color: Theme.palette.dangerColor1 + } + PropertyChanges { + target: message + visible: true + font.pixelSize: Constants.keycard.general.fontSize2 + text: qsTr("%n attempt(s) remaining", "", d.remainingAttempts) + color: d.remainingAttempts === 1? + Theme.palette.dangerColor1 : + Theme.palette.baseColor1 + } + PropertyChanges { + target: button + text: "" + visible: false + } + PropertyChanges { + target: link + text: "" + visible: false + } + }, + State { + name: Constants.startupState.loginKeycardMaxPinRetriesReached + when: root.startupStore.currentStartupState.stateType === Constants.startupState.loginKeycardMaxPinRetriesReached + PropertyChanges { + target: image + source: Style.svg("keycard/card-error3@2x") + Layout.preferredHeight: sourceSize.height + Layout.preferredWidth: sourceSize.width + } + PropertyChanges { + target: title + text: "" + visible: false + } + PropertyChanges { + target: passwordSection + visible: false + } + PropertyChanges { + target: pinSection + visible: false + } + PropertyChanges { + target: info + text: qsTr("Keycard locked") + visible: true + font.pixelSize: Constants.keycard.general.fontSize2 + color: Theme.palette.dangerColor1 + } + PropertyChanges { + target: message + text: "" + visible: false + } + PropertyChanges { + target: button + text: qsTr("Recover your Keycard") + visible: true + } + PropertyChanges { + target: link + text: "" + visible: false + } + }, + State { + name: Constants.startupState.loginKeycardMaxPukRetriesReached + when: root.startupStore.currentStartupState.stateType === Constants.startupState.loginKeycardMaxPukRetriesReached + PropertyChanges { + target: image + source: Style.svg("keycard/card-error3@2x") + Layout.preferredHeight: sourceSize.height + Layout.preferredWidth: sourceSize.width + } + PropertyChanges { + target: title + text: "" + visible: false + } + PropertyChanges { + target: passwordSection + visible: false + } + PropertyChanges { + target: pinSection + visible: false + } + PropertyChanges { + target: info + text: qsTr("Keycard locked") + visible: true + font.pixelSize: Constants.keycard.general.fontSize2 + color: Theme.palette.dangerColor1 + } + PropertyChanges { + target: message + text: "" + visible: false + } + PropertyChanges { + target: button + text: qsTr("Recover with seed phrase") + visible: true + } + PropertyChanges { + target: link + text: "" + visible: false + } + }, + State { + name: Constants.startupState.loginKeycardEmpty + when: root.startupStore.currentStartupState.stateType === Constants.startupState.loginKeycardEmpty + PropertyChanges { + target: image + source: Style.svg("keycard/card-wrong3@2x") + Layout.preferredHeight: sourceSize.height + Layout.preferredWidth: sourceSize.width + } + PropertyChanges { + target: title + text: "" + visible: false + } + PropertyChanges { + target: passwordSection + visible: false + } + PropertyChanges { + target: pinSection + visible: false + } + PropertyChanges { + target: info + text: qsTr("The card inserted is empty") + visible: true + font.pixelSize: Constants.keycard.general.fontSize2 + color: Theme.palette.dangerColor1 + } + PropertyChanges { + target: message + text: "" + visible: false + } + PropertyChanges { + target: button + text: "" + visible: false + } + PropertyChanges { + target: link + text: qsTr("Generate keys for a new Keycard") + visible: true + } + } + ] } diff --git a/ui/app/AppLayouts/Onboarding/views/SeedPhraseInputView.qml b/ui/app/AppLayouts/Onboarding/views/SeedPhraseInputView.qml index 3bc634b07a..3d71389366 100644 --- a/ui/app/AppLayouts/Onboarding/views/SeedPhraseInputView.qml +++ b/ui/app/AppLayouts/Onboarding/views/SeedPhraseInputView.qml @@ -277,9 +277,22 @@ Item { function checkMnemonicLength() { submitButton.enabled = (root.mnemonicInput.length === root.tabs[switchTabBar.currentIndex]) } - text: root.startupStore.currentStartupState.flowType === Constants.startupFlow.firstRunNewUserImportSeedPhrase? - qsTr("Import") : - qsTr("Restore Status Profile") + text: { + if (root.startupStore.currentStartupState.flowType === Constants.startupFlow.firstRunNewUserImportSeedPhrase) { + return qsTr("Import") + } + else if (root.startupStore.currentStartupState.flowType === Constants.startupFlow.firstRunOldUserImportSeedPhrase) { + return qsTr("Restore Status Profile") + } + else if (root.startupStore.currentStartupState.flowType === Constants.startupFlow.firstRunOldUserKeycardImport || + root.startupStore.currentStartupState.flowType === Constants.startupFlow.appLogin) { + return qsTr("Recover Keycard") + } + else if (root.startupStore.currentStartupState.flowType === Constants.startupFlow.firstRunNewUserImportSeedPhraseIntoKeycard) { + return qsTr("Next") + } + return "" + } onClicked: { let mnemonicString = ""; var sortTable = mnemonicInput.sort(function (a, b) { diff --git a/ui/app/AppLayouts/Onboarding/views/SeedPhraseView.qml b/ui/app/AppLayouts/Onboarding/views/SeedPhraseView.qml new file mode 100644 index 0000000000..82d948cb7e --- /dev/null +++ b/ui/app/AppLayouts/Onboarding/views/SeedPhraseView.qml @@ -0,0 +1,128 @@ +import QtQuick 2.13 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.13 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Controls 0.1 + +import utils 1.0 + +import "../stores" + +Item { + id: root + + property StartupStore startupStore + + Component.onCompleted: { + let seedPhrase = root.startupStore.getSeedPhrase() + if(seedPhrase.length === 0) + return + + d.seedPhraseModel = seedPhrase.split(" ") + } + + QtObject { + id: d + + property var seedPhraseModel: [] + readonly property int numOfColumns: 4 + readonly property int rowSpacing: Style.current.bigPadding + readonly property int columnSpacing: Style.current.bigPadding + } + + Keys.onPressed: { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + event.accepted = true + root.startupStore.doPrimaryAction() + } + } + + Item { + anchors.top: parent.top + anchors.bottom: footerWrapper.top + anchors.left: parent.left + anchors.right: parent.right + + ColumnLayout { + anchors.centerIn: parent + spacing: Style.current.padding + + StatusBaseText { + id: title + Layout.alignment: Qt.AlignHCenter + font.pixelSize: Constants.keycard.general.fontSize1 + font.weight: Font.Bold + color: Theme.palette.directColor1 + text: qsTr("Write down your seed phrase") + } + + StatusBaseText { + id: info + Layout.alignment: Qt.AlignHCenter + font.pixelSize: Constants.keycard.general.fontSize3 + color: Theme.palette.dangerColor1 + horizontalAlignment: Qt.AlignHCenter + text: qsTr("You will need this to recover your Keycard if you loose\nyour PIN of if the wrong PIN is entered five times in a row.") + } + + GridLayout { + id: grid + Layout.alignment: Qt.AlignHCenter + columns: d.numOfColumns + rowSpacing: d.rowSpacing + columnSpacing: d.columnSpacing + + Repeater { + model: d.seedPhraseModel + delegate: Item { + Layout.preferredWidth: Constants.keycard.general.seedPhraseCellWidth + Layout.preferredHeight: Constants.keycard.general.seedPhraseCellHeight + StatusBaseText { + id: wordNumber + width: Constants.keycard.general.seedPhraseCellNumberWidth + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Qt.AlignRight + font.pixelSize: Constants.keycard.general.seedPhraseCellFontSize + color: Theme.palette.directColor1 + text: "%1.".arg(model.index + 1) + } + + StatusBaseText { + id: word + anchors.left: wordNumber.right + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Style.current.xlPadding + horizontalAlignment: Qt.AlignLeft + font.pixelSize: Constants.keycard.general.seedPhraseCellFontSize + color: Theme.palette.directColor1 + text: model.modelData + } + } + } + } + } + } + + Item { + id: footerWrapper + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + height: Constants.keycard.general.footerWrapperHeight + + StatusButton { + anchors.top: parent.top + anchors.topMargin: Style.current.padding + anchors.horizontalCenter: parent.horizontalCenter + text: qsTr("Next") + focus: true + onClicked: { + root.startupStore.doPrimaryAction() + } + } + } +} diff --git a/ui/app/AppLayouts/Onboarding/views/SeedPhraseWordsInputView.qml b/ui/app/AppLayouts/Onboarding/views/SeedPhraseWordsInputView.qml new file mode 100644 index 0000000000..40c682527e --- /dev/null +++ b/ui/app/AppLayouts/Onboarding/views/SeedPhraseWordsInputView.qml @@ -0,0 +1,187 @@ +import QtQuick 2.13 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.13 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Controls 0.1 + +import utils 1.0 + +import "../stores" + +Item { + id: root + + property StartupStore startupStore + + Component.onCompleted: { + d.dirtyWordAtIndex = [false, false, false, false] + d.validWordAtIndex = [false, false, false, false] + d.anyInputDirty = false + d.allEntriesValid = false + + let seedPhrase = root.startupStore.getSeedPhrase() + if(seedPhrase.length === 0) + return + + d.seedPhrases = seedPhrase.split(" ") + + let numbers = [] + while (numbers.length < 4) { + let randomNo = Math.floor(Math.random() * 12) + if(numbers.indexOf(randomNo) == -1) + numbers.push(randomNo) + } + numbers.sort((a, b) => { return a < b? -1 : a > b? 1 : 0 }) + d.wordNumbers = numbers + } + + QtObject { + id: d + + property var seedPhrases: [] + property var wordNumbers: [] + property var dirtyWordAtIndex: [false, false, false, false] + property var validWordAtIndex: [false, false, false, false] + property bool anyInputDirty: false + property bool allEntriesValid: false + readonly property int numOfColumns: 4 + readonly property int rowSpacing: Style.current.bigPadding + readonly property int columnSpacing: Style.current.bigPadding + + function updateValidity(index, valid, dirty) { + dirtyWordAtIndex[index] = dirty + validWordAtIndex[index] = valid + let anyDirty = false + let allValid = true + for(let i = 0; i < validWordAtIndex.length; ++i) { + allValid = allValid && validWordAtIndex[i] + anyDirty = anyDirty || (dirtyWordAtIndex[index] && !validWordAtIndex[index]) + } + allEntriesValid = allValid + anyInputDirty = anyDirty + } + } + + Item { + anchors.top: parent.top + anchors.bottom: footerWrapper.top + anchors.left: parent.left + anchors.right: parent.right + + ColumnLayout { + anchors.centerIn: parent + spacing: Style.current.padding + + StatusBaseText { + id: title + Layout.alignment: Qt.AlignHCenter + font.pixelSize: Constants.keycard.general.fontSize1 + font.weight: Font.Bold + color: Theme.palette.directColor1 + text: qsTr("Enter seed phrase words") + } + + GridLayout { + id: grid + Layout.alignment: Qt.AlignHCenter + columns: d.numOfColumns + rowSpacing: d.rowSpacing + columnSpacing: d.columnSpacing + + Component.onCompleted: { + for (var i = 0; i < children.length - 1; ++i) { + if(children[i].inputField && children[i+1].inputField){ + children[i].inputField.input.tabNavItem = children[i+1].inputField.input.edit + } + } + } + + Repeater { + model: d.wordNumbers + delegate: Item { + Layout.preferredWidth: Constants.keycard.general.seedPhraseCellWidth + Layout.preferredHeight: Constants.keycard.general.seedPhraseCellHeight + + property alias inputField: word + property alias wN: wordNumber + + StatusBaseText { + id: wordNumber + width: Constants.keycard.general.seedPhraseCellNumberWidth + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Qt.AlignRight + font.pixelSize: Constants.keycard.general.seedPhraseCellFontSize + color: Theme.palette.directColor1 + text: "%1.".arg(model.modelData + 1) + } + + StatusInput { + id: word + anchors.left: wordNumber.right + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Style.current.xlPadding + input.edit.font.pixelSize: Constants.keycard.general.seedPhraseCellFontSize + input.acceptReturn: true + + onTextChanged: { + if(text.length == 0) + return + if(/(^\s|^\r|^\n)|(\s$|^\r$|^\n$)/.test(text)) { + text = text.trim() + return + } + else if(/\s|\r|\n/.test(text)) { + text = "" + return + } + valid = d.seedPhrases[model.modelData] === text + d.updateValidity(index, valid, text !== "") + } + + onKeyPressed: { + if (d.allEntriesValid && + (input.edit.keyEvent === Qt.Key_Return || + input.edit.keyEvent === Qt.Key_Enter)) { + event.accepted = true + root.startupStore.doPrimaryAction() + } + } + } + } + } + } + + StatusBaseText { + id: info + Layout.alignment: Qt.AlignHCenter + font.pixelSize: Constants.keycard.general.fontSize3 + color: Theme.palette.dangerColor1 + horizontalAlignment: Qt.AlignHCenter + text: d.anyInputDirty && !d.allEntriesValid? qsTr("Invalid word") : "" + } + } + } + + Item { + id: footerWrapper + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + height: Constants.keycard.general.footerWrapperHeight + + StatusButton { + anchors.top: parent.top + anchors.topMargin: Style.current.padding + anchors.horizontalCenter: parent.horizontalCenter + enabled: d.allEntriesValid + text: qsTr("Next") + onClicked: { + root.startupStore.doPrimaryAction() + } + } + } +} diff --git a/ui/app/AppLayouts/Onboarding/views/TouchIDAuthView.qml b/ui/app/AppLayouts/Onboarding/views/TouchIDAuthView.qml index 52fcb7b4f8..dc04037e3b 100644 --- a/ui/app/AppLayouts/Onboarding/views/TouchIDAuthView.qml +++ b/ui/app/AppLayouts/Onboarding/views/TouchIDAuthView.qml @@ -62,7 +62,11 @@ Item { anchors.top: txtTitle.bottom anchors.topMargin: Style.current.padding color: Style.current.secondaryText - text: qsTr("Would you like to use Touch ID\nto login to Status?") + text: root.startupStore.currentStartupState.flowType === Constants.startupFlow.firstRunNewUserNewKeycardKeys || + root.startupStore.currentStartupState.flowType === Constants.startupFlow.firstRunNewUserImportSeedPhraseIntoKeycard || + root.startupStore.currentStartupState.flowType === Constants.startupFlow.firstRunOldUserKeycardImport? + qsTr("Would you like to use TouchID instead of a PIN code\nto login to Status using your Keycard?") : + qsTr("Would you like to use Touch ID\nto login to Status?") horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap font.pixelSize: 15 @@ -86,7 +90,11 @@ Item { objectName: "touchIdIPreferToUseMyPasswordText" Layout.alignment: Qt.AlignHCenter color: Theme.palette.primaryColor1 - text: qsTr("I prefer to use my password") + text: root.startupStore.currentStartupState.flowType === Constants.startupFlow.firstRunNewUserNewKeycardKeys || + root.startupStore.currentStartupState.flowType === Constants.startupFlow.firstRunNewUserImportSeedPhraseIntoKeycard || + root.startupStore.currentStartupState.flowType === Constants.startupFlow.firstRunOldUserKeycardImport? + qsTr("I prefer to use my PIN") : + qsTr("I prefer to use my password") font.pixelSize: 15 MouseArea { anchors.fill: parent diff --git a/ui/app/AppLayouts/Profile/popups/ChangePasswordModal.qml b/ui/app/AppLayouts/Profile/popups/ChangePasswordModal.qml index 0149b61b34..ce8451da56 100644 --- a/ui/app/AppLayouts/Profile/popups/ChangePasswordModal.qml +++ b/ui/app/AppLayouts/Profile/popups/ChangePasswordModal.qml @@ -25,7 +25,7 @@ StatusModal { function onChangePasswordResponse(success, errorMsg) { if (success) { if (Qt.platform.os === "osx") { - localAccountSettings.storeToKeychainValue = Constants.storeToKeychainValueStore; + localAccountSettings.storeToKeychainValue = Constants.keychain.storedValue.store; root.privacyStore.storeToKeyChain(d.passwordProcessing); } passwordChanged() diff --git a/ui/app/AppLayouts/Profile/stores/AdvancedStore.qml b/ui/app/AppLayouts/Profile/stores/AdvancedStore.qml index 63d986082a..3f9abe64c4 100644 --- a/ui/app/AppLayouts/Profile/stores/AdvancedStore.qml +++ b/ui/app/AppLayouts/Profile/stores/AdvancedStore.qml @@ -31,7 +31,6 @@ QtObject { readonly property string nodeManagement: "nodeManagement" readonly property string onlineUsers: "onlineUsers" readonly property string gifWidget: "gifWidget" - readonly property string keycard: "keycard" readonly property string communityHistoryArchiveSupport: "communityHistoryArchiveSupport" readonly property string communitiesPortal: "communitiesPortal" } @@ -131,9 +130,5 @@ QtObject { else if (feature === experimentalFeatures.gifWidget) { localAccountSensitiveSettings.isGifWidgetEnabled = !localAccountSensitiveSettings.isGifWidgetEnabled } - else if (feature === experimentalFeatures.keycard) { - localAccountSettings.isKeycardEnabled = !localAccountSettings.isKeycardEnabled - } - } } diff --git a/ui/app/AppLayouts/Profile/views/AdvancedView.qml b/ui/app/AppLayouts/Profile/views/AdvancedView.qml index b7f92f3901..aad789efca 100644 --- a/ui/app/AppLayouts/Profile/views/AdvancedView.qml +++ b/ui/app/AppLayouts/Profile/views/AdvancedView.qml @@ -163,18 +163,6 @@ SettingsContentBase { } } - // TODO: replace with StatusQ component - StatusSettingsLineButton { - anchors.leftMargin: 0 - anchors.rightMargin: 0 - text: qsTr("Keycard") - isSwitch: true - switchChecked: localAccountSettings.isKeycardEnabled - onClicked: { - root.advancedStore.toggleExperimentalFeature(root.advancedStore.experimentalFeatures.keycard) - } - } - StatusSectionHeadline { anchors.left: parent.left anchors.right: parent.right diff --git a/ui/app/AppLayouts/Profile/views/profile/MyProfileSettingsView.qml b/ui/app/AppLayouts/Profile/views/profile/MyProfileSettingsView.qml index 826ea20930..fe8cade5ef 100644 --- a/ui/app/AppLayouts/Profile/views/profile/MyProfileSettingsView.qml +++ b/ui/app/AppLayouts/Profile/views/profile/MyProfileSettingsView.qml @@ -41,7 +41,7 @@ ColumnLayout { if (biometricsSwitch.checked) Global.openPopup(storePasswordModal) else - localAccountSettings.storeToKeychainValue = Constants.storeToKeychainValueNever; + localAccountSettings.storeToKeychainValue = Constants.keychain.storedValue.never; } function offerToStorePassword(password, runStoreToKeyChainPopup) @@ -49,7 +49,7 @@ ColumnLayout { if (Qt.platform.os !== "osx") return; - localAccountSettings.storeToKeychainValue = Constants.storeToKeychainValueStore; + localAccountSettings.storeToKeychainValue = Constants.keychain.storedValue.store; root.privacyStore.storeToKeyChain(password); } @@ -89,7 +89,7 @@ ColumnLayout { components: [ StatusSwitch { id: biometricsSwitch horizontalPadding: 0 - readonly property bool currentStoredValue: localAccountSettings.storeToKeychainValue === Constants.storeToKeychainValueStore + readonly property bool currentStoredValue: localAccountSettings.storeToKeychainValue === Constants.keychain.storedValue.store checked: currentStoredValue } ] sensor.onClicked: biometricsSwitch.toggle() diff --git a/ui/imports/assets/icons/keycard/card-error3@2x.svg b/ui/imports/assets/icons/keycard/card-error3@2x.svg new file mode 100644 index 0000000000..dfb29db956 --- /dev/null +++ b/ui/imports/assets/icons/keycard/card-error3@2x.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/imports/assets/icons/keycard/card-error3@3x.svg b/ui/imports/assets/icons/keycard/card-error3@3x.svg new file mode 100644 index 0000000000..dfb29db956 --- /dev/null +++ b/ui/imports/assets/icons/keycard/card-error3@3x.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/imports/assets/icons/keycard/card-success3@2x.svg b/ui/imports/assets/icons/keycard/card-success3@2x.svg new file mode 100644 index 0000000000..5d4484e3b8 --- /dev/null +++ b/ui/imports/assets/icons/keycard/card-success3@2x.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/imports/assets/icons/keycard/card-success3@3x.svg b/ui/imports/assets/icons/keycard/card-success3@3x.svg new file mode 100644 index 0000000000..5d4484e3b8 --- /dev/null +++ b/ui/imports/assets/icons/keycard/card-success3@3x.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/imports/assets/icons/keycard/card-wrong3@2x.svg b/ui/imports/assets/icons/keycard/card-wrong3@2x.svg new file mode 100644 index 0000000000..2269d568f1 --- /dev/null +++ b/ui/imports/assets/icons/keycard/card-wrong3@2x.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/imports/assets/icons/keycard/card-wrong3@3x.svg b/ui/imports/assets/icons/keycard/card-wrong3@3x.svg new file mode 100644 index 0000000000..2269d568f1 --- /dev/null +++ b/ui/imports/assets/icons/keycard/card-wrong3@3x.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/imports/assets/icons/keycard/card0@2x.svg b/ui/imports/assets/icons/keycard/card0@2x.svg new file mode 100644 index 0000000000..d438d7e086 --- /dev/null +++ b/ui/imports/assets/icons/keycard/card0@2x.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/ui/imports/assets/icons/keycard/card0@3x.svg b/ui/imports/assets/icons/keycard/card0@3x.svg new file mode 100644 index 0000000000..d438d7e086 --- /dev/null +++ b/ui/imports/assets/icons/keycard/card0@3x.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/ui/imports/assets/icons/keycard/card1@2x.svg b/ui/imports/assets/icons/keycard/card1@2x.svg new file mode 100644 index 0000000000..cfeddfad44 --- /dev/null +++ b/ui/imports/assets/icons/keycard/card1@2x.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/imports/assets/icons/keycard/card1@3x.svg b/ui/imports/assets/icons/keycard/card1@3x.svg new file mode 100644 index 0000000000..cfeddfad44 --- /dev/null +++ b/ui/imports/assets/icons/keycard/card1@3x.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/imports/assets/icons/keycard/card2@2x.svg b/ui/imports/assets/icons/keycard/card2@2x.svg new file mode 100644 index 0000000000..2b0560b56c --- /dev/null +++ b/ui/imports/assets/icons/keycard/card2@2x.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/imports/assets/icons/keycard/card2@3x.svg b/ui/imports/assets/icons/keycard/card2@3x.svg new file mode 100644 index 0000000000..2b0560b56c --- /dev/null +++ b/ui/imports/assets/icons/keycard/card2@3x.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/imports/assets/icons/keycard/card3@2x.svg b/ui/imports/assets/icons/keycard/card3@2x.svg new file mode 100644 index 0000000000..304e330a3f --- /dev/null +++ b/ui/imports/assets/icons/keycard/card3@2x.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/imports/assets/icons/keycard/card3@3x.svg b/ui/imports/assets/icons/keycard/card3@3x.svg new file mode 100644 index 0000000000..304e330a3f --- /dev/null +++ b/ui/imports/assets/icons/keycard/card3@3x.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/imports/shared/keycard/InsertCard.qml b/ui/imports/shared/keycard/InsertCard.qml deleted file mode 100644 index 7b727b61b3..0000000000 --- a/ui/imports/shared/keycard/InsertCard.qml +++ /dev/null @@ -1,72 +0,0 @@ -import QtQuick 2.13 -import QtQuick.Controls 2.13 -import QtQuick.Layouts 1.13 - -import StatusQ.Core 0.1 -import StatusQ.Core.Theme 0.1 -import StatusQ.Controls 0.1 -import StatusQ.Popups 0.1 - -StatusModal { - property var onCancel: function() {} - - id: insertCard - anchors.centerIn: parent - - showHeader: false - focus: visible - - contentItem: Item { - width: insertCard.width - implicitHeight: childrenRect.height - Column { - width: parent.width - 32 - anchors.horizontalCenter: parent.horizontalCenter - - Item { - width: parent.width - height: 16 - } - - StatusBaseText { - text: qsTr("Please insert your Keycard to proceed or press the cancel button to cancel the operation") - font.pixelSize: 15 - anchors.left: parent.left - anchors.right: parent.right - wrapMode: Text.WordWrap - color: Theme.palette.directColor1 - } - - Item { - width: parent.width - height: 16 - } - } - } - - rightButtons: [ - StatusButton { - id: cancelButton - text: qsTr("Cancel") - onClicked: { - insertCard.close() - onCancel() - } - } - ] - - // Not Refactored Yet -// Connections { -// id: connection -// target: keycardModel -// ignoreUnknownSignals: true - -// onCardConnected: { -// insertCard.close() -// } - -// onCardDisconnected: { -// insertCard.open() -// } -// } -} diff --git a/ui/imports/shared/keycard/PINModal.qml b/ui/imports/shared/keycard/PINModal.qml deleted file mode 100644 index 68d268489d..0000000000 --- a/ui/imports/shared/keycard/PINModal.qml +++ /dev/null @@ -1,73 +0,0 @@ -import QtQuick 2.13 -import QtQuick.Controls 2.13 -import QtQuick.Layouts 1.13 -import StatusQ.Core 0.1 -import StatusQ.Core.Theme 0.1 -import StatusQ.Controls 0.1 -import StatusQ.Popups 0.1 - -import shared 1.0 -import shared.controls 1.0 -import shared.stores 1.0 -import utils 1.0 as Imports - - -StatusModal { - property bool pinFieldValid: false - property bool submitted: false - - id: popup - header.title: qsTr("Authenticate PIN") - anchors.centerIn: parent - height: 400 - - onOpened: { - submitted = false - pinField.text = ""; - pinField.forceActiveFocus(Qt.MouseFocusReason) - } - - contentItem: Item { - Input { - id: pinField - anchors.rightMargin: 56 - anchors.leftMargin: 56 - anchors.top: parent.top - anchors.topMargin: 88 - placeholderText: qsTr("PIN") - textField.echoMode: TextInput.Password - onTextChanged: { - [pinFieldValid, _] = - Imports.Utils.validatePINs("first", pinField, pinField); - } - } - - StatusBaseText { - text: qsTr("Insert your 6-digit PIN") - wrapMode: Text.WordWrap - anchors.right: parent.right - anchors.left: parent.left - anchors.bottom: parent.bottom - anchors.bottomMargin: 20 - horizontalAlignment: Text.AlignHCenter - color: Theme.palette.directColor1 - font.pixelSize: 12 - } - } - - rightButtons: [ - StatusButton { - id: submitBtn - text: qsTr("Authenticate") - enabled: pinFieldValid - - onClicked: { - submitted = true - // Not Refactored Yet - RootStore.keycardModelInst.authenticate(pinField.text) - popup.close() - } - } - ] - -} diff --git a/ui/imports/shared/keycard/PairingModal.qml b/ui/imports/shared/keycard/PairingModal.qml deleted file mode 100644 index 427749893c..0000000000 --- a/ui/imports/shared/keycard/PairingModal.qml +++ /dev/null @@ -1,68 +0,0 @@ -import QtQuick 2.13 -import QtQuick.Controls 2.13 -import QtQuick.Layouts 1.13 -import StatusQ.Core 0.1 -import StatusQ.Core.Theme 0.1 -import StatusQ.Controls 0.1 -import StatusQ.Popups 0.1 -import shared.controls 1.0 -import shared.stores 1.0 - -StatusModal { - property bool pairingPasswordFieldValid: false - property bool submitted: false - - id: popup - header.title: qsTr("Insert pairing code") - anchors.centerIn: parent - height: 400 - - onOpened: { - submitted = false - pairingPasswordField.text = ""; - pairingPasswordField.forceActiveFocus(Qt.MouseFocusReason) - } - - contentItem: Item { - Input { - id: pairingPasswordField - anchors.rightMargin: 56 - anchors.leftMargin: 56 - anchors.top: parent.top - anchors.topMargin: 88 - anchors.bottomMargin: 0 - placeholderText: qsTr("Pairing code") - textField.echoMode: TextInput.Password - onTextChanged: { - pairingPasswordFieldValid = pairingPasswordField.text !== ""; - } - } - - StatusBaseText { - text: qsTr("Insert the Keycard pairing code") - wrapMode: Text.WordWrap - anchors.right: parent.right - anchors.left: parent.left - anchors.bottom: parent.bottom - anchors.bottomMargin: 20 - horizontalAlignment: Text.AlignHCenter - color: Theme.palette.directColor1 - font.pixelSize: 12 - } - } - - rightButtons: [ - StatusButton { - id: submitBtn - text: qsTr("Pair") - enabled: pairingPasswordFieldValid - - onClicked: { - submitted = true - // Not Refactored Yet - RootStore.keycardModelInst.pair(pairingPasswordField.text) - popup.close() - } - } - ] -} diff --git a/ui/imports/shared/keycard/qmldir b/ui/imports/shared/keycard/qmldir deleted file mode 100644 index 3a9fbf66a1..0000000000 --- a/ui/imports/shared/keycard/qmldir +++ /dev/null @@ -1,3 +0,0 @@ -InsertCard 1.0 InsertCard.qml -PairingModal 1.0 PairingModal.qml -PINModal 1.0 PINModal.qml diff --git a/ui/imports/shared/stores/RootStore.qml b/ui/imports/shared/stores/RootStore.qml index a3ccda7e16..f2f48ab39a 100644 --- a/ui/imports/shared/stores/RootStore.qml +++ b/ui/imports/shared/stores/RootStore.qml @@ -7,7 +7,6 @@ QtObject { // property var utilsModelInst: !!utilsModel ? utilsModel : null // property var chatsModelInst: !!chatsModel ?chatsModel : null // property var walletModelInst: !!walletModel ? walletModel : null -// property var keycardModelInst: !!keycardModel ? keycardModel : null // property var profileModelInst: !!profileModel ? profileModel : null property var profileSectionModuleInst: profileSectionModule diff --git a/ui/imports/utils/Constants.qml b/ui/imports/utils/Constants.qml index e4b852ff6a..063961af90 100644 --- a/ui/imports/utils/Constants.qml +++ b/ui/imports/utils/Constants.qml @@ -16,6 +16,7 @@ QtObject { readonly property string firstRunNewUserNewKeys: "FirstRunNewUserNewKeys" readonly property string firstRunNewUserNewKeycardKeys: "FirstRunNewUserNewKeycardKeys" readonly property string firstRunNewUserImportSeedPhrase: "FirstRunNewUserImportSeedPhrase" + readonly property string firstRunNewUserImportSeedPhraseIntoKeycard: "FirstRunNewUserImportSeedPhraseIntoKeycard" readonly property string firstRunOldUserSyncCode: "FirstRunOldUserSyncCode" readonly property string firstRunOldUserKeycardImport: "FirstRunOldUserKeycardImport" readonly property string firstRunOldUserImportSeedPhrase: "FirstRunOldUserImportSeedPhrase" @@ -35,7 +36,47 @@ QtObject { readonly property string userProfileImportSeedPhrase: "UserProfileImportSeedPhrase" readonly property string userProfileEnterSeedPhrase: "UserProfileEnterSeedPhrase" readonly property string biometrics: "Biometrics" + readonly property string keycardPluginReader: "KeycardPluginReader" + readonly property string keycardInsertKeycard: "KeycardInsertKeycard" + readonly property string keycardReadingKeycard: "KeycardReadingKeycard" + readonly property string keycardCreatePin: "KeycardCreatePin" + readonly property string keycardRepeatPin: "KeycardRepeatPin" + readonly property string keycardPinSet: "KeycardPinSet" + readonly property string keycardEnterPin: "KeycardEnterPin" + readonly property string keycardWrongPin: "KeycardWrongPin" + readonly property string keycardEnterPuk: "KeycardEnterPuk" + readonly property string keycardWrongPuk: "KeycardWrongPuk" + readonly property string keycardDisplaySeedPhrase: "KeycardDisplaySeedPhrase" + readonly property string keycardEnterSeedPhraseWords: "KeycardEnterSeedPhraseWords" + readonly property string keycardNotEmpty: "KeycardNotEmpty" + readonly property string keycardEmpty: "KeycardEmpty" + readonly property string keycardLocked: "KeycardLocked" + readonly property string keycardRecover: "KeycardRecover" + readonly property string keycardMaxPairingSlotsReached: "KeycardMaxPairingSlotsReached" + readonly property string keycardMaxPinRetriesReached: "KeycardMaxPinRetriesReached" + readonly property string keycardMaxPukRetriesReached: "KeycardMaxPukRetriesReached" readonly property string login: "Login" + readonly property string loginKeycardInsertKeycard: "LoginKeycardInsertKeycard" + readonly property string loginKeycardReadingKeycard: "LoginKeycardReadingKeycard" + readonly property string loginKeycardEnterPin: "LoginKeycardEnterPin" + readonly property string loginKeycardWrongKeycard: "LoginKeycardWrongKeycard" + readonly property string loginKeycardWrongPin: "LoginKeycardWrongPin" + readonly property string loginKeycardMaxPinRetriesReached: "LoginKeycardMaxPinRetriesReached" + readonly property string loginKeycardMaxPukRetriesReached: "LoginKeycardMaxPukRetriesReached" + readonly property string loginKeycardEmpty: "LoginKeycardEmpty" + } + + readonly property QtObject keychain: QtObject { + readonly property QtObject errorType: QtObject { + readonly property string authentication: "authentication" + readonly property string keychain: "keychain" + } + + readonly property QtObject storedValue: QtObject { + readonly property string store: "store" + readonly property string notNow: "notNow" + readonly property string never: "never" + } } readonly property QtObject appSection: QtObject { @@ -254,6 +295,24 @@ QtObject { readonly property int stable: 2 } + readonly property QtObject keycard: QtObject { + + readonly property QtObject general: QtObject { + readonly property int footerWrapperHeight: 125 + readonly property int keycardPinLength: 6 + readonly property int fontSize1: 22 + readonly property int fontSize2: 15 + readonly property int fontSize3: 12 + readonly property int seedPhraseCellWidth: 193 + readonly property int seedPhraseCellHeight: 60 + readonly property int seedPhraseCellNumberWidth: 24 + readonly property int seedPhraseCellFontSize: 12 + readonly property int buttonFontSize: 15 + readonly property int pukCellWidth: 50 + readonly property int pukCellHeight: 60 + } + } + readonly property int communityImported: 0 readonly property int communityImportingInProgress: 1 readonly property int communityImportingError: 2 @@ -377,10 +436,6 @@ QtObject { readonly property string ens_connected: "connected" readonly property string ens_connected_dkey: "connected-different-key" - readonly property string storeToKeychainValueStore: "store" - readonly property string storeToKeychainValueNotNow: "notNow" - readonly property string storeToKeychainValueNever: "never" - readonly property string editLabel: ` ` + qsTr("(edited)") + `` readonly property string newBookmark: " " diff --git a/ui/imports/utils/Utils.qml b/ui/imports/utils/Utils.qml index af595debaf..383a7dc261 100644 --- a/ui/imports/utils/Utils.qml +++ b/ui/imports/utils/Utils.qml @@ -8,6 +8,10 @@ import StatusQ.Core.Theme 0.1 QtObject { property var globalUtilsInst: globalUtils + function isDigit(value) { + return /^\d$/.test(value); + } + function isHex(value) { return /^(-0x|0x)?[0-9a-fA-F]*$/i.test(value) } diff --git a/vendor/nim-keycard-go b/vendor/nim-keycard-go index 126fbc9dc2..6a4bed51d5 160000 --- a/vendor/nim-keycard-go +++ b/vendor/nim-keycard-go @@ -1 +1 @@ -Subproject commit 126fbc9dc2b3d0740f99269753772643ab298a75 +Subproject commit 6a4bed51d53f3ef8d1eaff038e08449c288c8334 diff --git a/vendor/nim-status-go b/vendor/nim-status-go index 7fa256afd2..c8f5bf3737 160000 --- a/vendor/nim-status-go +++ b/vendor/nim-status-go @@ -1 +1 @@ -Subproject commit 7fa256afd2011f488bfb08bfc660c1499fe1f8d2 +Subproject commit c8f5bf373771a586400d98832f9652eba9901681 diff --git a/vendor/status-keycard-go b/vendor/status-keycard-go new file mode 160000 index 0000000000..bfe9a1c7c7 --- /dev/null +++ b/vendor/status-keycard-go @@ -0,0 +1 @@ +Subproject commit bfe9a1c7c76fc3c3d2b3b452d969f6dad6903d09